mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-22 10:49:59 +00:00
* fix(banking): correct usage of hooks in rule action * fix(banking): apply ESLint rules for hooks * fix(banking): add lazy imports and code-splitting
274 lines
8.1 KiB
TypeScript
274 lines
8.1 KiB
TypeScript
import * as React from "react"
|
|
import { Tabs as TabsPrimitive, Dialog as DialogPrimitive } from "radix-ui"
|
|
import { cn } from "@/lib/utils"
|
|
import { DialogContent } from "./dialog"
|
|
|
|
/**
|
|
* Sample Usage:
|
|
*
|
|
* <Dialog open={open} onOpenChange={setOpen}>
|
|
* <DialogTrigger>
|
|
* ...your content...
|
|
* </DialogTrigger>
|
|
*
|
|
* <SettingsDialog onClose={() => setOpen(false)} defaultValue="preferences">
|
|
* <SettingsTabs>
|
|
* <SettingsTabGroup header="Configuration">
|
|
* <SettingsTabItem icon={<SlidersVerticalIcon />} label="Preferences" value="preferences" />
|
|
* <SettingsTabItem icon={<ZapIcon />} label="Matching Rules" value="rules" />
|
|
* </SettingsTabGroup>
|
|
* <SettingsTabGroup header="Setup">
|
|
* <SettingsTabItem icon={<LandmarkIcon />} label="Bank Accounts" value="bank-accounts" />
|
|
* <SettingsTabItem icon={<ListIcon />} label="Masters" value="masters" />
|
|
* </SettingsTabGroup>
|
|
* </SettingsTabs>
|
|
*
|
|
* <SettingsPanels>
|
|
* <SettingsPanel value="preferences"><Preferences /></SettingsPanel>
|
|
* <SettingsPanel value="rules"><MatchingRules /></SettingsPanel>
|
|
* <SettingsPanel value="bank-accounts"><BankAccounts /></SettingsPanel>
|
|
* <SettingsPanel value="masters"><Masters /></SettingsPanel>
|
|
* </SettingsPanels>
|
|
* </SettingsDialog>
|
|
* </Dialog>
|
|
*/
|
|
|
|
type SettingsDialogContextValue = {
|
|
onClose?: VoidFunction
|
|
}
|
|
|
|
const SettingsDialogContext = React.createContext<SettingsDialogContextValue>({})
|
|
|
|
/**
|
|
* Exposes `onClose` to descendant panels so they can dismiss the dialog after
|
|
* a successful save without prop-drilling.
|
|
*/
|
|
export const useSettingsDialog = () => React.useContext(SettingsDialogContext)
|
|
|
|
type SettingsDialogProps = Omit<
|
|
React.ComponentProps<typeof TabsPrimitive.Root>,
|
|
"orientation"
|
|
> & {
|
|
onClose?: VoidFunction
|
|
contentClassName?: string
|
|
}
|
|
|
|
function SettingsDialog({
|
|
children,
|
|
className,
|
|
contentClassName,
|
|
onClose,
|
|
...props
|
|
}: SettingsDialogProps) {
|
|
const contextValue = React.useMemo(() => ({ onClose }), [onClose])
|
|
|
|
return (
|
|
<DialogContent className={cn("min-w-5xl max-lg:min-w-[98vw] p-0 overflow-y-hidden", contentClassName)} showCloseButton={false}>
|
|
<SettingsDialogContext.Provider value={contextValue}>
|
|
<TabsPrimitive.Root
|
|
data-slot="settings-dialog"
|
|
orientation="vertical"
|
|
className={cn(
|
|
"flex h-[calc(100vh-8rem)] bg-surface-menu-bar",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</TabsPrimitive.Root>
|
|
</SettingsDialogContext.Provider>
|
|
</DialogContent>
|
|
)
|
|
}
|
|
|
|
function SettingsTabs({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
return (
|
|
<TabsPrimitive.List
|
|
data-slot="settings-tabs"
|
|
className={cn(
|
|
"flex flex-col w-56 bg-surface-menu-bar rounded-s-lg shrink-0 overflow-y-auto m-1",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
type SettingsTabGroupProps = React.ComponentProps<"div"> & {
|
|
header?: React.ReactNode
|
|
}
|
|
|
|
function SettingsTabGroup({
|
|
children,
|
|
header,
|
|
className,
|
|
...props
|
|
}: SettingsTabGroupProps) {
|
|
return (
|
|
<div data-slot="settings-tab-group" className={className} {...props}>
|
|
{header && (
|
|
<div className="h-7.5 px-2 py-[7px] my-[3px] flex cursor-default gap-1.5 text-xs font-medium text-ink-gray-5 transition-all duration-300 ease-in-out sticky top-0 z-10 bg-surface-menu-bar">
|
|
<span>{header}</span>
|
|
</div>
|
|
)}
|
|
<nav className="space-y-[3px] px-1">{children}</nav>
|
|
<div className="mb-0.5 mt-[5px]"></div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
type SettingsTabItemProps = React.ComponentProps<typeof TabsPrimitive.Trigger> & {
|
|
icon?: React.ReactNode
|
|
label: React.ReactNode
|
|
}
|
|
|
|
function SettingsTabItem({
|
|
icon,
|
|
label,
|
|
className,
|
|
...props
|
|
}: SettingsTabItemProps) {
|
|
return (
|
|
<TabsPrimitive.Trigger
|
|
data-slot="settings-tab-item"
|
|
className={cn(
|
|
"flex h-7.5 cursor-pointer items-center rounded text-ink-gray-6 duration-300 ease-in-out focus:outline-none focus:transition-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-outline-gray-3 w-full",
|
|
"hover:bg-surface-gray-3",
|
|
"data-[state=active]:bg-surface-selected data-[state=active]:shadow-sm data-[state=active]:hover:bg-surface-selected",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div className="flex w-full items-center justify-between duration-300 ease-in-out px-2 py-[7px]">
|
|
<div className="flex items-center truncate">
|
|
{icon && (
|
|
<div className="[&_svg:not([class*='size-'])]:size-4 text-ink-gray-6 [&_svg:not([class*='text-'])]:text-ink-gray-6">
|
|
{icon}
|
|
</div>
|
|
)}
|
|
<span
|
|
className={cn(
|
|
"flex-1 shrink-0 truncate text-sm leading-4 duration-300 ease-in-out w-auto opacity-100 text-ink-gray-6",
|
|
icon && "ms-2"
|
|
)}
|
|
>
|
|
{label}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</TabsPrimitive.Trigger>
|
|
)
|
|
}
|
|
|
|
function SettingsPanels({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<"div">) {
|
|
return (
|
|
<div
|
|
data-slot="settings-panels"
|
|
className={cn(
|
|
"flex flex-col flex-1 overflow-y-auto bg-surface-modal",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function SettingsPanel({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
return (
|
|
<TabsPrimitive.Content
|
|
data-slot="settings-panel"
|
|
className={cn("flex flex-col h-full w-full text-ink-gray-8 py-8 px-6 gap-6", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Usage:
|
|
*
|
|
* <SettingsPanelHeader actions={<><Button>Add</Button></>}>
|
|
*
|
|
* <SettingsPanelTitle>Settings</SettingsPanelTitle>
|
|
* <SettingsPanelDescription>Settings description</SettingsPanelDescription>
|
|
*
|
|
* </SettingsPanelHeader>
|
|
*/
|
|
function SettingsPanelHeader({
|
|
className,
|
|
children,
|
|
actions,
|
|
...props
|
|
}: React.ComponentProps<"div"> & { actions?: React.ReactNode }) {
|
|
return (
|
|
<div
|
|
data-slot="dialog-header"
|
|
className={cn("flex justify-between items-start px-2 text-ink-gray-7", className)}
|
|
{...props}
|
|
>
|
|
<div className="flex flex-col gap-1 w-full">
|
|
{children}
|
|
</div>
|
|
<div className="flex item-center space-x-2 w-fit justify-end">
|
|
{actions}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SettingsPanelTitle({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
return (
|
|
<DialogPrimitive.Title
|
|
data-slot="dialog-title"
|
|
className={cn("flex gap-2 text-xl font-semibold leading-none h-5", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function SettingsPanelDescription({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
return (
|
|
<DialogPrimitive.Description
|
|
data-slot="dialog-description"
|
|
className={cn("text-p-base text-ink-gray-6", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function SettingsPanelContent({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<"div">) {
|
|
return (
|
|
<div className={cn("flex-1 flex flex-col overflow-y-auto px-2", className)} {...props} />
|
|
)
|
|
}
|
|
|
|
export {
|
|
SettingsDialog,
|
|
SettingsTabs,
|
|
SettingsTabGroup,
|
|
SettingsTabItem,
|
|
SettingsPanels,
|
|
SettingsPanel,
|
|
SettingsPanelHeader,
|
|
SettingsPanelTitle,
|
|
SettingsPanelDescription,
|
|
SettingsPanelContent
|
|
}
|