feat: new banking module (#54720)

* feat: initial SPA setup for banking

* wip: bring over new banking module

* feat: added Espresso design tokens

* feat: button styles

* fix: add all ink colors

* wip: espresso design system changes

* feat: button and badge espresso components

* fix: button styling for reconcile

* feat: Espresso progress bar

* feat: Espresso toggle switch

* feat: Espresso tabs design

* fix: vertical tab support

* fix: button sizing across modals

* feat: Espresso style table layout

* feat: Espresso tooltip

* feat: Espresso elevations and checkbox

* feat: Dialog with Espresso styles

* feat: Espresso textarea

* fix: input styles

* fix: colors on bank picker

* fix: breadcrumb styling

* fix: bank picker styling

* feat: create doctypes and fields for bank reconciliation

* feat: APIs for banking

* fix: use date format parser

* fix: font styling to match Espresso

* wip: settings modal

* feat: settings dialog component

* fix: icons and invalid requests

* feat: preferences tab

* fix: adjust icon stroke width to 1.5

* feat: rule configuration in settings

* fix: remove sheet component

* feat: alert and error banner component

* feat: dropdown in Espresso

* feat: popover and select in Espresso

* fix: cleanup more styles

* fix: match size of link fields

* feat: command styling

* fix: remove unused style tokens

* fix: styles for global date picker dropdown

* fix: styles for match and reconcile

* feat: table Espresso component

* feat: remove all other design tokens

* fix: remove unused tokens

* fix: form elements

* fix: remove unused styles and fix filters in bank transaction list

* feat: fetch bank rec doctypes for filtering

* fix: record payment modal

* feat: support for dark mode switching

* fix: move bank logos to public folder

* feat: add support for RTL

* feat: support for RTL

* chore: send layout direction in dev boot

* fix: make checkbox work in RTL

* feat: dark mode support

* fix: dark mode style

* feat: bank logos in dark mode

* feat: dark mode bank logos

* chore: use dark mode bank logos everywhere

* chore: move rule evaluation to controller

* chore: add tests for bank transaction rules

* fix: move deps to fix actions errors

* fix: move tw-animate-css to deps

* fix: remove shadcn

* fix: do not open modal if no transactions selected

* fix: add translation strings

* feat: add banner on existing bank reconciliation tool

* feat: bank statement import

* fix: translations and layout directions

* fix: validation for transaction matching rule

* fix: styles

* fix: show conflicting transactions in alert

* fix: show help text for new banking module forms

* feat: show total debits and credits

* fix: dark mode colors in automatic config

* feat: add keyboard shortcuts help

* feat: added keyboard shortcut for settings

* fix: decrease size of progress bar

* chore: bump packages

* feat: add tests for statement import

* fix: settings dialog

* fix: show banner on small screens

* fix: show banner when no bank account set
This commit is contained in:
Nikhil Kothari
2026-05-09 23:14:58 +05:30
committed by GitHub
parent 332026fe5e
commit 6de5367f12
262 changed files with 39467 additions and 14 deletions

View File

@@ -0,0 +1,273 @@
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 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
}