mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
Merge remote-tracking branch 'origin/release/0.6' into refactor/optimize-ui-bundle-size
# Conflicts: # app/build.ts # app/package.json
This commit is contained in:
@@ -4,15 +4,15 @@ import { twMerge } from "tailwind-merge";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
|
||||
const sizes = {
|
||||
small: "px-2 py-1.5 rounded-md gap-1.5 text-sm",
|
||||
default: "px-3 py-2.5 rounded-md gap-2.5",
|
||||
large: "px-4 py-3 rounded-md gap-3 text-lg"
|
||||
small: "px-2 py-1.5 rounded-md gap-1 text-sm",
|
||||
default: "px-3 py-2.5 rounded-md gap-1.5",
|
||||
large: "px-4 py-3 rounded-md gap-2.5 text-lg"
|
||||
};
|
||||
|
||||
const iconSizes = {
|
||||
small: 15,
|
||||
default: 18,
|
||||
large: 22
|
||||
small: 12,
|
||||
default: 16,
|
||||
large: 20
|
||||
};
|
||||
|
||||
const styles = {
|
||||
|
||||
@@ -10,9 +10,9 @@ export type IconType =
|
||||
|
||||
const styles = {
|
||||
xs: { className: "p-0.5", size: 13 },
|
||||
sm: { className: "p-0.5", size: 16 },
|
||||
md: { className: "p-1", size: 20 },
|
||||
lg: { className: "p-1.5", size: 24 }
|
||||
sm: { className: "p-0.5", size: 15 },
|
||||
md: { className: "p-1", size: 18 },
|
||||
lg: { className: "p-1.5", size: 22 }
|
||||
} as const;
|
||||
|
||||
interface IconButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
|
||||
@@ -6,16 +6,27 @@ export type AlertProps = ComponentPropsWithoutRef<"div"> & {
|
||||
visible?: boolean;
|
||||
title?: string;
|
||||
message?: ReactNode | string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const Base: React.FC<AlertProps> = ({ visible = true, title, message, className, ...props }) =>
|
||||
const Base: React.FC<AlertProps> = ({
|
||||
visible = true,
|
||||
title,
|
||||
message,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) =>
|
||||
visible ? (
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge("flex flex-row dark:bg-amber-300/20 bg-amber-200 p-4", className)}
|
||||
className={twMerge(
|
||||
"flex flex-row items-center dark:bg-amber-300/20 bg-amber-200 p-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{title && <b className="mr-2">{title}:</b>}
|
||||
{message}
|
||||
{message || children}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { Button } from "../buttons/Button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button, type ButtonProps } from "../buttons/Button";
|
||||
|
||||
export type EmptyProps = {
|
||||
Icon?: any;
|
||||
title?: string;
|
||||
description?: string;
|
||||
buttonText?: string;
|
||||
buttonOnClick?: () => void;
|
||||
primary?: ButtonProps;
|
||||
secondary?: ButtonProps;
|
||||
className?: string;
|
||||
};
|
||||
export const Empty: React.FC<EmptyProps> = ({
|
||||
Icon = undefined,
|
||||
title = undefined,
|
||||
description = "Check back later my friend.",
|
||||
buttonText,
|
||||
buttonOnClick
|
||||
primary,
|
||||
secondary,
|
||||
className
|
||||
}) => (
|
||||
<div className="flex flex-col h-full w-full justify-center items-center">
|
||||
<div className={twMerge("flex flex-col h-full w-full justify-center items-center", className)}>
|
||||
<div className="flex flex-col gap-3 items-center max-w-80">
|
||||
{Icon && <Icon size={48} className="opacity-50" stroke={1} />}
|
||||
<div className="flex flex-col gap-1">
|
||||
{title && <h3 className="text-center text-lg font-bold">{title}</h3>}
|
||||
<p className="text-center text-primary/60">{description}</p>
|
||||
</div>
|
||||
{buttonText && (
|
||||
<div className="mt-1.5">
|
||||
<Button variant="primary" onClick={buttonOnClick}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1.5 flex flex-row gap-2">
|
||||
{secondary && <Button variant="default" {...secondary} />}
|
||||
{primary && <Button variant="primary" {...primary} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
import { IconLockAccessOff } from "@tabler/icons-react";
|
||||
import { Empty, type EmptyProps } from "./Empty";
|
||||
|
||||
const NotFound = (props: Partial<EmptyProps>) => <Empty title="Not Found" {...props} />;
|
||||
const NotAllowed = (props: Partial<EmptyProps>) => <Empty title="Not Allowed" {...props} />;
|
||||
const MissingPermission = ({
|
||||
what,
|
||||
...props
|
||||
}: Partial<EmptyProps> & {
|
||||
what?: string;
|
||||
}) => (
|
||||
<Empty
|
||||
Icon={IconLockAccessOff}
|
||||
title="Missing Permission"
|
||||
description={`You're not allowed to access ${what ?? "this"}.`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Message = {
|
||||
NotFound
|
||||
NotFound,
|
||||
NotAllowed,
|
||||
MissingPermission
|
||||
};
|
||||
|
||||
29
app/src/ui/components/form/Formy/BooleanInputMantine.tsx
Normal file
29
app/src/ui/components/form/Formy/BooleanInputMantine.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Switch } from "@mantine/core";
|
||||
import { forwardRef, useEffect, useState } from "react";
|
||||
|
||||
export const BooleanInputMantine = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
(props, ref) => {
|
||||
const [checked, setChecked] = useState(Boolean(props.value));
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(Boolean(props.value));
|
||||
}, [props.value]);
|
||||
|
||||
function handleCheck(e) {
|
||||
setChecked(e.target.checked);
|
||||
props.onChange?.(e.target.checked);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<Switch
|
||||
ref={ref}
|
||||
checked={checked}
|
||||
onChange={handleCheck}
|
||||
disabled={props.disabled}
|
||||
id={props.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Switch } from "@mantine/core";
|
||||
import { getBrowser } from "core/utils";
|
||||
import type { Field } from "data";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
import { TbCalendar, TbChevronDown, TbInfoCircle } from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useEvent } from "../../hooks/use-event";
|
||||
import { IconButton } from "../buttons/IconButton";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
|
||||
export const Group: React.FC<React.ComponentProps<"div"> & { error?: boolean }> = ({
|
||||
error,
|
||||
@@ -131,17 +130,6 @@ export const BooleanInput = forwardRef<HTMLInputElement, React.ComponentProps<"i
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<Switch
|
||||
ref={ref}
|
||||
checked={checked}
|
||||
onChange={handleCheck}
|
||||
disabled={props.disabled}
|
||||
id={props.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
/*return (
|
||||
<div className="h-11 flex items-center">
|
||||
<input
|
||||
{...props}
|
||||
@@ -153,7 +141,7 @@ export const BooleanInput = forwardRef<HTMLInputElement, React.ComponentProps<"i
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</div>
|
||||
);*/
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
17
app/src/ui/components/form/Formy/index.ts
Normal file
17
app/src/ui/components/form/Formy/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { BooleanInputMantine } from "./BooleanInputMantine";
|
||||
import { DateInput, Input, Textarea } from "./components";
|
||||
|
||||
export const formElementFactory = (element: string, props: any) => {
|
||||
switch (element) {
|
||||
case "date":
|
||||
return DateInput;
|
||||
case "boolean":
|
||||
return BooleanInputMantine;
|
||||
case "textarea":
|
||||
return Textarea;
|
||||
default:
|
||||
return Input;
|
||||
}
|
||||
};
|
||||
|
||||
export * from "./components";
|
||||
@@ -15,12 +15,13 @@ export type JsonSchemaFormProps = any & {
|
||||
schema: RJSFSchema | Schema;
|
||||
uiSchema?: any;
|
||||
direction?: "horizontal" | "vertical";
|
||||
onChange?: (value: any) => void;
|
||||
onChange?: (value: any, isValid: () => boolean) => void;
|
||||
};
|
||||
|
||||
export type JsonSchemaFormRef = {
|
||||
formData: () => any;
|
||||
validateForm: () => boolean;
|
||||
silentValidate: () => boolean;
|
||||
cancel: () => void;
|
||||
};
|
||||
|
||||
@@ -52,15 +53,18 @@ export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>
|
||||
const handleChange = ({ formData }: any, e) => {
|
||||
const clean = JSON.parse(JSON.stringify(formData));
|
||||
//console.log("Data changed: ", clean, JSON.stringify(formData, null, 2));
|
||||
onChange?.(clean);
|
||||
setValue(clean);
|
||||
onChange?.(clean, () => isValid(clean));
|
||||
};
|
||||
|
||||
const isValid = (data: any) => validator.validateFormData(data, schema).errors.length === 0;
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
formData: () => value,
|
||||
validateForm: () => formRef.current!.validateForm(),
|
||||
silentValidate: () => isValid(value),
|
||||
cancel: () => formRef.current!.reset()
|
||||
}),
|
||||
[value]
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useClickOutside } from "@mantine/hooks";
|
||||
import { Fragment, type ReactElement, cloneElement, useState } from "react";
|
||||
import { clampNumber } from "core/utils";
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
Fragment,
|
||||
type ReactElement,
|
||||
cloneElement,
|
||||
useState
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useEvent } from "../../hooks/use-event";
|
||||
import { useEvent } from "ui/hooks/use-event";
|
||||
|
||||
export type DropdownItem =
|
||||
| (() => JSX.Element)
|
||||
@@ -14,26 +21,33 @@ export type DropdownItem =
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type DropdownClickableChild = ReactElement<{ onClick: () => void }>;
|
||||
export type DropdownProps = {
|
||||
className?: string;
|
||||
openEvent?: "onClick" | "onContextMenu";
|
||||
defaultOpen?: boolean;
|
||||
title?: string | ReactElement;
|
||||
dropdownWrapperProps?: Omit<ComponentPropsWithoutRef<"div">, "style">;
|
||||
position?: "bottom-start" | "bottom-end" | "top-start" | "top-end";
|
||||
hideOnEmpty?: boolean;
|
||||
items: (DropdownItem | undefined | boolean)[];
|
||||
itemsClassName?: string;
|
||||
children: ReactElement<{ onClick: () => void }>;
|
||||
children: DropdownClickableChild;
|
||||
onClickItem?: (item: DropdownItem) => void;
|
||||
renderItem?: (
|
||||
item: DropdownItem,
|
||||
props: { key: number; onClick: () => void }
|
||||
) => ReactElement<{ onClick: () => void }>;
|
||||
) => DropdownClickableChild;
|
||||
};
|
||||
|
||||
export function Dropdown({
|
||||
children,
|
||||
defaultOpen = false,
|
||||
position = "bottom-start",
|
||||
openEvent = "onClick",
|
||||
position: initialPosition = "bottom-start",
|
||||
dropdownWrapperProps,
|
||||
items,
|
||||
title,
|
||||
hideOnEmpty = true,
|
||||
onClickItem,
|
||||
renderItem,
|
||||
@@ -41,19 +55,58 @@ export function Dropdown({
|
||||
className
|
||||
}: DropdownProps) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const [position, setPosition] = useState(initialPosition);
|
||||
const clickoutsideRef = useClickOutside(() => setOpen(false));
|
||||
const menuItems = items.filter(Boolean) as DropdownItem[];
|
||||
const [_offset, _setOffset] = useState(0);
|
||||
|
||||
const toggle = useEvent((delay: number = 50) =>
|
||||
setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0)
|
||||
);
|
||||
|
||||
const onClickHandler = openEvent === "onClick" ? toggle : undefined;
|
||||
const onContextMenuHandler = useEvent((e) => {
|
||||
if (openEvent !== "onContextMenu") return;
|
||||
e.preventDefault();
|
||||
|
||||
if (open) {
|
||||
toggle(0);
|
||||
setTimeout(() => {
|
||||
setPosition(initialPosition);
|
||||
_setOffset(0);
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
// minimal popper impl, get pos and boundaries
|
||||
const x = e.clientX - e.currentTarget.getBoundingClientRect().left;
|
||||
const { left = 0, right = 0 } = clickoutsideRef.current?.getBoundingClientRect() ?? {};
|
||||
|
||||
// only if boundaries gien
|
||||
if (left > 0 && right > 0) {
|
||||
const safe = clampNumber(x, left, right);
|
||||
// if pos less than half, go left
|
||||
if (x < (left + right) / 2) {
|
||||
setPosition("bottom-start");
|
||||
_setOffset(safe);
|
||||
} else {
|
||||
setPosition("bottom-end");
|
||||
_setOffset(right - safe);
|
||||
}
|
||||
} else {
|
||||
setPosition(initialPosition);
|
||||
_setOffset(0);
|
||||
}
|
||||
|
||||
toggle();
|
||||
});
|
||||
|
||||
const offset = 4;
|
||||
const dropdownStyle = {
|
||||
"bottom-start": { top: "100%", left: 0, marginTop: offset },
|
||||
"bottom-end": { right: 0, top: "100%", marginTop: offset },
|
||||
"bottom-start": { top: "100%", left: _offset, marginTop: offset },
|
||||
"bottom-end": { right: _offset, top: "100%", marginTop: offset },
|
||||
"top-start": { bottom: "100%", marginBottom: offset },
|
||||
"top-end": { bottom: "100%", right: 0, marginBottom: offset }
|
||||
"top-end": { bottom: "100%", right: _offset, marginBottom: offset }
|
||||
}[position];
|
||||
|
||||
const internalOnClickItem = useEvent((item) => {
|
||||
@@ -94,13 +147,25 @@ export function Dropdown({
|
||||
));
|
||||
|
||||
return (
|
||||
<div role="dropdown" className={twMerge("relative flex", className)} ref={clickoutsideRef}>
|
||||
{cloneElement(children as any, { onClick: toggle })}
|
||||
<div
|
||||
role="dropdown"
|
||||
className={twMerge("relative flex", className)}
|
||||
ref={clickoutsideRef}
|
||||
onContextMenu={onContextMenuHandler}
|
||||
>
|
||||
{cloneElement(children as any, { onClick: onClickHandler })}
|
||||
{open && (
|
||||
<div
|
||||
className="absolute z-30 flex flex-col bg-background border border-muted px-1 py-1 rounded-lg shadow-lg min-w-full"
|
||||
{...dropdownWrapperProps}
|
||||
className={twMerge(
|
||||
"absolute z-30 flex flex-col bg-background border border-muted px-1 py-1 rounded-lg shadow-lg min-w-full",
|
||||
dropdownWrapperProps?.className
|
||||
)}
|
||||
style={dropdownStyle}
|
||||
>
|
||||
{title && (
|
||||
<div className="text-sm font-bold px-2.5 mb-1 mt-1 opacity-50">{title}</div>
|
||||
)}
|
||||
{menuItems.map((item, i) =>
|
||||
itemRenderer(item, { key: i, onClick: () => internalOnClickItem(item) })
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
export type TStepsProps = {
|
||||
children: any;
|
||||
initialPath?: string[];
|
||||
initialState?: any;
|
||||
lastBack?: () => void;
|
||||
[key: string]: any;
|
||||
};
|
||||
@@ -19,13 +20,14 @@ type TStepContext<T = any> = {
|
||||
stepBack: () => void;
|
||||
close: () => void;
|
||||
state: T;
|
||||
path: string[];
|
||||
setState: Dispatch<SetStateAction<T>>;
|
||||
};
|
||||
|
||||
const StepContext = createContext<TStepContext>(undefined as any);
|
||||
|
||||
export function Steps({ children, initialPath = [], lastBack }: TStepsProps) {
|
||||
const [state, setState] = useState<any>({});
|
||||
export function Steps({ children, initialPath = [], initialState = {}, lastBack }: TStepsProps) {
|
||||
const [state, setState] = useState<any>(initialState);
|
||||
const [path, setPath] = useState<string[]>(initialPath);
|
||||
const steps: any[] = Children.toArray(children).filter(
|
||||
(child: any) => child.props.disabled !== true
|
||||
@@ -46,7 +48,7 @@ export function Steps({ children, initialPath = [], lastBack }: TStepsProps) {
|
||||
const current = steps.find((step) => step.props.id === path[path.length - 1]) || steps[0];
|
||||
|
||||
return (
|
||||
<StepContext.Provider value={{ nextStep, stepBack, state, setState, close: lastBack! }}>
|
||||
<StepContext.Provider value={{ nextStep, stepBack, state, path, setState, close: lastBack! }}>
|
||||
{current}
|
||||
</StepContext.Provider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user