add front-urban

This commit is contained in:
Valentin Romero
2026-06-30 22:08:11 -03:00
parent 25d4ba6cc0
commit 03294769a4
266 changed files with 38239 additions and 0 deletions
@@ -0,0 +1,51 @@
'use client'
// React Imports
import { forwardRef } from 'react'
// MUI Imports
import MuiAvatar from '@mui/material/Avatar'
import { lighten, styled } from '@mui/material/styles'
import type { AvatarProps } from '@mui/material/Avatar'
// Type Imports
import type { ThemeColor } from '@core/types'
export type CustomAvatarProps = AvatarProps & {
color?: ThemeColor
skin?: 'filled' | 'light' | 'light-static'
size?: number
}
const Avatar = styled(MuiAvatar)<CustomAvatarProps>(({ skin, color, size, theme }) => {
return {
...(color &&
skin === 'light' && {
backgroundColor: `var(--mui-palette-${color}-lightOpacity)`,
color: `var(--mui-palette-${color}-main)`
}),
...(color &&
skin === 'light-static' && {
backgroundColor: lighten(theme.palette[color as ThemeColor].main, 0.84),
color: `var(--mui-palette-${color}-main)`
}),
...(color &&
skin === 'filled' && {
backgroundColor: `var(--mui-palette-${color}-main)`,
color: `var(--mui-palette-${color}-contrastText)`
}),
...(size && {
height: size,
width: size
})
}
})
const CustomAvatar = forwardRef<HTMLDivElement, CustomAvatarProps>((props: CustomAvatarProps, ref) => {
// Props
const { color, skin = 'filled', ...rest } = props
return <Avatar color={color} skin={skin} ref={ref} {...rest} />
})
export default CustomAvatar
@@ -0,0 +1,75 @@
'use client'
import MuiButton from '@mui/material/Button'
import { styled } from '@mui/material/styles'
// Config Imports
import themeConfig from '@configs/themeConfig'
const CustomIconButton = styled(MuiButton)(({ color, size, theme, variant }) => {
return {
minInlineSize: 0,
...(size === 'small'
? {
fontSize: '20px',
padding: theme.spacing(variant === 'outlined' ? 1 : 1.25),
'& i, & svg': {
fontSize: 'inherit'
}
}
: {
...(size === 'large'
? {
fontSize: '24px',
padding: theme.spacing(variant === 'outlined' ? 2 : 2.25),
'& i, & svg': {
fontSize: 'inherit'
}
}
: {
fontSize: '22px',
padding: theme.spacing(variant === 'outlined' ? 1.5 : 1.75),
'& i, & svg': {
fontSize: 'inherit'
}
})
}),
...(!color && {
color: 'var(--mui-palette-action-active)',
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active': {
backgroundColor: 'rgb(var(--mui-palette-text-primaryChannel) / 0.08)'
},
...(themeConfig.disableRipple && {
'&.Mui-focusVisible:not(.Mui-disabled)': {
backgroundColor: 'rgb(var(--mui-palette-text-primaryChannel) / 0.08)'
}
}),
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-action-active)'
},
...(variant === 'outlined' && {
border: 'none !important',
...(size === 'small'
? {
padding: theme.spacing(1.5)
}
: {
...(size === 'large'
? {
padding: theme.spacing(2.25)
}
: {
padding: theme.spacing(1.75)
})
})
}),
...(variant === 'contained' && {
boxShadow: 'none !important',
backgroundColor: 'transparent'
})
})
}
}) as typeof MuiButton
export default CustomIconButton
@@ -0,0 +1,135 @@
'use client'
// React Imports
import { useRef, useState } from 'react'
import type { ReactElement, ReactNode, SyntheticEvent } from 'react'
// Next Imports
import Link from 'next/link'
// MUI Imports
import Tooltip from '@mui/material/Tooltip'
import Box from '@mui/material/Box'
import Popper from '@mui/material/Popper'
import MenuItem from '@mui/material/MenuItem'
import MenuList from '@mui/material/MenuList'
import ClickAwayListener from '@mui/material/ClickAwayListener'
import Fade from '@mui/material/Fade'
import Paper from '@mui/material/Paper'
import IconButton from '@mui/material/IconButton'
import Divider from '@mui/material/Divider'
// Third-party Imports
import classnames from 'classnames'
// Type Imports
import type { OptionsMenuType, OptionType, OptionMenuItemType } from './types'
const IconButtonWrapper = (props: Pick<OptionsMenuType, 'tooltipProps'> & { children: ReactElement }) => {
// Props
const { tooltipProps, children } = props
return tooltipProps?.title ? <Tooltip {...tooltipProps}>{children}</Tooltip> : children
}
const MenuItemWrapper = ({ children, option }: { children: ReactNode; option: OptionMenuItemType }) => {
if (option.href) {
return (
<Box component={Link} href={option.href} {...option.linkProps}>
{children}
</Box>
)
} else {
return <>{children}</>
}
}
const OptionMenu = (props: OptionsMenuType) => {
// Props
const { tooltipProps, icon, iconClassName, options, leftAlignMenu, iconButtonProps } = props
// States
const [open, setOpen] = useState(false)
// Refs
const anchorRef = useRef<HTMLButtonElement>(null)
const handleToggle = () => {
setOpen(prevOpen => !prevOpen)
}
const handleClose = (event: Event | SyntheticEvent) => {
if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) {
return
}
setOpen(false)
}
return (
<>
<IconButtonWrapper tooltipProps={tooltipProps}>
<IconButton ref={anchorRef} size='small' onClick={handleToggle} {...iconButtonProps}>
{typeof icon === 'string' ? (
<i className={classnames(icon, iconClassName)} />
) : (icon as ReactNode) ? (
icon
) : (
<i className={classnames('ri-more-2-line', iconClassName)} />
)}
</IconButton>
</IconButtonWrapper>
<Popper
open={open}
anchorEl={anchorRef.current}
placement={leftAlignMenu ? 'bottom-start' : 'bottom-end'}
transition
disablePortal
sx={{ zIndex: 1 }}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps}>
<Paper className='shadow-lg'>
<ClickAwayListener onClickAway={handleClose}>
<MenuList autoFocusItem={open}>
{options.map((option: OptionType, index: number) => {
if (typeof option === 'string') {
return (
<MenuItem key={index} onClick={handleClose}>
{option}
</MenuItem>
)
} else if ('divider' in option) {
return option.divider && <Divider key={index} {...option.dividerProps} />
} else {
return (
<MenuItem
key={index}
{...option.menuItemProps}
{...(option.href && { className: 'p-0' })}
onClick={e => {
handleClose(e)
option.menuItemProps && option.menuItemProps.onClick
? option.menuItemProps.onClick(e)
: null
}}
>
<MenuItemWrapper option={option}>
{(typeof option.icon === 'string' ? <i className={option.icon} /> : option.icon) || null}
{option.text}
</MenuItemWrapper>
</MenuItem>
)
}
})}
</MenuList>
</ClickAwayListener>
</Paper>
</Fade>
)}
</Popper>
</>
)
}
export default OptionMenu
@@ -0,0 +1,42 @@
// React Imports
import type { ReactNode } from 'react'
// Next Imports
import type { LinkProps } from 'next/link'
// MUI Imports
import type { IconButtonProps } from '@mui/material/IconButton'
import type { MenuItemProps } from '@mui/material/MenuItem'
import type { DividerProps } from '@mui/material/Divider'
import type { BoxProps } from '@mui/material/Box'
import type { TooltipProps } from '@mui/material/Tooltip'
export type OptionDividerType = {
divider: boolean
dividerProps?: DividerProps
href?: never
icon?: never
text?: never
linkProps?: never
menuItemProps?: never
}
export type OptionMenuItemType = {
text: ReactNode
icon?: ReactNode
linkProps?: BoxProps
href?: LinkProps['href']
menuItemProps?: MenuItemProps
divider?: never
dividerProps?: never
}
export type OptionType = string | OptionDividerType | OptionMenuItemType
export type OptionsMenuType = {
tooltipProps?: Omit<TooltipProps, 'children'>
icon?: ReactNode
iconClassName?: string
options: OptionType[]
leftAlignMenu?: boolean
iconButtonProps?: IconButtonProps
}
@@ -0,0 +1,122 @@
'use client'
// React Imports
import type { ReactNode } from 'react'
import { createContext, useMemo, useState } from 'react'
// Type Imports
import type { Mode } from '@core/types'
// Config Imports
import themeConfig from '@configs/themeConfig'
// Hook Imports
import { useObjectCookie } from '@core/hooks/useObjectCookie'
// Settings type
export type Settings = {
mode?: Mode
}
// UpdateSettingsOptions type
type UpdateSettingsOptions = {
updateCookie?: boolean
}
// SettingsContextProps type
type SettingsContextProps = {
settings: Settings
updateSettings: (settings: Partial<Settings>, options?: UpdateSettingsOptions) => void
isSettingsChanged: boolean
resetSettings: () => void
updatePageSettings: (settings: Partial<Settings>) => () => void
}
type Props = {
children: ReactNode
settingsCookie: Settings | null
mode?: Mode
}
// Initial Settings Context
export const SettingsContext = createContext<SettingsContextProps | null>(null)
// Settings Provider
export const SettingsProvider = (props: Props) => {
// Initial Settings
const initialSettings: Settings = {
mode: themeConfig.mode
}
const updatedInitialSettings = {
...initialSettings,
mode: props.mode || themeConfig.mode
}
// Cookies
const [settingsCookie, updateSettingsCookie] = useObjectCookie<Settings>(
themeConfig.settingsCookieName,
JSON.stringify(props.settingsCookie) !== '{}' ? props.settingsCookie : updatedInitialSettings
)
// State
const [_settingsState, _updateSettingsState] = useState<Settings>(
JSON.stringify(settingsCookie) !== '{}' ? settingsCookie : updatedInitialSettings
)
const updateSettings = (settings: Partial<Settings>, options?: UpdateSettingsOptions) => {
const { updateCookie = true } = options || {}
_updateSettingsState(prev => {
const newSettings = { ...prev, ...settings }
// Update cookie if needed
if (updateCookie) updateSettingsCookie(newSettings)
return newSettings
})
}
/**
* Updates the settings for page with the provided settings object.
* Updated settings won't be saved to cookie hence will be reverted once navigating away from the page.
*
* @param settings - The partial settings object containing the properties to update.
* @returns A function to reset the page settings.
*
* @example
* useEffect(() => {
* return updatePageSettings({ theme: 'dark' });
* }, []);
*/
const updatePageSettings = (settings: Partial<Settings>): (() => void) => {
updateSettings(settings, { updateCookie: false })
// Returns a function to reset the page settings
return () => updateSettings(settingsCookie, { updateCookie: false })
}
const resetSettings = () => {
updateSettings(initialSettings)
}
const isSettingsChanged = useMemo(
() => JSON.stringify(initialSettings) !== JSON.stringify(_settingsState),
// eslint-disable-next-line react-hooks/exhaustive-deps
[_settingsState]
)
return (
<SettingsContext.Provider
value={{
settings: _settingsState,
updateSettings,
isSettingsChanged,
resetSettings,
updatePageSettings
}}
>
{props.children}
</SettingsContext.Provider>
)
}
@@ -0,0 +1,18 @@
import apiClient from '@/lib/apiClient';
export const downloadInvoicePdf = async (invoiceId: string) => {
const response = await apiClient.get(
`/invoices/download/${invoiceId}`,
{ responseType: 'blob' }
);
const blob = new Blob([response.data], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `factura-${invoiceId}.pdf`;
a.click();
URL.revokeObjectURL(url);
};
@@ -0,0 +1,55 @@
// src/@core/hooks/useAccessLogs.ts
'use client';
import { useCallback, useEffect, useState } from 'react';
import apiClient from '@/lib/apiClient';
import { useUserAuth } from '@/app/context/UserAuth';
export type AccessLog = {
id: string;
userId: string;
direction: 'INGRESS' | 'EGRESS';
timestamp: string;
};
export type ResponseAccessLogs = {
userId: string;
name: string;
accessLogs: AccessLog[];
};
export function useAccessLogs(userId?: string, date?: string, enabled: boolean = true) {
const { token } = useUserAuth();
const [data, setData] = useState<ResponseAccessLogs | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(
async (signal?: AbortSignal) => {
if (!enabled || !userId || !date) return;
setLoading(true);
setError(null);
try {
const res = await apiClient.get<ResponseAccessLogs>('/attendance', {
params: { userId, date },
signal,
});
setData(res.data);
} catch (e: any) {
if (e?.name === 'CanceledError') return;
setError(e?.response?.data?.message ?? 'No se pudieron cargar los registros de acceso.');
} finally {
setLoading(false);
}
},
[enabled, userId, date, token]
);
useEffect(() => {
const ctrl = new AbortController();
fetchData(ctrl.signal);
return () => ctrl.abort();
}, [fetchData]);
return { data, loading, error, refresh: fetchData };
}
@@ -0,0 +1,49 @@
// src/@core/hooks/useAttendeesSnapshot.ts
'use client';
import { useCallback, useEffect, useState } from 'react';
import apiClient from '@/lib/apiClient';
import { useUserAuth } from '@/app/context/UserAuth';
export type ResponseAttendeesSnapshot = {
dateRange: { start: string; end: string };
instructors: { id: string; name: string }[];
assistants: { id: string; name: string }[];
attendance: { id: string; name: string }[];
};
export function useAttendeesSnapshot(snapshotId?: string, enabled: boolean = true) {
const { token } = useUserAuth();
const [data, setData] = useState<ResponseAttendeesSnapshot | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(
async (signal?: AbortSignal) => {
if (!enabled || !snapshotId) return;
setLoading(true);
setError(null);
try {
const res = await apiClient.get<ResponseAttendeesSnapshot>(
`/sessions/${snapshotId}/attendees`,
{ signal }
);
setData(res.data);
} catch (e: any) {
if (e?.name === 'CanceledError') return;
setError(e?.response?.data?.message ?? 'No se pudieron cargar los asistentes.');
} finally {
setLoading(false);
}
},
[enabled, snapshotId, token]
);
useEffect(() => {
const ctrl = new AbortController();
fetchData(ctrl.signal);
return () => ctrl.abort();
}, [fetchData]);
return { data, loading, error, refresh: fetchData };
}
+66
View File
@@ -0,0 +1,66 @@
// useClases.ts
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import apiClient from '@/lib/apiClient';
import { Clase } from '@/views/clases/DetailClaseDialog';
type Options = {
/** Lanza el fetch al montar el hook (default: true) */
auto?: boolean;
/** Filtro de estado: 'all', 'active', 'inactive' */
status?: 'all' | 'active' | 'inactive';
};
type UseClasesResult = {
data: Clase[];
loading: boolean;
error: unknown;
/** Vuelve a pedir la lista */
refresh: () => Promise<void>;
/** Setea manualmente el array (útil para optimist updates) */
setData: React.Dispatch<React.SetStateAction<Clase[]>>;
};
export function useClases(opts: Options = {}): UseClasesResult {
const { auto = true, status = 'all' } = opts;
const [data, setData] = useState<Clase[]>([]);
const [loading, setLoading] = useState<boolean>(auto);
const [error, setError] = useState<unknown>(null);
// guardamos una referencia al abortController para cancelar en unmount
const abortRef = useRef<AbortController | null>(null);
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
// cancelar request previa si hubiera
if (abortRef.current) abortRef.current.abort();
const ac = new AbortController();
abortRef.current = ac;
try {
const params = status && status !== 'all' ? { status } : undefined;
const res = await apiClient.get<Clase[]>('/sessions', { params, signal: ac.signal });
setData(res.data ?? []);
} catch (e: any) {
if (e?.name === 'CanceledError' || e?.message === 'canceled') return;
setError(e);
console.error('Error al obtener clases:', e);
} finally {
if (!ac.signal.aborted) setLoading(false);
}
}, [status]);
useEffect(() => {
if (auto) refresh();
return () => {
// cancelar si el componente se desmonta
abortRef.current?.abort();
};
}, [auto, refresh]);
return { data, loading, error, refresh, setData };
}
@@ -0,0 +1,65 @@
// useDashboard.ts
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import apiClient from '@/lib/apiClient'
import { useUserAuth } from '@/app/context/UserAuth'
export type DashboardUser = {
message: string
accessLog: { currentAccess: string; lastAccess: string }
cardDetails: { sessions: number; users: number; assistants: number; revenue: number }
sessions: { id: string; description: string; instructors: string; startDate: string }[]
}
type Options = {
auto?: boolean
}
type UseDashboardResult = {
data: DashboardUser | null
loading: boolean
error: unknown
refresh: () => Promise<void>
}
export function useDashboard(opts: Options = {}): UseDashboardResult {
const { token } = useUserAuth()
const { auto = true } = opts
const [data, setData] = useState<DashboardUser | null>(null)
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<unknown>(null)
const abortRef = useRef<AbortController | null>(null)
const refresh = useCallback(async () => {
// si no hay token aún, no pidas nada ni cambies loading
if (!token) return
setLoading(true)
setError(null)
abortRef.current?.abort()
const ac = new AbortController()
abortRef.current = ac
try {
const res = await apiClient.get<DashboardUser>('/dashboard', { signal: ac.signal })
setData(res.data ?? null)
} catch (e: any) {
if (e?.name === 'CanceledError' || e?.message === 'canceled') return
setError(e)
console.error('Error al obtener dashboard:', e)
} finally {
if (!ac.signal.aborted) setLoading(false)
}
}, [token]) // ← incluye token!
useEffect(() => {
if (auto && token) refresh() // ← solo cuando haya token
return () => abortRef.current?.abort()
}, [auto, token, refresh]) // ← depende de token también
return { data, loading, error, refresh }
}
@@ -0,0 +1,28 @@
// React Imports
import { useMemo } from 'react'
// Third-party imports
import { useColorScheme } from '@mui/material'
// Type imports
import type { Mode } from '@core/types'
export const useImageVariant = (mode: Mode, imgLight: string, imgDark: string): string => {
// Hooks
const { mode: muiMode } = useColorScheme()
return useMemo(() => {
const isServer = typeof window === 'undefined'
const currentMode = (() => {
if (isServer) return mode
return muiMode
})()
const isDarkMode = currentMode === 'dark'
return isDarkMode ? imgDark : imgLight
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mode, muiMode])
}
@@ -0,0 +1,74 @@
// src/@core/hooks/useInstructors.ts
'use client';
import { useCallback, useEffect, useState } from 'react';
import apiClient from '@/lib/apiClient';
import { useUserAuth } from '@/app/context/UserAuth';
import type { Instructor } from '@/app/modules/users/dto/instructor.dto';
type UseInstructorsResult = {
data: Instructor[];
loading: boolean;
error: string | null;
refresh: () => Promise<void>;
};
/**
* Hook para obtener la lista de instructores.
* - Lee el token del contexto.
* - Maneja estados data/loading/error.
* - Cancela la request al desmontar.
*/
export function useInstructors(enabled: boolean = true): UseInstructorsResult {
const { token } = useUserAuth();
const [data, setData] = useState<Instructor[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(
async (signal?: AbortSignal) => {
if (!token) {
setData([]);
setLoading(false);
setError(null);
return;
}
try {
setLoading(true);
setError(null);
const res = await apiClient.get<Instructor[]>('/users/instructor', { signal });
setData(res.data ?? []);
} catch (err: any) {
if (err?.name !== 'CanceledError') {
const msg =
err?.response?.data?.message ||
err?.message ||
'Error al obtener instructores.';
setError(msg);
}
} finally {
setLoading(false);
}
},
[token]
);
useEffect(() => {
if (!enabled) return;
const ctrl = new AbortController();
fetchData(ctrl.signal);
return () => ctrl.abort();
}, [enabled, fetchData]);
const refresh = useCallback(async () => {
await fetchData();
}, [fetchData]);
return { data, loading, error, refresh };
}
export default useInstructors;
@@ -0,0 +1,68 @@
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import apiClient from '@/lib/apiClient';
export type InvoiceStatus = 'PENDING' | 'PAID' | 'FAILED' | 'CANCELED';
export type InvoiceWithDescription = {
id: string;
subscriptionId: string;
base64Invoice: string | null;
linkPayment: string | null;
status: InvoiceStatus | string;
amount: number;
createdAt: string | Date;
updatedAt: string | Date;
description: string;
};
export type UseInvoicesOptions = {
auto?: boolean;
};
export type UseInvoicesResult = {
data: InvoiceWithDescription[];
loading: boolean;
error: unknown;
refresh: () => Promise<void>;
setData: React.Dispatch<React.SetStateAction<InvoiceWithDescription[]>>;
};
export function useInvoices(
userId: string,
opts: UseInvoicesOptions = {}
): UseInvoicesResult {
const { auto = true } = opts;
const [data, setData] = useState<InvoiceWithDescription[]>([]);
const [loading, setLoading] = useState<boolean>(auto);
const [error, setError] = useState<unknown>(null);
const abortRef = useRef<AbortController | null>(null);
const refresh = useCallback(async () => {
if (!userId) return;
setLoading(true);
setError(null);
if (abortRef.current) abortRef.current.abort();
const ac = new AbortController();
abortRef.current = ac;
try {
const res = await apiClient.get<InvoiceWithDescription[] | null>(`/invoices/${encodeURIComponent(userId)}`, { signal: ac.signal });
setData((res.data ?? []) as InvoiceWithDescription[]);
} catch (e: any) {
if (e?.name === 'CanceledError' || e?.message === 'canceled') return;
setError(e);
} finally {
if (!ac.signal.aborted) setLoading(false);
}
}, [userId]);
useEffect(() => {
if (auto) refresh();
return () => abortRef.current?.abort();
}, [auto, refresh, userId]);
return { data, loading, error, refresh, setData };
}
@@ -0,0 +1,19 @@
// React Imports
import { useMemo } from 'react'
// Third-party Imports
import { useCookie } from 'react-use'
export const useObjectCookie = <T>(key: string, fallback?: T | null): [T, (newVal: T) => void] => {
// Hooks
const [valStr, updateCookie] = useCookie(key)
// eslint-disable-next-line react-hooks/exhaustive-deps
const value = useMemo<T>(() => (valStr ? JSON.parse(valStr) : fallback), [valStr])
const updateValue = (newVal: T) => {
updateCookie(JSON.stringify(newVal))
}
return [value, updateValue]
}
@@ -0,0 +1,82 @@
'use client';
import { useState, useCallback, useEffect } from 'react';
import apiClient from '@/lib/apiClient';
export type ProfilePictureResponse = {
profilePicture: string | null;
updatedAt: string | null;
};
export type UseProfilePictureOptions = {
auto?: boolean;
};
export type UseProfilePictureResult = {
profilePicture: string | null;
updatedAt: string | null;
loading: boolean;
error: unknown;
refresh: () => Promise<void>;
};
/**
* Hook para obtener la foto de perfil de un usuario
*
* @param userId - ID del usuario
* @param options - Opciones de configuración
*
* @example
* ```tsx
* const { profilePicture, loading, error, refresh } = useProfilePicture('user-123', { auto: true });
*
* return (
* <img src={profilePicture || '/default-avatar.png'} alt="Profile" />
* );
* ```
*/
export function useProfilePicture(
userId: string,
options: UseProfilePictureOptions = {}
): UseProfilePictureResult {
const { auto = true } = options;
const [profilePicture, setProfilePicture] = useState<string | null>(null);
const [updatedAt, setUpdatedAt] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(auto);
const [error, setError] = useState<unknown>(null);
const refresh = useCallback(async () => {
if (!userId) return;
setLoading(true);
setError(null);
try {
const res = await apiClient.get<ProfilePictureResponse>(`/users/${encodeURIComponent(userId)}/profile-picture`);
setProfilePicture(res.data.profilePicture);
setUpdatedAt(res.data.updatedAt);
} catch (e: any) {
setError(e);
setProfilePicture(null);
setUpdatedAt(null);
} finally {
setLoading(false);
}
}, [userId]);
useEffect(() => {
if (auto && userId) {
refresh();
}
}, [auto, userId, refresh]);
return {
profilePicture,
updatedAt,
loading,
error,
refresh,
};
}
@@ -0,0 +1,91 @@
// useSessionInvoicesByMonth.ts
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import apiClient from '@/lib/apiClient';
export type InvoiceStatus = 'PENDING' | 'PAID' | 'CANCELED';
export type AssistantInvoice = {
id: string;
name: string;
hasInvoice: boolean;
invoiceStatus: InvoiceStatus | null;
invoiceId: string | null;
amount: number | null;
};
export type SessionInvoicesByMonth = {
sessionId: string;
sessionCustomId: string;
sessionDescription: string;
sessionType: string;
month: string;
year: string;
assistants: AssistantInvoice[];
};
type Options = {
sessionId: string;
month: string; // "01" a "12"
year: string; // "YYYY"
auto?: boolean;
};
type UseSessionInvoicesByMonthResult = {
data: SessionInvoicesByMonth | null;
loading: boolean;
error: unknown;
refresh: () => Promise<void>;
};
export function useSessionInvoicesByMonth(
opts: Options
): UseSessionInvoicesByMonthResult {
const { sessionId, month, year, auto = true } = opts;
const [data, setData] = useState<SessionInvoicesByMonth | null>(null);
const [loading, setLoading] = useState<boolean>(auto);
const [error, setError] = useState<unknown>(null);
const abortRef = useRef<AbortController | null>(null);
const refresh = useCallback(async () => {
if (!sessionId || !month || !year) {
setData(null);
setLoading(false);
return;
}
setLoading(true);
setError(null);
// Cancelar request previa si hubiera
if (abortRef.current) abortRef.current.abort();
const ac = new AbortController();
abortRef.current = ac;
try {
const res = await apiClient.get<SessionInvoicesByMonth>(
`/sessions/${sessionId}/invoices-by-month`,
{ params: { month, year }, signal: ac.signal }
);
setData(res.data);
} catch (e: any) {
if (e?.name === 'CanceledError' || e?.message === 'canceled') return;
setError(e);
console.error('Error al obtener facturas por mes:', e);
} finally {
if (!ac.signal.aborted) setLoading(false);
}
}, [sessionId, month, year]);
useEffect(() => {
if (auto) refresh();
return () => {
abortRef.current?.abort();
};
}, [auto, refresh]);
return { data, loading, error, refresh };
}
@@ -0,0 +1,42 @@
// useSessionParticipants.ts
'use client';
import { useCallback, useEffect, useState } from 'react';
import apiClient from '@/lib/apiClient';
export type Participant = { id: string; name: string; customId?: string };
export function useSessionParticipants(token: string, sessionId: string, auto = true) {
const [selected, setSelected] = useState<Participant[]>([]);
const [available, setAvailable] = useState<Participant[]>([]);
const [loading, setLoading] = useState(auto);
const [error, setError] = useState<unknown>(null);
const refresh = useCallback(async () => {
setLoading(true); setError(null);
try {
const { data } = await apiClient.get(`/sessions/${sessionId}/participants`);
setSelected(data.selected);
setAvailable(data.available);
} catch (e) { setError(e); } finally { setLoading(false); }
}, [sessionId]);
const save = useCallback(async () => {
await apiClient.put(`/sessions/${sessionId}/participants`, {
userIds: selected.map(s => s.id),
});
}, [sessionId, selected]);
useEffect(() => { if (auto) refresh(); }, [auto, refresh]);
// helpers UI
const add = (p: Participant) => {
setAvailable(prev => prev.filter(x => x.id !== p.id));
setSelected(prev => [...prev, p]);
};
const remove = (p: Participant) => {
setSelected(prev => prev.filter(x => x.id !== p.id));
setAvailable(prev => [...prev, p]);
};
return { selected, available, setSelected, setAvailable, loading, error, refresh, save, add, remove };
}
@@ -0,0 +1,16 @@
// React Imports
import { useContext } from 'react'
// Context Imports
import { SettingsContext } from '@core/contexts/settingsContext'
export const useSettings = () => {
// Hooks
const context = useContext(SettingsContext)
if (!context) {
throw new Error('useSettingsContext must be used within a SettingsProvider')
}
return context
}
@@ -0,0 +1,94 @@
'use client';
import type { SystemConfig, UpdateSystemConfigDto } from '@/@core/types/system-config';
import apiClient from '@/lib/apiClient';
import { useCallback, useState } from 'react';
export type UseSystemConfigOptions = {};
export type UseSystemConfigResult = {
data: SystemConfig | null;
loading: boolean;
error: unknown;
fetchConfig: () => Promise<void>;
updateConfig: (dto: UpdateSystemConfigDto) => Promise<void>;
reloadConfig: () => Promise<void>;
updating: boolean;
reloading: boolean;
};
/**
* Hook para gestionar la configuración del sistema
*/
export function useSystemConfig(
options: UseSystemConfigOptions = {}
): UseSystemConfigResult {
const [data, setData] = useState<SystemConfig | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false);
const [reloading, setReloading] = useState<boolean>(false);
const [error, setError] = useState<unknown>(null);
// Obtener configuración actual
const fetchConfig = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await apiClient.get<SystemConfig>('/system-config');
setData(res.data);
} catch (e: any) {
console.log('Error fetching system config:', e);
setError(e);
throw e;
} finally {
setLoading(false);
}
}, []);
// Actualizar configuración
const updateConfig = useCallback(
async (dto: UpdateSystemConfigDto) => {
setUpdating(true);
setError(null);
try {
const res = await apiClient.patch<SystemConfig>('/system-config', dto);
setData(res.data);
} catch (e: any) {
setError(e);
throw e;
} finally {
setUpdating(false);
}
},
[]
);
// Recargar configuración desde BD
const reloadConfig = useCallback(async () => {
setReloading(true);
setError(null);
try {
const res = await apiClient.get<SystemConfig>('/system-config/reload');
setData(res.data);
} catch (e: any) {
setError(e);
throw e;
} finally {
setReloading(false);
}
}, []);
return {
data,
loading,
error,
fetchConfig,
updateConfig,
reloadConfig,
updating,
reloading,
};
}
@@ -0,0 +1,38 @@
import { useUserAuth } from '@/app/context/UserAuth';
import apiClient from '@/lib/apiClient';
import { useState } from 'react';
export const useToggleUserActive = () => {
const { token } = useUserAuth();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const toggleActive = async (userId: string, isActive: boolean) => {
setLoading(true);
setError(null);
try {
<<<<<<< Updated upstream
const deleted = isActive ? null : new Date().toISOString();
const response = await apiClient.put(`/users/${userId}/active`, { deleted });
console.log('✅ User active status updated successfully:', response.data);
=======
const url = `${process.env.NEXT_PUBLIC_BACKEND_URL ?? ''}/users/${userId}/active`;
const deleted = isActive ? null : new Date().toISOString();
const response = await axios.put(
url,
{ deleted },
{ headers: { Authorization: `Bearer ${token}` } }
);
>>>>>>> Stashed changes
return true;
} catch (err) {
console.error('❌ Error al cambiar estado del usuario', err);
setError('No se pudo cambiar el estado del usuario');
return false;
} finally {
setLoading(false);
}
};
return { toggleActive, loading, error };
};
@@ -0,0 +1,100 @@
'use client';
import { useState, useCallback } from 'react';
import apiClient from '@/lib/apiClient';
import type { InvoiceStatus } from './useInvoices';
export type UpdateInvoiceStatusParams = {
userId: string;
invoiceId: string;
status: InvoiceStatus;
};
export type UpdateInvoiceStatusOptions = {
onSuccess?: () => void | Promise<void>;
onError?: (error: any) => void;
};
export type UpdateInvoiceStatusResult = {
mutate: (params: UpdateInvoiceStatusParams) => Promise<void>;
loading: boolean;
error: unknown;
isSuccess: boolean;
reset: () => void;
};
/**
* Hook para actualizar el estado de una factura
*
* @example
* ```tsx
* const { mutate, loading, error, isSuccess } = useUpdateInvoiceStatus({
* onSuccess: () => {
* toast.success('Factura actualizada');
* refetchInvoices();
* },
* onError: (error) => {
* toast.error('Error al actualizar factura');
* }
* });
*
* // Usar la mutación
* await mutate({
* userId: 'user-123',
* invoiceId: 'invoice-456',
* status: 'PAID'
* });
* ```
*/
export function useUpdateInvoiceStatus(
options: UpdateInvoiceStatusOptions = {}
): UpdateInvoiceStatusResult {
const { onSuccess, onError } = options;
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<unknown>(null);
const [isSuccess, setIsSuccess] = useState<boolean>(false);
const mutate = useCallback(
async ({ userId, invoiceId, status }: UpdateInvoiceStatusParams) => {
if (!userId || !invoiceId || !status) {
const validationError = new Error('userId, invoiceId y status son requeridos');
setError(validationError);
onError?.(validationError);
throw validationError;
}
setLoading(true);
setError(null);
setIsSuccess(false);
try {
await apiClient.put(
`/invoices/${encodeURIComponent(userId)}/${encodeURIComponent(invoiceId)}`,
{ status }
);
setIsSuccess(true);
// Ejecutar callback de éxito
await onSuccess?.();
} catch (e: any) {
setError(e);
setIsSuccess(false);
onError?.(e);
throw e;
} finally {
setLoading(false);
}
},
[onSuccess, onError]
);
const reset = useCallback(() => {
setLoading(false);
setError(null);
setIsSuccess(false);
}, []);
return { mutate, loading, error, isSuccess, reset };
}
@@ -0,0 +1,78 @@
// useUsuarios.ts
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import apiClient from '@/lib/apiClient';
import { Usuario } from '@/views/usuarios/usuarios.dto';
type Options = {
/** Lanza el fetch al montar el hook (default: true) */
auto?: boolean;
};
type UseUsuariosResult = {
data: Usuario[];
loading: boolean;
error: unknown;
/** Vuelve a pedir la lista */
refresh: () => Promise<void>;
/** Setea manualmente el array (útil para optimist updates) */
setData: React.Dispatch<React.SetStateAction<Usuario[]>>;
};
export function useUsuarios(opts: Options = {}): UseUsuariosResult {
const { auto = true } = opts;
const [data, setData] = useState<Usuario[]>([]);
const [loading, setLoading] = useState<boolean>(auto);
const [error, setError] = useState<unknown>(null);
// guardamos una referencia al abortController para cancelar en unmount
const abortRef = useRef<AbortController | null>(null);
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
// cancelar request previa si hubiera
if (abortRef.current) abortRef.current.abort();
const ac = new AbortController();
abortRef.current = ac;
try {
<<<<<<< Updated upstream
const res = await apiClient.get<Usuario[]>('/users', { signal: ac.signal });
=======
if (!token) throw new Error('Token no encontrado');
// Asegurar que baseUrl no esté vacío
const backendUrl = baseUrl
const url = `${backendUrl}/users`;
const res = await axios.get<Usuario[]>(url, {
headers: { Authorization: `Bearer ${token}` },
signal: ac.signal, // axios v1 soporta AbortController
});
>>>>>>> Stashed changes
setData(res.data ?? []);
} catch (e: any) {
if (e?.name === 'CanceledError' || e?.message === 'canceled') return;
setError(e);
console.error('Error al obtener usuarios:', e);
} finally {
if (!ac.signal.aborted) setLoading(false);
}
}, []);
useEffect(() => {
if (auto) refresh();
return () => {
// cancelar si el componente se desmonta
abortRef.current?.abort();
};
}, [auto, refresh]);
return { data, loading, error, refresh, setData };
}
@@ -0,0 +1,36 @@
// hooks/useWhatsappStatus.ts
'use client';
import useSWR from 'swr';
import apiClient from '@/lib/apiClient';
const WHATSAPP_STATUS_PATH = '/whatsapp/status';
const fetcher = async (path: string) => {
const res = await apiClient.get(path);
return res.data; // { status, phone, qr }
};
export type UseWhatsappStatusOptions = {};
export function useWhatsappStatus(_options: UseWhatsappStatusOptions = {}) {
const { data, error, isLoading, mutate } = useSWR(
WHATSAPP_STATUS_PATH,
fetcher,
{
// más frecuencia si NO hay sesión; más relajado si ya está conectada
refreshInterval: (data) =>
data?.status === 'Correcto' ? 30_000 : 3_000,
revalidateOnFocus: false,
shouldRetryOnError: true,
}
);
return {
status: data?.status ?? 'Sin session',
phone: data?.phone ?? 'No disponible',
qr: data?.qr ?? null,
loading: isLoading,
error,
refresh: () => mutate(), // botón "Actualizar"
};
}
+103
View File
@@ -0,0 +1,103 @@
// MUI imports
import Box from '@mui/material/Box'
import { styled } from '@mui/material/styles'
import type { BoxProps } from '@mui/material/Box'
const StepperWrapper = styled(Box)<BoxProps>(({ theme }) => {
return {
[theme.breakpoints.down('md')]: {
'& .MuiStepper-horizontal:not(.MuiStepper-alternativeLabel)': {
flexDirection: 'column',
alignItems: 'flex-start'
}
},
'& .MuiStep-root': {
'& .MuiStepLabel-iconContainer:empty': {
display: 'none'
},
'& .step-label': {
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
},
'& .step-number': {
...theme.typography.h4,
marginRight: theme.spacing(2)
},
'&:not(:has(.step-subtitle)) .step-number': {
...theme.typography.h6
},
'& .step-title': {
...theme.typography.body1,
letterSpacing: 0.15,
fontWeight: 500
},
'& .step-subtitle': {
...theme.typography.body2,
color: 'var(--mui-palette-text-secondary)'
},
'& .MuiStepLabel-root.Mui-disabled': {
'& .step-number': {
color: 'var(--mui-palette-text-disabled)'
}
},
'& .Mui-error': {
'& .MuiStepLabel-labelContainer, & .step-number, & .step-title, & .step-subtitle': {
color: 'var(--mui-palette-error-main)'
}
}
},
'& .MuiStepConnector-root': {
'& .MuiStepConnector-line': {
borderBlockStartWidth: 3,
borderRadius: 3
},
'&.Mui-active, &.Mui-completed': {
'& .MuiStepConnector-line': {
borderColor: 'var(--mui-palette-primary-main)'
}
},
'&.Mui-disabled .MuiStepConnector-line': {
borderColor: 'var(--mui-palette-primary-lightOpacity)'
}
},
'& .MuiStepper-alternativeLabel': {
'& .MuiStepConnector-root': {
top: 9
},
'& .MuiStepLabel-labelContainer': {
display: 'flex',
alignItems: 'center',
flexDirection: 'column'
}
},
'& .MuiStepper-vertical': {
'& .MuiStep-root': {
'& .step-label': {
justifyContent: 'flex-start'
},
'& .MuiStepContent-root': {
borderInlineStartWidth: 3,
marginLeft: theme.spacing(2.25),
borderColor: 'var(--mui-palette-primary-main)'
},
'& .button-wrapper': {
marginTop: theme.spacing(4)
},
'&.active + .MuiStepConnector-root .MuiStepConnector-line': {
borderColor: 'var(--mui-palette-primary-main)'
}
},
'& .MuiStepConnector-root': {
marginLeft: theme.spacing(2.25),
'& .MuiStepConnector-line': {
borderBlockStartWidth: 0,
borderInlineStartWidth: 3,
borderRadius: 0
}
}
}
}
})
export default StepperWrapper
@@ -0,0 +1,92 @@
.table {
inline-size: 100%;
border-collapse: collapse;
font-size: 0.875rem;
white-space: nowrap;
[align='right'] > * {
text-align: end;
}
[align='center'] > * {
text-align: center;
}
thead {
text-transform: uppercase;
color: var(--mui-palette-text-primary);
th {
font-weight: 500;
font-size: 0.8125rem;
letter-spacing: 0.2px;
line-height: 1.8462;
text-align: start;
block-size: 56px;
background-color: var(--mui-palette-customColors-tableHeaderBg);
&:not(:first-of-type):not(:last-of-type) {
padding-block: 0.5rem;
padding-inline: 1rem;
}
&:first-of-type {
&:not(:has(input[type='checkbox'])) {
padding-block: 0.5rem;
padding-inline: 1.25rem 1rem;
}
&:has(input[type='checkbox']) {
padding-inline-start: 0.6875rem;
}
}
&:last-of-type {
padding-block: 0.5rem;
padding-inline: 1rem 1.25rem;
}
}
}
tbody {
color: var(--mui-palette-text-secondary);
th,
td {
font-size: 0.9375rem;
line-height: 1.4667;
block-size: 50px;
&:not(:first-of-type):not(:last-of-type) {
padding-block: 0.5rem;
padding-inline: 1rem;
}
&:first-of-type {
&:not(:has(input[type='checkbox'])) {
padding-block: 0.5rem;
padding-inline: 1.25rem 1rem;
}
&:has(input[type='checkbox']) {
padding-inline-start: 0.6875rem;
}
}
&:last-of-type {
padding-block: 0.5rem;
padding-inline: 1rem 1.25rem;
}
}
tr:not(:last-child) {
border-block-end: 1px solid var(--border-color);
}
}
}
.cellWithInput input {
inline-size: 100%;
background-color: transparent;
font-size: inherit;
color: inherit;
border-radius: var(--mui-shape-customBorderRadius-sm);
padding-block: 6px;
padding-inline: 10px;
margin-inline: -10px;
&:focus-visible {
outline: 1px solid var(--mui-palette-primary-main);
}
}
@@ -0,0 +1,99 @@
// MUI Imports
import { lighten } from '@mui/material/styles'
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { MenuItemStyles } from '@menu/types'
// Util Imports
import { menuClasses } from '@menu/utils/menuClasses'
const menuItemStyles = (theme: Theme): MenuItemStyles => {
return {
root: {
marginBlockStart: theme.spacing(1.5),
[`&.${menuClasses.subMenuRoot}.${menuClasses.open} > .${menuClasses.button}, &.${menuClasses.subMenuRoot} > .${menuClasses.button}.${menuClasses.active}`]:
{
backgroundColor: 'var(--mui-palette-action-selected) !important'
},
[`&.${menuClasses.disabled} > .${menuClasses.button}`]: {
color: 'var(--mui-palette-text-disabled)',
[`& .${menuClasses.icon}`]: {
color: 'inherit'
}
},
[`&:not(.${menuClasses.subMenuRoot}) > .${menuClasses.button}.${menuClasses.active}`]: {
color: 'var(--mui-palette-primary-contrastText)',
background:
theme.direction === 'ltr'
? `linear-gradient(270deg, var(--mui-palette-primary-main), ${lighten(
theme.palette.primary.main,
0.5
)} 100%)`
: `linear-gradient(270deg, ${lighten(
theme.palette.primary.main,
0.5
)}, var(--mui-palette-primary-main) 100%)`,
[`& .${menuClasses.icon}`]: {
color: 'inherit'
}
}
},
button: ({ active }) => ({
paddingBlock: theme.spacing(2),
'&:has(.MuiChip-root)': {
paddingBlock: theme.spacing(1.75)
},
paddingInlineStart: theme.spacing(5.5),
paddingInlineEnd: theme.spacing(3.5),
borderStartEndRadius: 50,
borderEndEndRadius: 50,
...(!active && {
'&:hover, &:focus-visible': {
backgroundColor: 'var(--mui-palette-action-hover)'
},
'&[aria-expanded="true"]': {
backgroundColor: 'var(--mui-palette-action-selected)'
}
})
}),
icon: ({ level }) => ({
...(level === 0 && {
fontSize: '1.375rem',
marginInlineEnd: theme.spacing(2)
}),
...(level > 0 && {
fontSize: '0.75rem',
color: 'var(--mui-palette-text-secondary)',
marginInlineEnd: theme.spacing(3.5)
}),
...(level === 1 && {
marginInlineStart: theme.spacing(1.5)
}),
...(level > 1 && {
marginInlineStart: theme.spacing(1.5 + 2.5 * (level - 1))
}),
'& > i, & > svg': {
fontSize: 'inherit'
}
}),
prefix: {
marginInlineEnd: theme.spacing(2)
},
suffix: {
marginInlineStart: theme.spacing(2)
},
subMenuExpandIcon: {
fontSize: '1.375rem',
marginInlineStart: theme.spacing(2),
'& i, & svg': {
fontSize: 'inherit'
}
},
subMenuContent: {
backgroundColor: 'transparent'
}
}
}
export default menuItemStyles
@@ -0,0 +1,42 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { MenuProps } from '@menu/vertical-menu'
// Util Imports
import { menuClasses } from '@menu/utils/menuClasses'
const menuSectionStyles = (theme: Theme): MenuProps['menuSectionStyles'] => {
return {
root: {
marginBlockStart: theme.spacing(7),
[`& .${menuClasses.menuSectionContent}`]: {
color: 'var(--mui-palette-text-disabled)',
paddingInline: '0 !important',
paddingBlock: `${theme.spacing(1.75)} !important`,
gap: theme.spacing(2.5),
'&:before': {
content: '""',
blockSize: 1,
inlineSize: '0.875rem',
backgroundColor: 'var(--mui-palette-divider)'
},
'&:after': {
content: '""',
blockSize: 1,
flexGrow: 1,
backgroundColor: 'var(--mui-palette-divider)'
}
},
[`& .${menuClasses.menuSectionLabel}`]: {
flexGrow: 0,
fontSize: '13px',
lineHeight: 1.38462
}
}
}
}
export default menuSectionStyles
@@ -0,0 +1,35 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Util Imports
import { menuClasses, verticalNavClasses } from '@menu/utils/menuClasses'
const navigationCustomStyles = (theme: Theme) => {
return {
color: 'var(--mui-palette-text-primary)',
zIndex: 'var(--drawer-z-index) !important',
[`& .${verticalNavClasses.bgColorContainer}`]: {
backgroundColor: 'var(--mui-palette-background-default)'
},
[`& .${verticalNavClasses.header}`]: {
paddingBlock: theme.spacing(5),
paddingInline: theme.spacing(5.5, 4)
},
[`& .${verticalNavClasses.container}`]: {
transition: 'none',
borderColor: 'transparent',
[`& .${verticalNavClasses.toggled}`]: {
boxShadow: 'var(--mui-customShadows-lg)'
}
},
[`& .${menuClasses.root}`]: {
paddingBlockEnd: theme.spacing(2),
paddingInlineEnd: theme.spacing(4)
},
[`& .${verticalNavClasses.backdrop}`]: {
backgroundColor: 'var(--backdrop-color)'
}
}
}
export default navigationCustomStyles
+77
View File
@@ -0,0 +1,77 @@
// React Imports
import type { SVGAttributes } from 'react'
const Logo = (props: SVGAttributes<SVGElement>) => {
return (
<svg width='1.2658em' height='1em' viewBox='0 0 100 79' fill='none' xmlns='http://www.w3.org/2000/svg' {...props}>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M4.92008 0.501904L22.662 11.4573C23.614 12.0451 24.1936 13.0844 24.1936 14.2036V64.2521C24.1936 65.3871 23.5976 66.4387 22.6241 67.0214L4.8822 77.6429C3.35344 78.5581 1.37254 78.0602 0.457741 76.5307C0.158194 76.0299 0 75.4572 0 74.8736V3.24818C0 1.46582 1.44424 0.0209274 3.22581 0.0209274C3.82422 0.0209274 4.41085 0.18746 4.92008 0.501904Z'
fill='currentColor'
/>
<path
opacity='0.077704'
fillRule='evenodd'
clipRule='evenodd'
d='M0 26.1063L24.1936 39.9852V53.5915L0 26.1063Z'
fill='black'
/>
<path
opacity='0.077704'
fillRule='evenodd'
clipRule='evenodd'
d='M0 26.1063L24.1936 39.6319V47.9438L0 26.1063Z'
fill='black'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M95.084 0.489601L77.3421 11.4083C76.3878 11.9956 75.8064 13.0362 75.8064 14.1571V64.2526C75.8064 65.3875 76.4024 66.4391 77.3759 67.0219L95.1178 77.6433C96.6466 78.5585 98.6275 78.0606 99.5423 76.5312C99.8418 76.0303 100 75.4576 100 74.874V3.23842C100 1.45605 98.5558 0.0111618 96.7742 0.0111618C96.1774 0.0111618 95.5923 0.176782 95.084 0.489601Z'
fill='currentColor'
/>
<path
opacity='0.077704'
fillRule='evenodd'
clipRule='evenodd'
d='M100 26.1063L75.8064 39.956V54.0023L100 26.1063Z'
fill='black'
/>
<path
opacity='0.077704'
fillRule='evenodd'
clipRule='evenodd'
d='M100 26.1063L75.8064 39.6199V48.3546L100 26.1063Z'
fill='black'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M4.91149 0.475694L50 28.123V54.7479L0 26.0986V3.22726C0 1.44489 1.44424 0 3.22581 0C3.8208 0 4.4042 0.164633 4.91149 0.475694Z'
fill='currentColor'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M4.91149 0.475694L50 28.123V54.7479L0 26.0986V3.22726C0 1.44489 1.44424 0 3.22581 0C3.8208 0 4.4042 0.164633 4.91149 0.475694Z'
fill='white'
fillOpacity='0.15'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M95.0885 0.475694L50 28.123V54.7479L100 26.0986V3.22726C100 1.44489 98.5558 0 96.7742 0C96.1792 0 95.5958 0.164633 95.0885 0.475694Z'
fill='currentColor'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M95.0885 0.475694L50 28.123V54.7479L100 26.0986V3.22726C100 1.44489 98.5558 0 96.7742 0C96.1792 0 95.5958 0.164633 95.0885 0.475694Z'
fill='white'
fillOpacity='0.3'
/>
</svg>
)
}
export default Logo
+74
View File
@@ -0,0 +1,74 @@
import plugin from 'tailwindcss/plugin'
module.exports = plugin(function () {}, {
theme: {
borderColor: ({ theme }) => ({
...theme('colors'),
DEFAULT: 'var(--border-color, currentColor)'
}),
borderRadius: {
none: '0px',
xs: 'var(--mui-shape-customBorderRadius-xs)',
sm: 'var(--mui-shape-customBorderRadius-sm)',
DEFAULT: '0.375rem',
md: 'var(--mui-shape-customBorderRadius-md)',
lg: 'var(--mui-shape-customBorderRadius-lg)',
xl: 'var(--mui-shape-customBorderRadius-xl)',
'2xl': '0.75rem',
'3xl': '1rem',
'4xl': '1.5rem',
full: '9999px'
},
screens: {
sm: '600px',
md: '900px',
lg: '1200px',
xl: '1536px',
'2xl': '1920px'
},
extend: {
boxShadow: {
xs: 'var(--mui-customShadows-xs)',
sm: 'var(--mui-customShadows-sm)',
DEFAULT: 'var(--mui-customShadows-md)',
md: 'var(--mui-customShadows-md)',
lg: 'var(--mui-customShadows-lg)',
xl: 'var(--mui-customShadows-xl)'
},
colors: {
primary: 'var(--primary-color)',
primaryLight: 'var(--mui-palette-primary-lightOpacity)',
primaryLighter: 'var(--mui-palette-primary-lighterOpacity)',
secondary: 'var(--mui-palette-secondary-main)',
error: 'var(--mui-palette-error-main)',
errorLight: 'var(--mui-palette-error-lightOpacity)',
errorLighter: 'var(--mui-palette-error-lighterOpacity)',
warning: 'var(--mui-palette-warning-main)',
info: 'var(--mui-palette-info-main)',
success: 'var(--mui-palette-success-main)',
textPrimary: 'var(--mui-palette-text-primary)',
textSecondary: 'var(--mui-palette-text-secondary)',
textDisabled: 'var(--mui-palette-text-disabled)',
actionActive: 'var(--mui-palette-action-active)',
actionHover: 'var(--mui-palette-action-hover)',
actionSelected: 'var(--mui-palette-action-selected)',
actionFocus: 'var(--mui-palette-action-focus)',
backgroundPaper: 'var(--mui-palette-background-paper)',
backgroundDefault: 'var(--mui-palette-background-default)',
track: 'var(--mui-palette-customColors-trackBg)',
backdrop: 'var(--backdrop-color)',
facebook: '#497ce2',
twitter: '#1da1f2',
github: '#272727',
googlePlus: '#db4437'
},
zIndex: {
header: 'var(--header-z-index)',
footer: 'var(--footer-z-index)',
customizer: 'var(--customizer-z-index)',
search: 'var(--search-z-index)',
drawer: 'var(--drawer-z-index)'
}
}
}
})
+329
View File
@@ -0,0 +1,329 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const colorSchemes = (): Theme['colorSchemes'] => {
const skin = 'default' as string
return {
light: {
palette: {
primary: {
main: '#8C57FF',
light: '#A379FF',
dark: '#7E4EE6',
lighterOpacity: 'rgb(var(--mui-palette-primary-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-primary-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-primary-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-primary-mainChannel) / 0.38)'
},
secondary: {
main: '#8A8D93',
light: '#A1A4A9',
dark: '#7C7F84',
contrastText: '#fff',
lighterOpacity: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.38)'
},
error: {
main: '#FF4C51',
light: '#FF7074',
dark: '#E64449',
contrastText: '#fff',
lighterOpacity: 'rgb(var(--mui-palette-error-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-error-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-error-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-error-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-error-mainChannel) / 0.38)'
},
warning: {
main: '#FFB400',
light: '#FFC333',
dark: '#E6A200',
contrastText: '#fff',
lighterOpacity: 'rgb(var(--mui-palette-warning-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-warning-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-warning-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-warning-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-warning-mainChannel) / 0.38)'
},
info: {
main: '#16B1FF',
light: '#45C1FF',
dark: '#149FE6',
contrastText: '#fff',
lighterOpacity: 'rgb(var(--mui-palette-info-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-info-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-info-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-info-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-info-mainChannel) / 0.38)'
},
success: {
main: '#56CA00',
light: '#78D533',
dark: '#4DB600',
contrastText: '#fff',
lighterOpacity: 'rgb(var(--mui-palette-success-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-success-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-success-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-success-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-success-mainChannel) / 0.38)'
},
text: {
primary: `rgb(var(--mui-mainColorChannels-light) / 0.9)`,
secondary: `rgb(var(--mui-mainColorChannels-light) / 0.7)`,
disabled: `rgb(var(--mui-mainColorChannels-light) / 0.4)`,
primaryChannel: 'var(--mui-mainColorChannels-light)',
secondaryChannel: 'var(--mui-mainColorChannels-light)'
},
divider: `rgb(var(--mui-mainColorChannels-light) / 0.12)`,
dividerChannel: 'var(--mui-mainColorChannels-light)',
background: {
default: skin === 'bordered' ? '#FFFFFF' : '#F4F5FA',
paper: '#FFFFFF'
},
action: {
active: `rgb(var(--mui-mainColorChannels-light) / 0.6)`,
hover: `rgb(var(--mui-mainColorChannels-light) / 0.04)`,
selected: `rgb(var(--mui-mainColorChannels-light) / 0.08)`,
disabled: `rgb(var(--mui-mainColorChannels-light) / 0.3)`,
disabledBackground: `rgb(var(--mui-mainColorChannels-light) / 0.12)`,
focus: `rgb(var(--mui-mainColorChannels-light) / 0.1)`,
focusOpacity: 0.1,
activeChannel: 'var(--mui-mainColorChannels-light)',
selectedChannel: 'var(--mui-mainColorChannels-light)'
},
Alert: {
errorColor: 'var(--mui-palette-error-main)',
warningColor: 'var(--mui-palette-warning-main)',
infoColor: 'var(--mui-palette-info-main)',
successColor: 'var(--mui-palette-success-main)',
errorStandardBg: 'var(--mui-palette-error-lightOpacity)',
warningStandardBg: 'var(--mui-palette-warning-lightOpacity)',
infoStandardBg: 'var(--mui-palette-info-lightOpacity)',
successStandardBg: 'var(--mui-palette-success-lightOpacity)',
errorFilledColor: 'var(--mui-palette-error-contrastText)',
warningFilledColor: 'var(--mui-palette-warning-contrastText)',
infoFilledColor: 'var(--mui-palette-info-contrastText)',
successFilledColor: 'var(--mui-palette-success-contrastText)',
errorFilledBg: 'var(--mui-palette-error-main)',
warningFilledBg: 'var(--mui-palette-warning-main)',
infoFilledBg: 'var(--mui-palette-info-main)',
successFilledBg: 'var(--mui-palette-success-main)'
},
Avatar: {
defaultBg: '#F0EFF0'
},
Chip: {
defaultBorder: 'var(--mui-palette-divider)'
},
FilledInput: {
bg: `rgb(var(--mui-mainColorChannels-light) / 0.06)`,
hoverBg: `rgb(var(--mui-mainColorChannels-light) / 0.08)`,
disabledBg: `rgb(var(--mui-mainColorChannels-light) / 0.06)`
},
LinearProgress: {
primaryBg: 'var(--mui-palette-primary-mainOpacity)',
secondaryBg: 'var(--mui-palette-secondary-mainOpacity)',
errorBg: 'var(--mui-palette-error-mainOpacity)',
warningBg: 'var(--mui-palette-warning-mainOpacity)',
infoBg: 'var(--mui-palette-info-mainOpacity)',
successBg: 'var(--mui-palette-success-mainOpacity)'
},
SnackbarContent: {
bg: '#1A0E33',
color: 'var(--mui-palette-background-paper)'
},
Switch: {
defaultColor: 'var(--mui-palette-common-white)',
defaultDisabledColor: 'var(--mui-palette-common-white)',
primaryDisabledColor: 'var(--mui-palette-common-white)',
secondaryDisabledColor: 'var(--mui-palette-common-white)',
errorDisabledColor: 'var(--mui-palette-common-white)',
warningDisabledColor: 'var(--mui-palette-common-white)',
infoDisabledColor: 'var(--mui-palette-common-white)',
successDisabledColor: 'var(--mui-palette-common-white)'
},
Tooltip: {
bg: '#1A0E33'
},
TableCell: {
border: 'var(--mui-palette-divider)'
},
customColors: {
bodyBg: '#F4F5FA',
chatBg: '#F7F6FA',
greyLightBg: '#FAFAFA',
inputBorder: `rgb(var(--mui-mainColorChannels-light) / 0.22)`,
tableHeaderBg: '#F6F7FB',
tooltipText: '#FFFFFF',
trackBg: '#F0F2F8'
}
}
},
dark: {
palette: {
primary: {
main: '#8C57FF',
light: '#A379FF',
dark: '#7E4EE6',
lighterOpacity: 'rgb(var(--mui-palette-primary-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-primary-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-primary-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-primary-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-primary-mainChannel) / 0.38)'
},
secondary: {
main: '#8A8D93',
light: '#A1A4A9',
dark: '#7C7F84',
contrastText: '#fff',
lighterOpacity: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.38)'
},
error: {
main: '#FF4C51',
light: '#FF7074',
dark: '#E64449',
contrastText: '#fff',
lighterOpacity: 'rgb(var(--mui-palette-error-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-error-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-error-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-error-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-error-mainChannel) / 0.38)'
},
warning: {
main: '#FFB400',
light: '#FFC333',
dark: '#E6A200',
contrastText: '#fff',
lighterOpacity: 'rgb(var(--mui-palette-warning-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-warning-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-warning-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-warning-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-warning-mainChannel) / 0.38)'
},
info: {
main: '#16B1FF',
light: '#45C1FF',
dark: '#149FE6',
contrastText: '#fff',
lighterOpacity: 'rgb(var(--mui-palette-info-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-info-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-info-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-info-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-info-mainChannel) / 0.38)'
},
success: {
main: '#56CA00',
light: '#78D533',
dark: '#4DB600',
contrastText: '#fff',
lighterOpacity: 'rgb(var(--mui-palette-success-mainChannel) / 0.08)',
lightOpacity: 'rgb(var(--mui-palette-success-mainChannel) / 0.16)',
mainOpacity: 'rgb(var(--mui-palette-success-mainChannel) / 0.24)',
darkOpacity: 'rgb(var(--mui-palette-success-mainChannel) / 0.32)',
darkerOpacity: 'rgb(var(--mui-palette-success-mainChannel) / 0.38)'
},
text: {
primary: `rgb(var(--mui-mainColorChannels-dark) / 0.9)`,
secondary: `rgb(var(--mui-mainColorChannels-dark) / 0.7)`,
disabled: `rgb(var(--mui-mainColorChannels-dark) / 0.4)`,
primaryChannel: 'var(--mui-mainColorChannels-dark)',
secondaryChannel: 'var(--mui-mainColorChannels-dark)'
},
divider: `rgb(var(--mui-mainColorChannels-dark) / 0.12)`,
dividerChannel: 'var(--mui-mainColorChannels-dark)',
background: {
default: skin === 'bordered' ? '#312D4B' : '#28243D',
paper: '#312D4B'
},
action: {
active: `rgb(var(--mui-mainColorChannels-dark) / 0.6)`,
hover: `rgb(var(--mui-mainColorChannels-dark) / 0.04)`,
selected: `rgb(var(--mui-mainColorChannels-dark) / 0.08)`,
disabled: `rgb(var(--mui-mainColorChannels-dark) / 0.3)`,
disabledBackground: `rgb(var(--mui-mainColorChannels-dark) / 0.12)`,
focus: `rgb(var(--mui-mainColorChannels-dark) / 0.1)`,
focusOpacity: 0.1,
activeChannel: 'var(--mui-mainColorChannels-dark)',
selectedChannel: 'var(--mui-mainColorChannels-dark)'
},
Alert: {
errorColor: 'var(--mui-palette-error-main)',
warningColor: 'var(--mui-palette-warning-main)',
infoColor: 'var(--mui-palette-info-main)',
successColor: 'var(--mui-palette-success-main)',
errorStandardBg: 'var(--mui-palette-error-lightOpacity)',
warningStandardBg: 'var(--mui-palette-warning-lightOpacity)',
infoStandardBg: 'var(--mui-palette-info-lightOpacity)',
successStandardBg: 'var(--mui-palette-success-lightOpacity)',
errorFilledColor: 'var(--mui-palette-error-contrastText)',
warningFilledColor: 'var(--mui-palette-warning-contrastText)',
infoFilledColor: 'var(--mui-palette-info-contrastText)',
successFilledColor: 'var(--mui-palette-success-contrastText)',
errorFilledBg: 'var(--mui-palette-error-main)',
warningFilledBg: 'var(--mui-palette-warning-main)',
infoFilledBg: 'var(--mui-palette-info-main)',
successFilledBg: 'var(--mui-palette-success-main)'
},
Avatar: {
defaultBg: '#3F3B59'
},
Chip: {
defaultBorder: 'var(--mui-palette-divider)'
},
FilledInput: {
bg: `rgb(var(--mui-mainColorChannels-dark) / 0.06)`,
hoverBg: `rgb(var(--mui-mainColorChannels-dark) / 0.08)`,
disabledBg: `rgb(var(--mui-mainColorChannels-dark) / 0.06)`
},
LinearProgress: {
primaryBg: 'var(--mui-palette-primary-mainOpacity)',
secondaryBg: 'var(--mui-palette-secondary-mainOpacity)',
errorBg: 'var(--mui-palette-error-mainOpacity)',
warningBg: 'var(--mui-palette-warning-mainOpacity)',
infoBg: 'var(--mui-palette-info-mainOpacity)',
successBg: 'var(--mui-palette-success-mainOpacity)'
},
SnackbarContent: {
bg: '#F7F4FF',
color: 'var(--mui-palette-background-paper)'
},
Switch: {
defaultColor: 'var(--mui-palette-common-white)',
defaultDisabledColor: 'var(--mui-palette-common-white)',
primaryDisabledColor: 'var(--mui-palette-common-white)',
secondaryDisabledColor: 'var(--mui-palette-common-white)',
errorDisabledColor: 'var(--mui-palette-common-white)',
warningDisabledColor: 'var(--mui-palette-common-white)',
infoDisabledColor: 'var(--mui-palette-common-white)',
successDisabledColor: 'var(--mui-palette-common-white)'
},
Tooltip: {
bg: '#F7F4FF'
},
TableCell: {
border: 'var(--mui-palette-divider)'
},
customColors: {
bodyBg: '#28243D',
chatBg: '#373452',
greyLightBg: '#373350',
inputBorder: `rgb(var(--mui-mainColorChannels-dark) / 0.22)`,
tableHeaderBg: '#3D3759',
tooltipText: '#312D4B',
trackBg: '#474360'
}
}
}
} as Theme['colorSchemes']
}
export default colorSchemes
@@ -0,0 +1,17 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { SystemMode } from '@core/types'
const customShadows = (mode: SystemMode): Theme['customShadows'] => {
return {
xs: `0px 2px 4px rgb(var(--mui-mainColorChannels-${mode}Shadow) / ${mode === 'light' ? 0.16 : 0.2})`,
sm: `0px 3px 6px rgb(var(--mui-mainColorChannels-${mode}Shadow) / ${mode === 'light' ? 0.18 : 0.22})`,
md: `0px 4px 10px rgb(var(--mui-mainColorChannels-${mode}Shadow) / ${mode === 'light' ? 0.2 : 0.24})`,
lg: `0px 6px 16px rgb(var(--mui-mainColorChannels-${mode}Shadow) / ${mode === 'light' ? 0.22 : 0.26})`,
xl: `0px 8px 28px rgb(var(--mui-mainColorChannels-${mode}Shadow) / ${mode === 'light' ? 0.24 : 0.28})`
}
}
export default customShadows
+48
View File
@@ -0,0 +1,48 @@
// Next Imports
import { Inter } from 'next/font/google'
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { SystemMode } from '@core/types'
// Theme Options Imports
import overrides from './overrides'
import colorSchemes from './colorSchemes'
import spacing from './spacing'
import shadows from './shadows'
import customShadows from './customShadows'
import typography from './typography'
const inter = Inter({ subsets: ['latin'], weight: ['300', '400', '500', '600', '700', '800', '900'] })
const theme = (mode: SystemMode, direction: Theme['direction']): Theme => {
return {
direction,
components: overrides(),
colorSchemes: colorSchemes(),
...spacing,
shape: {
borderRadius: 6,
customBorderRadius: {
xs: 2,
sm: 4,
md: 6,
lg: 8,
xl: 10
}
},
shadows: shadows(mode),
typography: typography(inter.style.fontFamily),
customShadows: customShadows(mode),
mainColorChannels: {
light: '46 38 61',
dark: '231 227 252',
lightShadow: '46 38 61',
darkShadow: '19 17 32'
}
} as Theme
}
export default theme
@@ -0,0 +1,90 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { Skin } from '@core/types'
const accordion = (skin: Skin): Theme['components'] => ({
MuiAccordion: {
defaultProps: {
...(skin === 'bordered' && {
variant: 'outlined'
})
},
styleOverrides: {
root: ({ theme }) => ({
transition: theme.transitions.create(['margin', 'border-radius', 'box-shadow']),
...(skin !== 'bordered'
? {
boxShadow: 'var(--mui-customShadows-xs)'
}
: {
'&:not(.Mui-expanded) + &:not(.Mui-expanded)': {
borderBlockStart: 0
},
'&:not(.Mui-expanded):has(+ &:not(.Mui-expanded))': {
borderBlockEnd: 0
}
}),
'&:not(.Mui-expanded):has(+ .Mui-expanded)': {
borderBottomLeftRadius: 'var(--mui-shape-borderRadius)',
borderBottomRightRadius: 'var(--mui-shape-borderRadius)'
},
'&.Mui-expanded': {
borderRadius: 'var(--mui-shape-borderRadius)',
...(skin !== 'bordered' && {
boxShadow: 'var(--mui-customShadows-md)'
}),
margin: theme.spacing(2, 0),
'& + .MuiAccordion-root': {
borderTopLeftRadius: 'var(--mui-shape-borderRadius)',
borderTopRightRadius: 'var(--mui-shape-borderRadius)',
'&:before': {
opacity: 0
}
}
}
})
}
},
MuiAccordionSummary: {
defaultProps: {
expandIcon: <i className='ri-arrow-down-s-line' />
},
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(3, 5),
color: 'var(--mui-palette-text-primary)',
'&.Mui-expanded': {
minHeight: 48
},
'& .MuiTypography-root': {
color: 'inherit',
fontWeight: theme.typography.fontWeightMedium
}
}),
content: {
margin: '0 !important'
},
expandIconWrapper: {
color: 'var(--mui-palette-text-primary)',
fontSize: '1.25rem',
'& i, & svg': {
fontSize: 'inherit'
}
}
}
},
MuiAccordionDetails: {
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(0, 5, 5),
'& .MuiTypography-root': {
color: 'var(--mui-palette-text-secondary)'
}
})
}
}
})
export default accordion
@@ -0,0 +1,179 @@
// React Imports
import React from 'react'
// MUI Imports
import type { Theme } from '@mui/material/styles'
const alerts: Theme['components'] = {
MuiAlert: {
defaultProps: {
iconMapping: {
error: <i className='ri-error-warning-line' />,
warning: <i className='ri-alert-line' />,
info: <i className='ri-information-line' />,
success: <i className='ri-checkbox-circle-line' />
}
},
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(4),
gap: theme.spacing(4),
...theme.typography.body1,
'&:not(:has(.MuiAlertTitle-root))': {
'& .MuiAlert-icon + .MuiAlert-message': {
alignSelf: 'center'
}
}
}),
icon: {
padding: 0,
margin: 0,
minWidth: 30,
height: 30,
borderRadius: 'var(--mui-shape-borderRadius)',
alignItems: 'center',
justifyContent: 'center',
'& i, & svg': {
fontSize: 'inherit'
}
},
message: {
padding: 0
},
action: {
padding: 0,
marginRight: 0
}
},
variants: [
{
props: { variant: 'standard', severity: 'error' },
style: {
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-error-main)',
color: 'var(--mui-palette-error-contrastText)'
}
}
},
{
props: { variant: 'standard', severity: 'warning' },
style: {
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-warning-main)',
color: 'var(--mui-palette-warning-contrastText)'
}
}
},
{
props: { variant: 'standard', severity: 'info' },
style: {
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-info-main)',
color: 'var(--mui-palette-info-contrastText)'
}
}
},
{
props: { variant: 'standard', severity: 'success' },
style: {
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-success-main)',
color: 'var(--mui-palette-success-contrastText)'
}
}
},
{
props: { variant: 'outlined', severity: 'error' },
style: {
borderColor: 'var(--mui-palette-error-main)',
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-error-mainOpacity)',
color: 'var(--mui-palette-error-main)'
}
}
},
{
props: { variant: 'outlined', severity: 'warning' },
style: {
borderColor: 'var(--mui-palette-warning-main)',
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-warning-mainOpacity)',
color: 'var(--mui-palette-warning-main)'
}
}
},
{
props: { variant: 'outlined', severity: 'info' },
style: {
borderColor: 'var(--mui-palette-info-main)',
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-info-mainOpacity)',
color: 'var(--mui-palette-info-main)'
}
}
},
{
props: { variant: 'outlined', severity: 'success' },
style: {
borderColor: 'var(--mui-palette-success-main)',
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-success-mainOpacity)',
color: 'var(--mui-palette-success-main)'
}
}
},
{
props: { variant: 'filled', severity: 'error' },
style: {
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-common-white)',
color: 'var(--mui-palette-error-main)',
boxShadow: 'var(--mui-customShadows-xs)'
}
}
},
{
props: { variant: 'filled', severity: 'warning' },
style: {
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-common-white)',
color: 'var(--mui-palette-warning-main)',
boxShadow: 'var(--mui-customShadows-xs)'
}
}
},
{
props: { variant: 'filled', severity: 'info' },
style: {
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-common-white)',
color: 'var(--mui-palette-info-main)',
boxShadow: 'var(--mui-customShadows-xs)'
}
}
},
{
props: { variant: 'filled', severity: 'success' },
style: {
'& .MuiAlert-icon': {
backgroundColor: 'var(--mui-palette-common-white)',
color: 'var(--mui-palette-success-main)',
boxShadow: 'var(--mui-customShadows-xs)'
}
}
}
]
},
MuiAlertTitle: {
styleOverrides: {
root: ({ theme }) => ({
...theme.typography.h5,
marginTop: 0,
marginBottom: theme.spacing(1),
color: 'inherit'
})
}
}
}
export default alerts
@@ -0,0 +1,76 @@
// React Imports
import React from 'react'
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { Skin } from '@core/types'
const autocomplete = (skin: Skin): Theme['components'] => ({
MuiAutocomplete: {
defaultProps: {
...(skin === 'bordered' && {
slotProps: {
paper: {
variant: 'outlined'
}
}
}),
ChipProps: {
size: 'small'
},
popupIcon: <i className='ri-arrow-down-s-line' />
},
styleOverrides: {
root: {
'& .MuiButtonBase-root.Mui-disabled i, & .MuiButtonBase-root.Mui-disabled svg': {
color: 'var(--mui-palette-action-disabled)'
},
'& .MuiOutlinedInput-input': {
height: '1.4375em'
}
},
input: {
'& + .MuiAutocomplete-endAdornment': {
right: '1rem',
'& i, & svg': {
fontSize: '1.5rem',
color: 'var(--mui-palette-text-primary)'
},
'& .MuiAutocomplete-clearIndicator': {
padding: 2
}
},
'&.MuiInputBase-inputSizeSmall + .MuiAutocomplete-endAdornment': {
'& i, & svg': {
fontSize: '1.375rem'
}
}
},
paper: {
...(skin !== 'bordered' && {
boxShadow: 'var(--mui-customShadows-lg)',
marginBlockStart: '0.125rem'
})
},
listbox: ({ theme }) => ({
'& .MuiAutocomplete-option': {
padding: theme.spacing(2, 5),
'&[aria-selected="true"]': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
color: 'var(--mui-palette-primary-main)',
'&.Mui-focused, &.Mui-focusVisible': {
backgroundColor: 'var(--mui-palette-primary-mainOpacity)'
}
}
},
'& .MuiAutocomplete-option.Mui-focusVisible': {
backgroundColor: 'var(--mui-palette-action-hover)'
}
})
}
}
})
export default autocomplete
@@ -0,0 +1,38 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const avatar: Theme['components'] = {
MuiAvatarGroup: {
styleOverrides: {
root: ({ theme }) => ({
justifyContent: 'flex-end',
'& .MuiAvatar-root': {
borderColor: 'var(--mui-palette-background-paper)'
},
'&.pull-up .MuiAvatar-root': {
cursor: 'pointer',
transition: theme.transitions.create(['box-shadow', 'transform'], {
easing: 'ease',
duration: theme.transitions.duration.shorter
}),
'&:hover': {
zIndex: 2,
boxShadow: 'var(--mui-customShadows-md)',
transform: 'translateY(-5px)'
}
}
})
}
},
MuiAvatar: {
styleOverrides: {
root: ({ theme }) => ({
color: 'var(--mui-palette-text-primary)',
fontSize: theme.typography.body1.fontSize,
lineHeight: 1.2
})
}
}
}
export default avatar
@@ -0,0 +1,16 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const backdrop: Theme['components'] = {
MuiBackdrop: {
styleOverrides: {
root: {
'&:not(.MuiBackdrop-invisible)': {
backgroundColor: 'var(--backdrop-color)'
}
}
}
}
}
export default backdrop
@@ -0,0 +1,19 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const badges: Theme['components'] = {
MuiBadge: {
styleOverrides: {
standard: ({ theme }) => ({
height: 22,
minWidth: 22,
borderRadius: 20,
fontSize: theme.typography.subtitle2.fontSize,
lineHeight: 1.07,
padding: theme.spacing(1, 2)
})
}
}
}
export default badges
@@ -0,0 +1,29 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const breadcrumbs: Theme['components'] = {
MuiBreadcrumbs: {
styleOverrides: {
root: {
'& svg, & i': {
fontSize: '1.25rem'
},
'& a': {
textDecoration: 'none',
color: 'var(--mui-palette-text-secondary)',
'&:hover': {
color: 'var(--mui-palette-text-primary)'
}
}
},
li: ({ theme }) => ({
lineHeight: theme.typography.body1.lineHeight,
'& > *:not(a)': {
color: 'var(--mui-palette-text-primary)'
}
})
}
}
}
export default breadcrumbs
@@ -0,0 +1,73 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Config Imports
import themeConfig from '@configs/themeConfig'
const buttonGroup: Theme['components'] = {
MuiButtonGroup: {
defaultProps: {
disableRipple: themeConfig.disableRipple
},
styleOverrides: {
contained: ({ ownerState }) => ({
boxShadow: 'var(--mui-customShadows-xs)',
...(ownerState.disabled && {
boxShadow: 'none'
})
})
},
variants: [
{
props: { variant: 'text', color: 'primary' },
style: {
'& .MuiButtonGroup-firstButton, & .MuiButtonGroup-middleButton': {
borderColor: 'var(--mui-palette-primary-main)'
}
}
},
{
props: { variant: 'text', color: 'secondary' },
style: {
'& .MuiButtonGroup-firstButton, & .MuiButtonGroup-middleButton': {
borderColor: 'var(--mui-palette-secondary-main)'
}
}
},
{
props: { variant: 'text', color: 'error' },
style: {
'& .MuiButtonGroup-firstButton, & .MuiButtonGroup-middleButton': {
borderColor: 'var(--mui-palette-error-main)'
}
}
},
{
props: { variant: 'text', color: 'warning' },
style: {
'& .MuiButtonGroup-firstButton, & .MuiButtonGroup-middleButton': {
borderColor: 'var(--mui-palette-warning-main)'
}
}
},
{
props: { variant: 'text', color: 'info' },
style: {
'& .MuiButtonGroup-firstButton, & .MuiButtonGroup-middleButton': {
borderColor: 'var(--mui-palette-info-main)'
}
}
},
{
props: { variant: 'text', color: 'success' },
style: {
'& .MuiButtonGroup-firstButton, & .MuiButtonGroup-middleButton': {
borderColor: 'var(--mui-palette-success-main)'
}
}
}
]
}
}
export default buttonGroup
@@ -0,0 +1,377 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Config Imports
import themeConfig from '@configs/themeConfig'
const iconStyles = (size?: string) => ({
'& > *:nth-of-type(1)': {
...(size === 'small'
? {
fontSize: '14px'
}
: {
...(size === 'medium'
? {
fontSize: '16px'
}
: {
fontSize: '20px'
})
})
}
})
const button: Theme['components'] = {
MuiButtonBase: {
defaultProps: {
disableRipple: themeConfig.disableRipple
}
},
MuiButton: {
styleOverrides: {
root: ({ theme, ownerState }) => ({
...(ownerState.variant === 'text'
? {
...(ownerState.size === 'small' && {
padding: theme.spacing(2, 2.5)
}),
...(ownerState.size === 'medium' && {
padding: theme.spacing(2, 3.5)
}),
...(ownerState.size === 'large' && {
padding: theme.spacing(2, 4.5)
})
}
: {
...(ownerState.variant === 'outlined'
? {
...(ownerState.size === 'small' && {
padding: theme.spacing(1.75, 3.25)
}),
...(ownerState.size === 'medium' && {
padding: theme.spacing(1.75, 4.25)
}),
...(ownerState.size === 'large' && {
padding: theme.spacing(1.75, 5.25)
})
}
: {
...(ownerState.size === 'small' && {
padding: theme.spacing(2, 3.5)
}),
...(ownerState.size === 'medium' && {
padding: theme.spacing(2, 4.5)
}),
...(ownerState.size === 'large' && {
padding: theme.spacing(2, 5.5)
})
})
})
}),
contained: ({ ownerState }) => ({
boxShadow: 'var(--mui-customShadows-xs)',
...(!ownerState.disabled && {
'&:hover, &.Mui-focusVisible': {
boxShadow: 'var(--mui-customShadows-xs)'
},
'&:active': {
boxShadow: 'none'
}
})
}),
sizeSmall: ({ theme }) => ({
lineHeight: 1.38462,
fontSize: theme.typography.body2.fontSize,
borderRadius: 'var(--mui-shape-customBorderRadius-sm)'
}),
sizeLarge: {
fontSize: '1.0625rem',
lineHeight: 1.529412,
borderRadius: 'var(--mui-shape-customBorderRadius-lg)'
},
startIcon: ({ theme, ownerState }) => ({
...(ownerState.size === 'small'
? {
marginInlineEnd: theme.spacing(1.5)
}
: {
...(ownerState.size === 'medium'
? {
marginInlineEnd: theme.spacing(2)
}
: {
marginInlineEnd: theme.spacing(2.5)
})
}),
...iconStyles(ownerState.size)
}),
endIcon: ({ theme, ownerState }) => ({
...(ownerState.size === 'small'
? {
marginInlineStart: theme.spacing(1.5)
}
: {
...(ownerState.size === 'medium'
? {
marginInlineStart: theme.spacing(2)
}
: {
marginInlineStart: theme.spacing(2.5)
})
}),
...iconStyles(ownerState.size)
})
},
variants: [
{
props: { variant: 'text', color: 'primary' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-primary-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-primary-main)'
}
}
},
{
props: { variant: 'text', color: 'secondary' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-secondary-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-secondary-main)'
}
}
},
{
props: { variant: 'text', color: 'error' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-error-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-error-main)'
}
}
},
{
props: { variant: 'text', color: 'warning' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-warning-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-warning-main)'
}
}
},
{
props: { variant: 'text', color: 'info' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-info-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-info-main)'
}
}
},
{
props: { variant: 'text', color: 'success' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-success-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-success-main)'
}
}
},
{
props: { variant: 'outlined', color: 'primary' },
style: {
borderColor: 'var(--mui-palette-primary-main)',
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-primary-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-primary-main)',
borderColor: 'var(--mui-palette-primary-main)'
}
}
},
{
props: { variant: 'outlined', color: 'secondary' },
style: {
borderColor: 'var(--mui-palette-secondary-main)',
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-secondary-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-secondary-main)',
borderColor: 'var(--mui-palette-secondary-main)'
}
}
},
{
props: { variant: 'outlined', color: 'error' },
style: {
borderColor: 'var(--mui-palette-error-main)',
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-error-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-error-main)',
borderColor: 'var(--mui-palette-error-main)'
}
}
},
{
props: { variant: 'outlined', color: 'warning' },
style: {
borderColor: 'var(--mui-palette-warning-main)',
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-warning-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-warning-main)',
borderColor: 'var(--mui-palette-warning-main)'
}
}
},
{
props: { variant: 'outlined', color: 'info' },
style: {
borderColor: 'var(--mui-palette-info-main)',
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-info-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-info-main)',
borderColor: 'var(--mui-palette-info-main)'
}
}
},
{
props: { variant: 'outlined', color: 'success' },
style: {
borderColor: 'var(--mui-palette-success-main)',
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))':
{
backgroundColor: 'var(--mui-palette-success-lighterOpacity)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-success-main)',
borderColor: 'var(--mui-palette-success-main)'
}
}
},
{
props: { variant: 'contained', color: 'primary' },
style: {
'&:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-primary-dark)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-primary-contrastText)',
backgroundColor: 'var(--mui-palette-primary-main)'
}
}
},
{
props: { variant: 'contained', color: 'secondary' },
style: {
'&:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-secondary-dark)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-secondary-contrastText)',
backgroundColor: 'var(--mui-palette-secondary-main)'
}
}
},
{
props: { variant: 'contained', color: 'error' },
style: {
'&:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-error-dark)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-error-contrastText)',
backgroundColor: 'var(--mui-palette-error-main)'
}
}
},
{
props: { variant: 'contained', color: 'warning' },
style: {
'&:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-warning-dark)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-warning-contrastText)',
backgroundColor: 'var(--mui-palette-warning-main)'
}
}
},
{
props: { variant: 'contained', color: 'info' },
style: {
'&:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-info-dark)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-info-contrastText)',
backgroundColor: 'var(--mui-palette-info-main)'
}
}
},
{
props: { variant: 'contained', color: 'success' },
style: {
'&:not(.Mui-disabled):active, &.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-success-dark)'
},
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-success-contrastText)',
backgroundColor: 'var(--mui-palette-success-main)'
}
}
}
]
}
}
export default button
@@ -0,0 +1,91 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { Skin } from '@core/types'
const card = (skin: Skin): Theme['components'] => {
return {
MuiCard: {
defaultProps: {
...(skin === 'bordered' && {
variant: 'outlined'
})
},
styleOverrides: {
root: ({ ownerState }) => ({
...(ownerState.variant !== 'outlined' && {
boxShadow: 'var(--mui-customShadows-md)'
})
})
}
},
MuiCardHeader: {
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(5),
'& + .MuiCardContent-root, & + .MuiCardActions-root': {
paddingBlockStart: 0
},
'& + .MuiCollapse-root .MuiCardContent-root:first-child, & + .MuiCollapse-root .MuiCardActions-root:first-child':
{
paddingBlockStart: 0
}
}),
subheader: ({ theme }) => ({
...theme.typography.subtitle1,
color: 'rgb(var(--mui-palette-text-primaryChannel) / 0.55)'
}),
action: ({ theme }) => ({
...theme.typography.body1,
color: 'var(--mui-palette-text-disabled)',
marginBlock: 0,
marginInlineEnd: 0,
'& .MuiIconButton-root': {
color: 'inherit'
}
})
}
},
MuiCardContent: {
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(5),
color: 'var(--mui-palette-text-secondary)',
'&:last-child': {
paddingBlockEnd: theme.spacing(5)
},
'& + .MuiCardHeader-root, & + .MuiCardContent-root, & + .MuiCardActions-root': {
paddingBlockStart: 0
},
'& + .MuiCollapse-root .MuiCardHeader-root:first-child, & + .MuiCollapse-root .MuiCardContent-root:first-child, & + .MuiCollapse-root .MuiCardActions-root:first-child':
{
paddingBlockStart: 0
}
})
}
},
MuiCardActions: {
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(5),
'&:where(.card-actions-dense)': {
padding: theme.spacing(2.5),
'& .MuiButton-text': {
paddingInline: theme.spacing(2.5)
}
},
'& + .MuiCardHeader-root, & + .MuiCardContent-root, & + .MuiCardActions-root': {
paddingBlockStart: 0
},
'& + .MuiCollapse-root .MuiCardHeader-root:first-child, & + .MuiCollapse-root .MuiCardContent-root:first-child, & + .MuiCollapse-root .MuiCardActions-root:first-child':
{
paddingBlockStart: 0
}
})
}
}
}
}
export default card
@@ -0,0 +1,94 @@
// React Imports
import React from 'react'
// MUI Imports
import type { Theme } from '@mui/material/styles'
const Icon = () => {
return (
<svg width='1em' height='1em' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M8 4h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z'
stroke='var(--mui-palette-text-secondary)'
strokeWidth='2'
/>
</svg>
)
}
const IndeterminateIcon = () => {
return (
<svg width='1em' height='1em' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M3 8a5 5 0 0 1 5-5h8a5 5 0 0 1 5 5v8a5 5 0 0 1-5 5H8a5 5 0 0 1-5-5V8Z' fill='currentColor' />
<path d='M8.5 11.5h7v1h-7v-1Z' fill='var(--mui-palette-common-white)' />
</svg>
)
}
const CheckedIcon = () => {
return (
<svg width='1em' height='1em' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M3 8a5 5 0 0 1 5-5h8a5 5 0 0 1 5 5v8a5 5 0 0 1-5 5H8a5 5 0 0 1-5-5V8Z' fill='currentColor' />
<path
d='m11 13.586 4.596-4.597.707.707L11 15l-3.182-3.182.707-.707L11 13.586Z'
fill='var(--mui-palette-common-white)'
/>
</svg>
)
}
const checkbox: Theme['components'] = {
MuiCheckbox: {
defaultProps: {
icon: <Icon />,
indeterminateIcon: <IndeterminateIcon />,
checkedIcon: <CheckedIcon />
},
styleOverrides: {
root: ({ theme, ownerState }) => ({
...(ownerState.size === 'small'
? {
padding: theme.spacing(1),
'& svg': {
fontSize: '1.25rem'
}
}
: {
padding: theme.spacing(1.5),
'& svg': {
fontSize: '1.5rem'
}
}),
'&.Mui-checked:not(.Mui-disabled) svg': {
filter: 'drop-shadow(var(--mui-customShadows-xs))'
},
'&.Mui-disabled': {
opacity: 0.45,
'&:not(.Mui-checked)': {
color: 'var(--mui-palette-text-secondary)'
},
'&.Mui-checked.MuiCheckbox-colorPrimary': {
color: 'var(--mui-palette-primary-main)'
},
'&.Mui-checked.MuiCheckbox-colorSecondary': {
color: 'var(--mui-palette-secondary-main)'
},
'&.Mui-checked.MuiCheckbox-colorError': {
color: 'var(--mui-palette-error-main)'
},
'&.Mui-checked.MuiCheckbox-colorWarning': {
color: 'var(--mui-palette-warning-main)'
},
'&.Mui-checked.MuiCheckbox-colorInfo': {
color: 'var(--mui-palette-info-main)'
},
'&.Mui-checked.MuiCheckbox-colorSuccess': {
color: 'var(--mui-palette-success-main)'
}
}
})
}
}
}
export default checkbox
@@ -0,0 +1,190 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const chip: Theme['components'] = {
MuiChip: {
variants: [
{
props: { variant: 'tonal', color: 'primary' },
style: {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
color: 'var(--mui-palette-primary-main)',
'&.Mui-focusVisible': {
backgroundColor: 'var(--mui-palette-primary-mainOpacity)'
},
'& .MuiChip-deleteIcon': {
color: 'rgb(var(--mui-palette-primary-mainChannel) / 0.7)',
'&:hover': {
color: 'var(--mui-palette-primary-main)'
}
},
'&.MuiChip-clickable:hover': {
backgroundColor: 'var(--mui-palette-primary-main)',
color: 'var(--mui-palette-common-white)'
}
}
},
{
props: { variant: 'tonal', color: 'secondary' },
style: {
backgroundColor: 'var(--mui-palette-secondary-lightOpacity)',
color: 'var(--mui-palette-secondary-main)',
'&.Mui-focusVisible': {
backgroundColor: 'var(--mui-palette-secondary-mainOpacity)'
},
'& .MuiChip-deleteIcon': {
color: 'rgb(var(--mui-palette-secondary-mainChannel) / 0.7)',
'&:hover': {
color: 'var(--mui-palette-secondary-main)'
}
},
'&.MuiChip-clickable:hover': {
backgroundColor: 'var(--mui-palette-secondary-main)',
color: 'var(--mui-palette-common-white)'
}
}
},
{
props: { variant: 'tonal', color: 'error' },
style: {
backgroundColor: 'var(--mui-palette-error-lightOpacity)',
color: 'var(--mui-palette-error-main)',
'&.Mui-focusVisible': {
backgroundColor: 'var(--mui-palette-error-mainOpacity)'
},
'& .MuiChip-deleteIcon': {
color: 'rgb(var(--mui-palette-error-mainChannel) / 0.7)',
'&:hover': {
color: 'var(--mui-palette-error-main)'
}
},
'&.MuiChip-clickable:hover': {
backgroundColor: 'var(--mui-palette-error-main)',
color: 'var(--mui-palette-common-white)'
}
}
},
{
props: { variant: 'tonal', color: 'warning' },
style: {
backgroundColor: 'var(--mui-palette-warning-lightOpacity)',
color: 'var(--mui-palette-warning-main)',
'&.Mui-focusVisible': {
backgroundColor: 'var(--mui-palette-warning-mainOpacity)'
},
'& .MuiChip-deleteIcon': {
color: 'rgb(var(--mui-palette-warning-mainChannel) / 0.7)',
'&:hover': {
color: 'var(--mui-palette-warning-main)'
}
},
'&.MuiChip-clickable:hover': {
backgroundColor: 'var(--mui-palette-warning-main)',
color: 'var(--mui-palette-common-white)'
}
}
},
{
props: { variant: 'tonal', color: 'info' },
style: {
backgroundColor: 'var(--mui-palette-info-lightOpacity)',
color: 'var(--mui-palette-info-main)',
'&.Mui-focusVisible': {
backgroundColor: 'var(--mui-palette-info-mainOpacity)'
},
'& .MuiChip-deleteIcon': {
color: 'rgb(var(--mui-palette-info-mainChannel) / 0.7)',
'&:hover': {
color: 'var(--mui-palette-info-main)'
}
},
'&.MuiChip-clickable:hover': {
backgroundColor: 'var(--mui-palette-info-main)',
color: 'var(--mui-palette-common-white)'
}
}
},
{
props: { variant: 'tonal', color: 'success' },
style: {
backgroundColor: 'var(--mui-palette-success-lightOpacity)',
color: 'var(--mui-palette-success-main)',
'&.Mui-focusVisible': {
backgroundColor: 'var(--mui-palette-success-mainOpacity)'
},
'& .MuiChip-deleteIcon': {
color: 'rgb(var(--mui-palette-success-mainChannel) / 0.7)',
'&:hover': {
color: 'var(--mui-palette-success-main)'
}
},
'&.MuiChip-clickable:hover': {
backgroundColor: 'var(--mui-palette-success-main)',
color: 'var(--mui-palette-common-white)'
}
}
}
],
styleOverrides: {
root: ({ ownerState, theme }) => ({
...theme.typography.body2,
fontWeight: theme.typography.fontWeightMedium,
'& .MuiChip-deleteIcon': {
...(ownerState.size === 'small'
? {
fontSize: '1rem',
marginInlineEnd: theme.spacing(1),
marginInlineStart: theme.spacing(-2)
}
: {
fontSize: '1.25rem',
marginInlineEnd: theme.spacing(2),
marginInlineStart: theme.spacing(-3)
})
},
'& .MuiChip-avatar, & .MuiChip-icon': {
'& i, & svg': {
...(ownerState.size === 'small'
? {
fontSize: 13
}
: {
fontSize: 15
})
},
...(ownerState.size === 'small'
? {
height: 16,
width: 16,
marginInlineStart: theme.spacing(1),
marginInlineEnd: theme.spacing(-2)
}
: {
height: 20,
width: 20,
marginInlineStart: theme.spacing(2),
marginInlineEnd: theme.spacing(-3)
})
}
}),
label: ({ ownerState, theme }) => ({
...(ownerState.size === 'small'
? {
paddingInline: theme.spacing(3)
}
: {
paddingInline: theme.spacing(4)
})
}),
iconMedium: {
fontSize: '1.25rem'
},
iconSmall: {
fontSize: '1rem'
}
}
}
}
export default chip
@@ -0,0 +1,67 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { Skin } from '@core/types'
const dialog = (skin: Skin): Theme['components'] => ({
MuiDialog: {
styleOverrides: {
paper: ({ theme }) => ({
...(skin !== 'bordered'
? {
boxShadow: 'var(--mui-customShadows-xl)'
}
: {
boxShadow: 'none'
}),
[theme.breakpoints.down('sm')]: {
'&:not(.MuiDialog-paperFullScreen)': {
margin: theme.spacing(6)
}
}
})
}
},
MuiDialogTitle: {
defaultProps: {
variant: 'h5'
},
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(5),
'& + .MuiDialogActions-root': {
paddingTop: 0
}
})
}
},
MuiDialogContent: {
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(5),
'& + .MuiDialogContent-root, & + .MuiDialogActions-root': {
paddingTop: 0
}
})
}
},
MuiDialogActions: {
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(5),
'& .MuiButtonBase-root:not(:first-of-type)': {
marginInlineStart: theme.spacing(4)
},
'&:where(.dialog-actions-dense)': {
padding: theme.spacing(2.5),
'& .MuiButton-text': {
paddingInline: theme.spacing(2.5)
}
}
})
}
}
})
export default dialog
@@ -0,0 +1,26 @@
// MUI Imports
import type { Theme } from '@mui/material'
// Type Imports
import type { Skin } from '@core/types'
const drawer = (skin: Skin): Theme['components'] => ({
MuiDrawer: {
defaultProps: {
...(skin === 'bordered' && {
PaperProps: {
elevation: 0
}
})
},
styleOverrides: {
paper: {
...(skin !== 'bordered' && {
boxShadow: 'var(--mui-customShadows-lg)'
})
}
}
}
})
export default drawer
@@ -0,0 +1,68 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const fab: Theme['components'] = {
MuiFab: {
variants: [
{
props: { color: 'default' },
style: {
color: 'rgb(var(--mui-mainColorChannels-light) / 0.9)',
'&.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-grey-A100)'
}
}
},
{
props: { color: 'primary' },
style: {
'&.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-primary-dark)'
}
}
},
{
props: { color: 'secondary' },
style: {
'&.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-secondary-dark)'
}
}
},
{
props: { color: 'error' },
style: {
'&.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-error-dark)'
}
}
},
{
props: { color: 'warning' },
style: {
'&.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-warning-dark)'
}
}
},
{
props: { color: 'info' },
style: {
'&.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-info-dark)'
}
}
},
{
props: { color: 'success' },
style: {
'&.Mui-focusVisible:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'var(--mui-palette-success-dark)'
}
}
}
]
}
}
export default fab
@@ -0,0 +1,22 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const formControlLabel: Theme['components'] = {
MuiFormControlLabel: {
styleOverrides: {
root: ({ theme }) => ({
marginInlineStart: theme.spacing(-2)
}),
label: {
'&, &.Mui-disabled': {
color: 'var(--mui-palette-text-primary)'
},
'&.Mui-disabled': {
opacity: 0.45
}
}
}
}
}
export default formControlLabel
@@ -0,0 +1,140 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Config Imports
import themeConfig from '@configs/themeConfig'
const iconButton: Theme['components'] = {
MuiIconButton: {
styleOverrides: {
root: {
'& .MuiSvgIcon-root, & i, & svg': {
fontSize: 'inherit'
}
},
sizeSmall: ({ theme }) => ({
padding: theme.spacing(1.75),
fontSize: '1.25rem'
}),
sizeMedium: ({ theme }) => ({
padding: theme.spacing(2),
fontSize: '1.375rem'
}),
sizeLarge: ({ theme }) => ({
padding: theme.spacing(2.25),
fontSize: '1.5rem'
})
},
variants: [
{
props: { color: 'default' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active': {
backgroundColor: 'rgb(var(--mui-palette-text-primaryChannel) / 0.08)'
},
...(themeConfig.disableRipple && {
'&.Mui-focusVisible:not(.Mui-disabled)': {
backgroundColor: 'rgb(var(--mui-palette-text-primaryChannel) / 0.08)'
}
}),
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-action-active)'
}
}
},
{
props: { color: 'primary' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active': {
backgroundColor: 'var(--mui-palette-primary-lighterOpacity)'
},
...(themeConfig.disableRipple && {
'&.Mui-focusVisible:not(.Mui-disabled)': { backgroundColor: 'var(--mui-palette-primary-lighterOpacity)' }
}),
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-primary-main)'
}
}
},
{
props: { color: 'secondary' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active': {
backgroundColor: 'var(--mui-palette-secondary-lighterOpacity)'
},
...(themeConfig.disableRipple && {
'&.Mui-focusVisible:not(.Mui-disabled)': { backgroundColor: 'var(--mui-palette-secondary-lighterOpacity)' }
}),
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-secondary-main)'
}
}
},
{
props: { color: 'error' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active': {
backgroundColor: 'var(--mui-palette-error-lighterOpacity)'
},
...(themeConfig.disableRipple && {
'&.Mui-focusVisible:not(.Mui-disabled)': { backgroundColor: 'var(--mui-palette-error-lighterOpacity)' }
}),
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-error-main)'
}
}
},
{
props: { color: 'warning' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active': {
backgroundColor: 'var(--mui-palette-warning-lighterOpacity)'
},
...(themeConfig.disableRipple && {
'&.Mui-focusVisible:not(.Mui-disabled)': { backgroundColor: 'var(--mui-palette-warning-lighterOpacity)' }
}),
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-warning-main)'
}
}
},
{
props: { color: 'info' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active': {
backgroundColor: 'var(--mui-palette-info-lighterOpacity)'
},
...(themeConfig.disableRipple && {
'&.Mui-focusVisible:not(.Mui-disabled)': { backgroundColor: 'var(--mui-palette-info-lighterOpacity)' }
}),
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-info-main)'
}
}
},
{
props: { color: 'success' },
style: {
'&:not(.Mui-disabled):hover, &:not(.Mui-disabled):active': {
backgroundColor: 'var(--mui-palette-success-lighterOpacity)'
},
...(themeConfig.disableRipple && {
'&.Mui-focusVisible:not(.Mui-disabled)': { backgroundColor: 'var(--mui-palette-success-lighterOpacity)' }
}),
'&.Mui-disabled': {
opacity: 0.45,
color: 'var(--mui-palette-success-main)'
}
}
}
]
}
}
export default iconButton
@@ -0,0 +1,83 @@
// Override Imports
import Accordion from './accordion'
import Alerts from './alerts'
import Autocomplete from './autocomplete'
import avatar from './avatar'
import backdrop from './backdrop'
import badges from './badges'
import breadcrumbs from './breadcrumbs'
import button from './button'
import buttonGroup from './button-group'
import card from './card'
import Checkbox from './checkbox'
import chip from './chip'
import dialog from './dialog'
import drawer from './drawer'
import fab from './fab'
import formControlLabel from './form-control-label'
import iconButton from './icon-button'
import input from './input'
import list from './list'
import menu from './menu'
import pagination from './pagination'
import paper from './paper'
import popover from './popover'
import progress from './progress'
import Radio from './radio'
import Rating from './rating'
import Select from './select'
import slider from './slider'
import snackbar from './snackbar'
import switchOverrides from './switch'
import tablePagination from './table-pagination'
import tabs from './tabs'
import timeline from './timeline'
import toggleButton from './toggle-button'
import tooltip from './tooltip'
import typography from './typography'
const overrides = () => {
const skin = 'default'
return Object.assign(
{},
Accordion(skin),
Alerts,
Autocomplete(skin),
avatar,
backdrop,
badges,
breadcrumbs,
button,
buttonGroup,
card(skin),
Checkbox,
chip,
dialog(skin),
drawer(skin),
fab,
formControlLabel,
iconButton,
input,
list,
menu(skin),
pagination,
paper,
popover(skin),
progress,
Radio,
Rating,
Select,
slider,
snackbar(skin),
switchOverrides,
tablePagination,
tabs,
timeline,
toggleButton,
tooltip,
typography
)
}
export default overrides
@@ -0,0 +1,112 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const input: Theme['components'] = {
MuiFormControl: {
styleOverrides: {
root: {
'&:has(.MuiRadio-root) .MuiFormHelperText-root, &:has(.MuiCheckbox-root) .MuiFormHelperText-root, &:has(.MuiSwitch-root) .MuiFormHelperText-root':
{
marginInline: 0
}
}
}
},
MuiInputBase: {
styleOverrides: {
root: {
lineHeight: 1.6,
'&.MuiInput-underline': {
'&:before': {
borderColor: 'var(--mui-palette-customColors-inputBorder)'
},
'&:not(.Mui-disabled, .Mui-error):hover:before': {
borderColor: 'var(--mui-palette-action-active)'
}
},
'&.Mui-disabled .MuiInputAdornment-root, &.Mui-disabled .MuiInputAdornment-root > *': {
color: 'var(--mui-palette-action-disabled)'
}
}
}
},
MuiFilledInput: {
styleOverrides: {
root: {
'&:before': {
borderBottom: '1px solid var(--mui-palette-text-secondary)'
},
'&.Mui-disabled:before': {
borderBottomStyle: 'solid'
}
}
}
},
MuiInputLabel: {
styleOverrides: {
shrink: ({ ownerState }) => ({
...(ownerState.variant === 'outlined' && {
color: 'var(--mui-palette-text-secondary)',
transform: 'translate(14px, -8px) scale(0.867)'
}),
...(ownerState.variant === 'filled' && {
transform: 'translate(12px, 7px) scale(0.867)'
}),
...(ownerState.variant === 'standard' && {
transform: 'translate(0, -1.5px) scale(0.867)'
})
})
}
},
MuiOutlinedInput: {
styleOverrides: {
root: {
'&:not(.Mui-focused):not(.Mui-error):not(.Mui-disabled):hover .MuiOutlinedInput-notchedOutline': {
borderColor: 'var(--mui-palette-action-active)'
},
'&.Mui-disabled .MuiOutlinedInput-notchedOutline': {
borderColor: 'var(--mui-palette-action-disabledBackground)'
}
},
input: ({ theme, ownerState }) => ({
...(ownerState?.size === 'medium' && {
'&:not(.MuiInputBase-inputMultiline, .MuiInputBase-inputAdornedStart)': {
paddingBlock: theme.spacing(4)
},
height: '1.5em'
}),
'& ~ .MuiOutlinedInput-notchedOutline': {
borderColor: 'var(--mui-palette-customColors-inputBorder)'
}
}),
notchedOutline: {
'& legend': {
fontSize: '0.867em'
}
}
}
},
MuiInputAdornment: {
styleOverrides: {
root: {
color: 'var(--mui-palette-text-primary)',
'& i, & svg': {
fontSize: '1.25rem'
},
'& *': {
color: 'inherit !important'
}
}
}
},
MuiFormHelperText: {
styleOverrides: {
root: {
lineHeight: 1,
letterSpacing: 'unset'
}
}
}
}
export default input
@@ -0,0 +1,81 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const list: Theme['components'] = {
MuiListItem: {
styleOverrides: {
root: ({ theme }) => ({
gap: theme.spacing(4)
}),
padding: ({ theme, ownerState }) => ({
...(!ownerState.dense && {
paddingBlock: theme.spacing(2),
paddingInlineStart: theme.spacing(5)
})
})
}
},
MuiListItemAvatar: {
styleOverrides: {
root: {
minWidth: 'unset'
}
}
},
MuiListItemIcon: {
styleOverrides: {
root: {
minWidth: 0,
color: 'var(--mui-palette-text-primary)',
fontSize: '1.375rem',
'& > svg, & > i': {
fontSize: 'inherit'
}
}
}
},
MuiListItemButton: {
styleOverrides: {
root: ({ theme, ownerState }) => ({
gap: theme.spacing(4),
...(!ownerState.dense && {
paddingBlock: theme.spacing(2)
}),
paddingInlineStart: theme.spacing(5),
'&.Mui-selected': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
'&:hover, &.Mui-focused, &.Mui-focusVisible': {
backgroundColor: 'var(--mui-palette-primary-mainOpacity)'
},
'& .MuiTypography-root': {
color: 'var(--mui-palette-primary-main)'
},
'& + .MuiListItemSecondaryAction-root .MuiIconButton-root': {
color: 'var(--mui-palette-primary-main)'
}
}
})
}
},
MuiListItemText: {
styleOverrides: {
root: {
margin: 0
},
primary: {
color: 'var(--mui-palette-text-primary)'
}
}
},
MuiListSubheader: {
styleOverrides: {
root: ({ theme }) => ({
...theme.typography.subtitle2,
paddingBlock: 10,
paddingInline: theme.spacing(5)
})
}
}
}
export default list
@@ -0,0 +1,57 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { Skin } from '@core/types'
const menu = (skin: Skin): Theme['components'] => ({
MuiMenu: {
defaultProps: {
...(skin === 'bordered' && {
slotProps: {
paper: {
elevation: 0
}
}
})
},
styleOverrides: {
paper: ({ theme }) => ({
marginBlockStart: theme.spacing(0.5),
...(skin !== 'bordered' && {
boxShadow: 'var(--mui-customShadows-lg)'
})
})
}
},
MuiMenuItem: {
styleOverrides: {
root: ({ theme }) => ({
paddingBlock: theme.spacing(2),
color: 'var(--mui-palette-text-primary)',
'& i, & svg': {
fontSize: '1.375rem'
},
'& .MuiListItemIcon-root': {
minInlineSize: 0
},
'&.Mui-selected': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
color: 'var(--mui-palette-primary-main)',
'& .MuiListItemIcon-root': {
color: 'var(--mui-palette-primary-main)'
},
'&:hover, &.Mui-focused, &.Mui-focusVisible': {
backgroundColor: 'var(--mui-palette-primary-mainOpacity)'
}
},
'&.Mui-disabled': {
color: 'var(--mui-palette-text-disabled)',
opacity: 1
}
})
}
}
})
export default menu
@@ -0,0 +1,172 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const pagination: Theme['components'] = {
MuiPagination: {
styleOverrides: {
ul: {
rowGap: 6
}
},
variants: [
{
props: { variant: 'text', color: 'primary' },
style: {
'& .MuiPaginationItem-root.Mui-selected.Mui-disabled': {
backgroundColor: 'var(--mui-palette-primary-main)',
color: 'var(--mui-palette-primary-contrastText)'
}
}
},
{
props: { variant: 'text', color: 'secondary' },
style: {
'& .MuiPaginationItem-root.Mui-selected.Mui-disabled': {
backgroundColor: 'var(--mui-palette-secondary-main)',
color: 'var(--mui-palette-secondary-contrastText)'
}
}
},
{
props: { variant: 'outlined', color: 'standard' },
style: {
'& .MuiPaginationItem-root.Mui-selected.Mui-disabled': {
borderColor: 'var(--mui-palette-action-selected)'
}
}
},
{
props: { variant: 'outlined', color: 'primary' },
style: {
'& .MuiPaginationItem-root.Mui-selected.Mui-disabled': {
color: 'var(--mui-palette-primary-main)',
backgroundColor: 'rgb(var(--mui-palette-primary-mainChannel) / var(--mui-palette-action-activatedOpacity))',
borderColor: 'rgba(var(--mui-palette-primary-mainChannel) / 0.5)'
}
}
},
{
props: { variant: 'outlined', color: 'secondary' },
style: {
'& .MuiPaginationItem-root.Mui-selected.Mui-disabled': {
color: 'var(--mui-palette-secondary-main)',
backgroundColor:
'rgb(var(--mui-palette-secondary-mainChannel) / var(--mui-palette-action-activatedOpacity))',
borderColor: 'rgba(var(--mui-palette-secondary-mainChannel) / 0.5)'
}
}
},
{
props: { variant: 'tonal' },
style: {
'& .MuiPaginationItem-root:not(.MuiPaginationItem-ellipsis)': {
backgroundColor: 'var(--mui-palette-action-selected)'
}
}
},
{
props: { variant: 'tonal', color: 'standard' },
style: {
'& .MuiPaginationItem-root.Mui-selected': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
color: 'var(--mui-palette-primary-main)',
'&:hover': {
backgroundColor: 'var(--mui-palette-primary-mainOpacity)'
}
},
'& .MuiPaginationItem-root:hover:not(.Mui-selected):not(.MuiPaginationItem-ellipsis)': {
backgroundColor: 'var(--mui-palette-action-focus)'
},
'& .MuiPaginationItem-root.Mui-selected.Mui-disabled': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
color: 'var(--mui-palette-primary-main)'
}
}
},
{
props: { variant: 'tonal', color: 'primary' },
style: {
'& .MuiPaginationItem-root.Mui-selected': {
backgroundColor: 'var(--mui-palette-primary-main)',
color: 'var(--mui-palette-primary-contrastText)',
'&:not(.Mui-disabled)': {
boxShadow: 'var(--mui-customShadows-xs)'
},
'&:hover': {
backgroundColor: 'var(--mui-palette-primary-dark)'
}
},
'& .MuiPaginationItem-root:hover:not(.Mui-selected):not(.MuiPaginationItem-ellipsis)': {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
color: 'var(--mui-palette-primary-main)'
},
'& .MuiPaginationItem-root.Mui-selected.Mui-disabled': {
backgroundColor: 'var(--mui-palette-primary-main)',
color: 'var(--mui-palette-primary-contrastText)'
}
}
},
{
props: { variant: 'tonal', color: 'secondary' },
style: {
'& .MuiPaginationItem-root.Mui-selected': {
backgroundColor: 'var(--mui-palette-secondary-main)',
color: 'var(--mui-palette-secondary-contrastText)',
'&:not(.Mui-disabled)': {
boxShadow: 'var(--mui-customShadows-xs)'
},
'&:hover': {
backgroundColor: 'var(--mui-palette-secondary-dark)'
}
},
'& .MuiPaginationItem-root:hover:not(.Mui-selected):not(.MuiPaginationItem-ellipsis)': {
backgroundColor: 'var(--mui-palette-secondary-mainOpacity)',
color: 'var(--mui-palette-secondary-main)'
},
'& .MuiPaginationItem-root.Mui-selected.Mui-disabled': {
backgroundColor: 'var(--mui-palette-secondary-main)',
color: 'var(--mui-palette-secondary-contrastText)'
}
}
}
]
},
MuiPaginationItem: {
styleOverrides: {
root: ({ ownerState }) => ({
...(ownerState.size === 'medium' && {
height: '2.375rem',
minWidth: '2.375rem'
}),
...(ownerState.shape !== 'rounded' && {
borderRadius: '50px'
}),
'&.Mui-selected.Mui-disabled': {
color: 'var(--mui-palette-text-primary)',
opacity: 0.45
},
'&.Mui-disabled': {
opacity: 0.45
},
...(ownerState.shape === 'rounded' &&
ownerState.size === 'small' && {
borderRadius: 'var(--mui-shape-customBorderRadius-sm)'
}),
...(ownerState.shape === 'rounded' &&
ownerState.size === 'large' && {
borderRadius: 'var(--mui-shape-customBorderRadius-lg)'
})
}),
sizeSmall: {
height: '2.125rem',
minWidth: '2.125rem'
},
sizeLarge: {
height: '2.625rem',
minWidth: '2.625rem'
}
}
}
}
export default pagination
@@ -0,0 +1,14 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const paper: Theme['components'] = {
MuiPaper: {
styleOverrides: {
root: {
backgroundImage: 'none'
}
}
}
}
export default paper
@@ -0,0 +1,21 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { Skin } from '@core/types'
const popover = (skin: Skin): Theme['components'] => ({
MuiPopover: {
styleOverrides: {
paper: {
...(skin === 'bordered'
? { boxShadow: 'none', border: '1px solid var(--mui-palette-divider)' }
: {
boxShadow: 'var(--mui-customShadows-sm)'
})
}
}
}
})
export default popover
@@ -0,0 +1,18 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const progress: Theme['components'] = {
MuiLinearProgress: {
styleOverrides: {
root: ({ theme }) => ({
height: 6,
borderRadius: theme.shape.borderRadius,
'& .MuiLinearProgress-bar': {
borderRadius: theme.shape.borderRadius
}
})
}
}
}
export default progress
@@ -0,0 +1,81 @@
// React Imports
import React from 'react'
// MUI Imports
import type { Theme } from '@mui/material/styles'
const IconChecked = () => {
return (
<svg width='1em' height='1em' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M12 18.5a6.5 6.5 0 1 1 0-13 6.5 6.5 0 0 1 0 13Z'
fill='var(--mui-palette-common-white)'
stroke='currentColor'
strokeWidth='5'
/>
</svg>
)
}
const UncheckedIcon = () => {
return (
<svg width='1em' height='1em' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M12 20a8 8 0 1 1 0-16 8 8 0 0 1 0 16Z' stroke='var(--mui-palette-text-secondary)' strokeWidth='2' />
</svg>
)
}
const radio: Theme['components'] = {
MuiRadio: {
defaultProps: {
icon: <UncheckedIcon />,
checkedIcon: <IconChecked />
},
styleOverrides: {
root: ({ theme, ownerState }) => ({
...(ownerState.size === 'small'
? {
padding: theme.spacing(1),
'& svg': {
fontSize: '1.25rem'
}
}
: {
padding: theme.spacing(1.5),
'& svg': {
fontSize: '1.5rem'
}
}),
'&.Mui-checked:not(.Mui-disabled) svg': {
filter: 'drop-shadow(var(--mui-customShadows-xs))'
},
'&.Mui-disabled': {
opacity: 0.45,
'&:not(.Mui-checked)': {
color: 'var(--mui-palette-text-secondary)'
},
'&.Mui-checked.MuiRadio-colorPrimary': {
color: 'var(--mui-palette-primary-main)'
},
'&.Mui-checked.MuiRadio-colorSecondary': {
color: 'var(--mui-palette-secondary-main)'
},
'&.Mui-checked.MuiRadio-colorError': {
color: 'var(--mui-palette-error-main)'
},
'&.Mui-checked.MuiRadio-colorWarning': {
color: 'var(--mui-palette-warning-main)'
},
'&.Mui-checked.MuiRadio-colorInfo': {
color: 'var(--mui-palette-info-main)'
},
'&.Mui-checked.MuiRadio-colorSuccess': {
color: 'var(--mui-palette-success-main)'
}
}
})
}
}
}
export default radio
@@ -0,0 +1,35 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const rating: Theme['components'] = {
MuiRating: {
defaultProps: {
emptyIcon: <i className='ri-star-line' />,
icon: <i className='ri-star-fill' />
},
styleOverrides: {
root: {
gap: '2px',
color: 'var(--mui-palette-warning-main)',
'& i, & svg': {
flexShrink: 0
},
'& .MuiRating-decimal > label:first-of-type, & .MuiRating-decimal > span:first-of-type': {
zIndex: 1
}
},
sizeSmall: {
'& .MuiRating-icon i, & .MuiRating-icon svg': {
fontSize: '1.25rem'
}
},
sizeLarge: {
'& .MuiRating-icon i, & .MuiRating-icon svg': {
fontSize: '1.75rem'
}
}
}
}
}
export default rating
@@ -0,0 +1,61 @@
// React Imports
import React from 'react'
// MUI Imports
import type { Theme } from '@mui/material/styles'
const SelectIcon = () => {
return <i className='ri-arrow-down-s-line' />
}
const iconStyles = (theme: Theme) => ({
userSelect: 'none',
display: 'inline-block',
fill: 'currentColor',
flexShrink: 0,
transition: theme.transitions.create('fill', {
duration: theme.transitions.duration.shorter
}),
fontSize: '1.25rem',
position: 'absolute',
right: '1rem',
top: 'calc(50% - 0.5em)',
pointerEvents: 'none'
})
const select: Theme['components'] = {
MuiSelect: {
defaultProps: {
IconComponent: SelectIcon
},
styleOverrides: {
select: ({ theme, ownerState }) => ({
...(ownerState.variant === 'outlined' && {
minHeight: '1.5em'
}),
'&[aria-expanded="true"] ~ i, &[aria-expanded="true"] ~ svg': {
transform: 'rotate(180deg)'
},
'& ~ i, & ~ svg': iconStyles(theme as Theme),
'&.MuiInputBase-inputSizeSmall': {
'& ~ i, & ~ svg': {
height: '1.375rem',
width: '1.375rem'
}
},
'&:not(aria-label="Without label") ~ .MuiOutlinedInput-notchedOutline > legend > span': {
paddingInline: '5px'
}
})
}
},
MuiNativeSelect: {
styleOverrides: {
select: ({ theme }) => ({
'& + i, & + svg': iconStyles(theme as Theme)
})
}
}
}
export default select
@@ -0,0 +1,100 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const slider: Theme['components'] = {
MuiSlider: {
styleOverrides: {
root: ({ ownerState }) => ({
boxSizing: 'border-box',
...(ownerState.orientation === 'horizontal'
? ownerState.size !== 'small'
? { height: 6 }
: { height: 4 }
: ownerState.size !== 'small'
? { width: 6 }
: { width: 4 }),
'&.Mui-disabled': {
opacity: 0.45,
color: `var(--mui-palette-${ownerState.color}-main)`
}
}),
thumb: ({ ownerState }) => ({
...(ownerState.size === 'small'
? {
height: 14,
width: 14,
border: '2px solid currentColor',
'&:hover, &.Mui-focusVisible': {
boxShadow: `0 0 0 7px var(--mui-palette-${ownerState.color}-lightOpacity)`
},
'&.Mui-active.Mui-focusVisible': {
boxShadow: `0 0 0 10px var(--mui-palette-${ownerState.color}-lightOpacity)`
}
}
: {
height: 22,
width: 22,
border: '4px solid currentColor'
}),
backgroundColor: 'var(--mui-palette-common-white)',
...(!ownerState.disabled && {
boxShadow: 'var(--mui-customShadows-sm)'
}),
'&:before': {
boxShadow: 'none'
},
'&:after': {
...(ownerState.size === 'small'
? {
height: 28,
width: 28
}
: {
height: 38,
width: 38
})
},
'&:hover, &.Mui-focusVisible': {
boxShadow: `0 0 0 8px var(--mui-palette-${ownerState.color}-lightOpacity)`
},
'&.Mui-active.Mui-focusVisible': {
boxShadow: `0 0 0 13px var(--mui-palette-${ownerState.color}-lightOpacity)`
}
}),
rail: ({ ownerState }) => ({
opacity: 1,
color: `var(--mui-palette-${ownerState.color}-lightOpacity)`,
...(ownerState.track === 'inverted' && {
backgroundColor: `var(--mui-palette-${ownerState.color}-main)`
})
}),
valueLabel: ({ theme, ownerState }) => ({
...(ownerState.size === 'small'
? {
...theme.typography.caption,
borderRadius: 'var(--mui-shape-customBorderRadius-sm)',
padding: theme.spacing(1, 2)
}
: {
...theme.typography.body2,
fontWeight: theme.typography.fontWeightMedium,
borderRadius: 'var(--mui-shape-borderRadius)',
padding: theme.spacing(1, 2.5)
}),
color: 'var(--mui-palette-customColors-tooltipText)',
backgroundColor: 'var(--mui-palette-Tooltip-bg)',
'&:before': {
display: 'none'
}
}),
track: ({ theme, ownerState }) => ({
...(ownerState.track === 'inverted' && {
backgroundColor: `color-mix(in srgb, ${theme.palette[ownerState.color || 'primary'].main} 16%, var(--mui-palette-background-paper))`,
borderColor: `color-mix(in srgb, ${theme.palette[ownerState.color || 'primary'].main} 16%, var(--mui-palette-background-paper))`
})
})
}
}
}
export default slider
@@ -0,0 +1,27 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { Skin } from '@core/types'
const snackbar = (skin: Skin): Theme['components'] => ({
MuiSnackbarContent: {
styleOverrides: {
root: ({ theme }) => ({
padding: theme.spacing(0, 4),
...(skin !== 'bordered'
? {
boxShadow: 'var(--mui-customShadows-xs)'
}
: {
boxShadow: 'none'
}),
'& .MuiSnackbarContent-message': {
paddingBlock: theme.spacing(3)
}
})
}
}
})
export default snackbar
@@ -0,0 +1,69 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const switchOverrides: Theme['components'] = {
MuiSwitch: {
defaultProps: {
disableRipple: true
},
styleOverrides: {
root: ({ theme, ownerState }) => ({
'&:has(.Mui-disabled)': {
opacity: 0.45
},
...(ownerState.size !== 'small'
? {
width: 46,
height: 36,
padding: theme.spacing(2.25, 2)
}
: {
width: 42,
height: 30,
padding: theme.spacing(1.75, 2),
'& .MuiSwitch-thumb': {
width: 12,
height: 12
},
'& .MuiSwitch-switchBase': {
padding: 7,
left: 3,
'&.Mui-checked': {
left: -3
}
}
})
}),
switchBase: {
top: 2,
left: 1,
'&.Mui-checked': {
left: -7,
color: 'var(--mui-palette-common-white)',
'& + .MuiSwitch-track': {
opacity: 1
}
},
'&.Mui-disabled + .MuiSwitch-track': {
opacity: 1
},
'&:hover:not(:has(span.MuiTouchRipple-root))': {
backgroundColor: 'transparent'
}
},
thumb: {
width: 14,
height: 14,
boxShadow: 'var(--mui-customShadows-xs)'
},
track: {
opacity: 1,
borderRadius: 10,
backgroundColor: 'var(--mui-palette-action-focus)',
boxShadow: `0 0 4px rgb(0 0 0 / 0.16) inset`
}
}
}
}
export default switchOverrides
@@ -0,0 +1,42 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const tablePagination: Theme['components'] = {
MuiTablePagination: {
styleOverrides: {
toolbar: ({ theme }) => ({
paddingInlineEnd: `${theme.spacing(3)} !important`
}),
select: ({ theme }) => ({
...theme.typography.body1,
paddingInlineStart: 0,
'& ~ i, & ~ svg': {
fontSize: 20,
right: '2px !important',
color: 'var(--mui-palette-action-active)'
}
}),
selectLabel: ({ theme }) => ({
...theme.typography.body1,
color: 'var(--mui-palette-text-secondary)'
}),
input: ({ theme }) => ({
marginInlineEnd: theme.spacing(6)
}),
displayedRows: ({ theme }) => ({
...theme.typography.body1
}),
actions: ({ theme }) => ({
marginInlineStart: theme.spacing(6),
'& .Mui-disabled': {
color: 'var(--mui-palette-action-active)'
},
'& .MuiIconButton-root:last-of-type': {
marginInlineStart: theme.spacing(2)
}
})
}
}
}
export default tablePagination
@@ -0,0 +1,92 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const tabs: Theme['components'] = {
MuiTabs: {
styleOverrides: {
root: ({ theme, ownerState }) => ({
minBlockSize: 38,
...(ownerState.orientation === 'horizontal'
? {
borderBlockEnd: '1px solid var(--mui-palette-divider)'
}
: {
borderInlineEnd: '1px solid var(--mui-palette-divider)'
}),
'& .MuiTab-root:hover': {
...(ownerState.orientation === 'horizontal'
? {
paddingBlockEnd: theme.spacing(1.5),
...(ownerState.textColor === 'secondary'
? {
color: 'var(--mui-palette-secondary-main)',
borderBlockEnd: '2px solid var(--mui-palette-secondary-lightOpacity)'
}
: {
color: 'var(--mui-palette-primary-main)',
borderBlockEnd: '2px solid var(--mui-palette-primary-lightOpacity)'
})
}
: {
paddingInlineEnd: theme.spacing(5),
...(ownerState.textColor === 'secondary'
? {
color: 'var(--mui-palette-secondary-main)',
borderInlineEnd: '2px solid var(--mui-palette-secondary-mainOpacity)'
}
: {
color: 'var(--mui-palette-primary-main)',
borderInlineEnd: '2px solid var(--mui-palette-primary-mainOpacity)'
})
}),
'& .MuiTabScrollButton-root': {
borderRadius: theme.shape.borderRadius
}
},
'& ~ .MuiTabPanel-root': {
...(ownerState.orientation === 'horizontal'
? {
paddingBlockStart: theme.spacing(5)
}
: {
paddingInlineStart: theme.spacing(5)
})
}
}),
vertical: {
minWidth: 131,
'& .MuiTab-root': {
minWidth: 130
}
}
}
},
MuiTab: {
styleOverrides: {
root: ({ theme, ownerState }) => ({
lineHeight: 1.4667,
padding: theme.spacing(2, 5.5),
minBlockSize: 38,
color: 'var(--mui-palette-text-primary)',
'& > .MuiTab-iconWrapper': {
fontSize: '1.125rem',
...(ownerState.iconPosition === 'start' && {
marginInlineEnd: theme.spacing(1.5)
}),
...(ownerState.iconPosition === 'end' && {
marginInlineStart: theme.spacing(1.5)
})
}
})
}
},
MuiTabPanel: {
styleOverrides: {
root: {
padding: 0
}
}
}
}
export default tabs
@@ -0,0 +1,161 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const timeline: Theme['components'] = {
MuiTimeline: {
styleOverrides: {
root: {
padding: 0
}
}
},
MuiTimelineDot: {
styleOverrides: {
root: ({ theme }) => ({
margin: theme.spacing(3, 0),
boxShadow: 'none',
'&:has(> i), &:has(> svg)': {
padding: 6
},
'& > svg, & > i': {
fontSize: '1.25rem'
},
'&:has(svg)': {
width: 32,
height: 32,
alignItems: 'center',
justifyContent: 'center'
}
})
},
variants: [
{
props: { variant: 'outlined' },
style: {
padding: 5,
'& + .MuiTimelineConnector-root': {
backgroundColor: 'transparent',
borderInlineStart: '1px dashed var(--mui-palette-divider)'
},
'&:has(+ .MuiTimelineConnector-root)': {
marginBlock: '0.625rem'
}
}
},
{
props: { variant: 'filled', color: 'grey' },
style: {
boxShadow: '0 0 0 3px rgb(var(--mui-mainColorChannels-light) / 0.04)'
}
},
{
props: { variant: 'filled', color: 'primary' },
style: {
boxShadow: '0 0 0 3px var(--mui-palette-primary-lightOpacity)'
}
},
{
props: { variant: 'filled', color: 'secondary' },
style: {
boxShadow: '0 0 0 3px var(--mui-palette-secondary-lightOpacity)'
}
},
{
props: { variant: 'filled', color: 'error' },
style: {
boxShadow: '0 0 0 3px var(--mui-palette-error-lightOpacity)'
}
},
{
props: { variant: 'filled', color: 'warning' },
style: {
boxShadow: '0 0 0 3px var(--mui-palette-warning-lightOpacity)'
}
},
{
props: { variant: 'filled', color: 'info' },
style: {
boxShadow: '0 0 0 3px var(--mui-palette-info-lightOpacity)'
}
},
{
props: { variant: 'filled', color: 'success' },
style: {
boxShadow: '0 0 0 3px var(--mui-palette-success-lightOpacity)'
}
},
{
props: { variant: 'tonal' },
style: {
border: 0
}
},
{
props: { variant: 'tonal', color: 'grey' },
style: {
backgroundColor: 'var(--mui-palette-action-selected)',
color: 'var(--mui-palette-text-primary)'
}
},
{
props: { variant: 'tonal', color: 'primary' },
style: {
backgroundColor: 'var(--mui-palette-primary-lightOpacity)',
color: 'var(--mui-palette-primary-main)'
}
},
{
props: { variant: 'tonal', color: 'secondary' },
style: {
backgroundColor: 'var(--mui-palette-secondary-lightOpacity)',
color: 'var(--mui-palette-secondary-main)'
}
},
{
props: { variant: 'tonal', color: 'error' },
style: {
backgroundColor: 'var(--mui-palette-error-lightOpacity)',
color: 'var(--mui-palette-error-main)'
}
},
{
props: { variant: 'tonal', color: 'warning' },
style: {
backgroundColor: 'var(--mui-palette-warning-lightOpacity)',
color: 'var(--mui-palette-warning-main)'
}
},
{
props: { variant: 'tonal', color: 'info' },
style: {
backgroundColor: 'var(--mui-palette-info-lightOpacity)',
color: 'var(--mui-palette-info-main)'
}
},
{
props: { variant: 'tonal', color: 'success' },
style: {
backgroundColor: 'var(--mui-palette-success-lightOpacity)',
color: 'var(--mui-palette-success-main)'
}
}
]
},
MuiTimelineConnector: {
styleOverrides: {
root: {
width: 1,
backgroundColor: 'var(--mui-palette-divider)'
}
}
},
MuiTimelineContent: {
styleOverrides: {
root: {
paddingBottom: '1rem'
}
}
}
}
export default timeline
@@ -0,0 +1,34 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const toggleButton: Theme['components'] = {
MuiToggleButtonGroup: {
styleOverrides: {
root: ({ ownerState }) => ({
...(ownerState.size === 'small' && {
borderRadius: 'var(--mui-shape-customBorderRadius-sm)'
}),
...(ownerState.size === 'large' && {
borderRadius: 'var(--mui-shape-customBorderRadius-lg)'
})
})
}
},
MuiToggleButton: {
styleOverrides: {
root: {
'&:not(.Mui-selected):not(.Mui-disabled)': {
color: 'var(--mui-palette-text-secondary)'
}
},
sizeSmall: {
borderRadius: 'var(--mui-shape-customBorderRadius-sm)'
},
sizeLarge: {
borderRadius: 'var(--mui-shape-customBorderRadius-lg)'
}
}
}
}
export default toggleButton
@@ -0,0 +1,32 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const tooltip: Theme['components'] = {
MuiTooltip: {
styleOverrides: {
popper: {
'&[data-popper-placement*="bottom"] .MuiTooltip-tooltip': {
marginTop: '6px !important'
},
'&[data-popper-placement*="top"] .MuiTooltip-tooltip': {
marginBottom: '6px !important'
},
'&[data-popper-placement*="left"] .MuiTooltip-tooltip': {
marginRight: '6px !important'
},
'&[data-popper-placement*="right"] .MuiTooltip-tooltip': {
marginLeft: '6px !important'
}
},
tooltip: ({ theme }) => ({
borderRadius: 'var(--mui-shape-customBorderRadius-sm)',
fontSize: theme.typography.subtitle2.fontSize,
lineHeight: 1.539,
color: 'var(--mui-palette-customColors-tooltipText)',
paddingInline: theme.spacing(3)
})
}
}
}
export default tooltip
@@ -0,0 +1,68 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const typography: Theme['components'] = {
MuiTypography: {
styleOverrides: {
gutterBottom: ({ theme }) => ({
marginBottom: theme.spacing(2)
})
},
variants: [
{
props: { variant: 'h1' },
style: { color: 'var(--mui-palette-text-primary)' }
},
{
props: { variant: 'h2' },
style: { color: 'var(--mui-palette-text-primary)' }
},
{
props: { variant: 'h3' },
style: { color: 'var(--mui-palette-text-primary)' }
},
{
props: { variant: 'h4' },
style: { color: 'var(--mui-palette-text-primary)' }
},
{
props: { variant: 'h5' },
style: { color: 'var(--mui-palette-text-primary)' }
},
{
props: { variant: 'h6' },
style: { color: 'var(--mui-palette-text-primary)' }
},
{
props: { variant: 'subtitle1' },
style: { color: 'rgb(var(--mui-palette-text-primaryChannel) / 0.55)' }
},
{
props: { variant: 'subtitle2' },
style: { color: 'rgb(var(--mui-palette-text-primaryChannel) / 0.55)' }
},
{
props: { variant: 'body1' },
style: { color: 'var(--mui-palette-text-secondary)' }
},
{
props: { variant: 'body2' },
style: { color: 'var(--mui-palette-text-secondary)' }
},
{
props: { variant: 'button' },
style: { color: 'var(--mui-palette-text-primary)' }
},
{
props: { variant: 'caption' },
style: { color: 'var(--mui-palette-text-disabled)', display: 'inline-block' }
},
{
props: { variant: 'overline' },
style: { color: 'var(--mui-palette-text-primary)', display: 'inline-block' }
}
]
}
}
export default typography
+39
View File
@@ -0,0 +1,39 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
// Type Imports
import type { SystemMode } from '@core/types'
const shadows = (mode: SystemMode): Theme['shadows'] => {
const color = `var(--mui-mainColorChannels-${mode}Shadow)`
return [
'none',
`0px 2px 1px -1px rgb(${color} / 0.2),0px 1px 1px 0px rgb(${color} / 0.14),0px 1px 3px 0px rgb(${color} / 0.12)`,
`0px 3px 1px -2px rgb(${color} / 0.2),0px 2px 2px 0px rgb(${color} / 0.14),0px 1px 5px 0px rgb(${color} / 0.12)`,
`0px 3px 3px -2px rgb(${color} / 0.2),0px 3px 4px 0px rgb(${color} / 0.14),0px 1px 8px 0px rgb(${color} / 0.12)`,
`0px 2px 4px -1px rgb(${color} / 0.2),0px 4px 5px 0px rgb(${color} / 0.14),0px 1px 10px 0px rgb(${color} / 0.12)`,
`0px 3px 5px -1px rgb(${color} / 0.2),0px 5px 8px 0px rgb(${color} / 0.14),0px 1px 14px 0px rgb(${color} / 0.12)`,
`0px 3px 5px -1px rgb(${color} / 0.2),0px 6px 10px 0px rgb(${color} / 0.14),0px 1px 18px 0px rgb(${color} / 0.12)`,
`0px 4px 5px -2px rgb(${color} / 0.2),0px 7px 10px 1px rgb(${color} / 0.14),0px 2px 16px 1px rgb(${color} / 0.12)`,
`0px 5px 5px -3px rgb(${color} / 0.2),0px 8px 10px 1px rgb(${color} / 0.14),0px 3px 14px 2px rgb(${color} / 0.12)`,
`0px 5px 6px -3px rgb(${color} / 0.2),0px 9px 12px 1px rgb(${color} / 0.14),0px 3px 16px 2px rgb(${color} / 0.12)`,
`0px 6px 6px -3px rgb(${color} / 0.2),0px 10px 14px 1px rgb(${color} / 0.14),0px 4px 18px 3px rgb(${color} / 0.12)`,
`0px 6px 7px -4px rgb(${color} / 0.2),0px 11px 15px 1px rgb(${color} / 0.14),0px 4px 20px 3px rgb(${color} / 0.12)`,
`0px 7px 8px -4px rgb(${color} / 0.2),0px 12px 17px 2px rgb(${color} / 0.14),0px 5px 22px 4px rgb(${color} / 0.12)`,
`0px 7px 8px -4px rgb(${color} / 0.2),0px 13px 19px 2px rgb(${color} / 0.14),0px 5px 24px 4px rgb(${color} / 0.12)`,
`0px 7px 9px -4px rgb(${color} / 0.2),0px 14px 21px 2px rgb(${color} / 0.14),0px 5px 26px 4px rgb(${color} / 0.12)`,
`0px 8px 9px -5px rgb(${color} / 0.2),0px 15px 22px 2px rgb(${color} / 0.14),0px 6px 28px 5px rgb(${color} / 0.12)`,
`0px 8px 10px -5px rgb(${color} / 0.2),0px 16px 24px 2px rgb(${color} / 0.14),0px 6px 30px 5px rgb(${color} / 0.12)`,
`0px 8px 11px -5px rgb(${color} / 0.2),0px 17px 26px 2px rgb(${color} / 0.14),0px 6px 32px 5px rgb(${color} / 0.12)`,
`0px 9px 11px -5px rgb(${color} / 0.2),0px 18px 28px 2px rgb(${color} / 0.14),0px 7px 34px 6px rgb(${color} / 0.12)`,
`0px 9px 12px -6px rgb(${color} / 0.2),0px 19px 29px 2px rgb(${color} / 0.14),0px 7px 36px 6px rgb(${color} / 0.12)`,
`0px 10px 13px -6px rgb(${color} / 0.2),0px 20px 31px 3px rgb(${color} / 0.14),0px 8px 38px 7px rgb(${color} / 0.12)`,
`0px 10px 13px -6px rgb(${color} / 0.2),0px 21px 33px 3px rgb(${color} / 0.14),0px 8px 40px 7px rgb(${color} / 0.12)`,
`0px 10px 14px -6px rgb(${color} / 0.2),0px 22px 35px 3px rgb(${color} / 0.14),0px 8px 42px 7px rgb(${color} / 0.12)`,
`0px 11px 14px -7px rgb(${color} / 0.2),0px 23px 36px 3px rgb(${color} / 0.14),0px 9px 44px 8px rgb(${color} / 0.12)`,
`0px 11px 15px -7px rgb(${color} / 0.2),0px 24px 38px 3px rgb(${color} / 0.14),0px 9px 46px 8px rgb(${color} / 0.12)`
]
}
export default shadows
+5
View File
@@ -0,0 +1,5 @@
const spacing = {
spacing: (factor: number) => `${0.25 * factor}rem`
}
export default spacing
+88
View File
@@ -0,0 +1,88 @@
// MUI Imports
import type { Theme } from '@mui/material/styles'
const typography = (fontFamily: string): Theme['typography'] =>
({
fontFamily:
typeof fontFamily === 'undefined' || fontFamily === ''
? [
'Inter',
'sans-serif',
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"'
].join(',')
: fontFamily,
fontSize: 13.125,
h1: {
fontSize: '2.875rem',
fontWeight: 500,
lineHeight: 1.478261
},
h2: {
fontSize: '2.375rem',
fontWeight: 500,
lineHeight: 1.47368421
},
h3: {
fontSize: '1.75rem',
fontWeight: 500,
lineHeight: 1.5
},
h4: {
fontSize: '1.5rem',
fontWeight: 500,
lineHeight: 1.58334
},
h5: {
fontSize: '1.125rem',
fontWeight: 500,
lineHeight: 1.5556
},
h6: {
fontSize: '0.9375rem',
fontWeight: 500,
lineHeight: 1.46667
},
subtitle1: {
fontSize: '0.9375rem',
lineHeight: 1.46667
},
subtitle2: {
fontSize: '0.8125rem',
fontWeight: 400,
lineHeight: 1.53846154
},
body1: {
fontSize: '0.9375rem',
lineHeight: 1.46667
},
body2: {
fontSize: '0.8125rem',
lineHeight: 1.53846154
},
button: {
fontSize: '0.9375rem',
lineHeight: 1.46667,
textTransform: 'none'
},
caption: {
fontSize: '0.8125rem',
lineHeight: 1.38462,
letterSpacing: '0.4px'
},
overline: {
fontSize: '0.75rem',
lineHeight: 1.16667,
letterSpacing: '0.8px'
}
}) as Theme['typography']
export default typography
+16
View File
@@ -0,0 +1,16 @@
// React Imports
import type { ReactNode } from 'react'
export type Skin = 'default' | 'bordered'
export type Mode = 'light' | 'dark'
export type SystemMode = 'light' | 'dark'
export type Direction = 'ltr' | 'rtl'
export type ChildrenType = {
children: ReactNode
}
export type ThemeColor = 'primary' | 'secondary' | 'error' | 'warning' | 'info' | 'success'
@@ -0,0 +1,18 @@
export type SystemConfig = {
id: string;
sessionCleanupIntervalMinutes: number;
invoiceGenerationDayOfMonth: number;
tokenExpirationMinutes: number;
tokenCleanupIntervalMinutes: number;
profilePictureUpdateIntervalDays: number;
createdAt: string;
updatedAt: string;
};
export type UpdateSystemConfigDto = {
sessionCleanupIntervalMinutes?: number;
invoiceGenerationDayOfMonth?: number;
tokenExpirationMinutes?: number;
tokenCleanupIntervalMinutes?: number;
profilePictureUpdateIntervalDays?: number;
};
@@ -0,0 +1,40 @@
import 'server-only'
// Next Imports
import { cookies } from 'next/headers'
// Type Imports
import type { Settings } from '@core/contexts/settingsContext'
import type { SystemMode } from '@core/types'
// Config Imports
import themeConfig from '@configs/themeConfig'
export const getSettingsFromCookie = (): Settings => {
const cookieStore = cookies()
const cookieName = themeConfig.settingsCookieName
return JSON.parse(cookieStore.get(cookieName)?.value || '{}')
}
export const getMode = () => {
const settingsCookie = getSettingsFromCookie()
// Get mode from cookie or fallback to theme config
const _mode = settingsCookie.mode || themeConfig.mode
return _mode
}
export const getSystemMode = (): SystemMode => {
const mode = getMode()
return mode
}
export const getServerMode = () => {
const mode = getMode()
return mode
}
+16
View File
@@ -0,0 +1,16 @@
'use client'
// Third-party Imports
import classnames from 'classnames'
// Type Imports
import type { ChildrenType } from '@core/types'
// Util Imports
import { blankLayoutClasses } from './utils/layoutClasses'
const BlankLayout = ({ children }: ChildrenType) => {
return <div className={classnames(blankLayoutClasses.root, 'is-full bs-full')}>{children}</div>
}
export default BlankLayout
@@ -0,0 +1,11 @@
'use client'
// React Imports
import type { ReactElement } from 'react'
const LayoutWrapper = ({ verticalLayout }: { verticalLayout: ReactElement }) => {
// Return the layout based on the layout context
return <div className='flex flex-col flex-auto'>{verticalLayout}</div>
}
export default LayoutWrapper
@@ -0,0 +1,39 @@
// React Imports
import type { ReactNode } from 'react'
// Third-party Imports
import classnames from 'classnames'
// Type Imports
import type { ChildrenType } from '@core/types'
// Component Imports
import LayoutContent from './components/vertical/LayoutContent'
// Util Imports
import { verticalLayoutClasses } from './utils/layoutClasses'
type VerticalLayoutProps = ChildrenType & {
navigation?: ReactNode
navbar?: ReactNode
footer?: ReactNode
}
const VerticalLayout = (props: VerticalLayoutProps) => {
// Props
const { navbar, footer, navigation, children } = props
return (
<div className={classnames(verticalLayoutClasses.root, 'flex flex-auto')}>
{navigation || null}
<div className={classnames(verticalLayoutClasses.contentWrapper, 'flex flex-col min-is-0 is-full')}>
{navbar || null}
{/* Content */}
<LayoutContent>{children}</LayoutContent>
{footer || null}
</div>
</div>
)
}
export default VerticalLayout
@@ -0,0 +1,40 @@
'use client'
// Third-party Imports
import classnames from 'classnames'
import type { CSSObject } from '@emotion/styled'
// Type Imports
import type { ChildrenType } from '@core/types'
// Util Imports
import { verticalLayoutClasses } from '@layouts/utils/layoutClasses'
// Styled Component Imports
import StyledFooter from '@layouts/styles/vertical/StyledFooter'
type Props = ChildrenType & {
overrideStyles?: CSSObject
}
const Footer = (props: Props) => {
// Props
const { children, overrideStyles } = props
return (
<StyledFooter
overrideStyles={overrideStyles}
className={classnames(
verticalLayoutClasses.footer,
verticalLayoutClasses.footerContentCompact,
verticalLayoutClasses.footerStatic,
verticalLayoutClasses.footerDetached,
'is-full'
)}
>
<div className={verticalLayoutClasses.footerContentWrapper}>{children}</div>
</StyledFooter>
)
}
export default Footer
@@ -0,0 +1,26 @@
'use client'
// Third-party Imports
import classnames from 'classnames'
// Type Imports
import type { ChildrenType } from '@core/types'
// Util Imports
import { verticalLayoutClasses } from '@layouts/utils/layoutClasses'
// Styled Component Imports
import StyledMain from '@layouts/styles/shared/StyledMain'
const LayoutContent = ({ children }: ChildrenType) => {
return (
<StyledMain
isContentCompact={true}
className={classnames(verticalLayoutClasses.content, verticalLayoutClasses.contentCompact, 'flex-auto is-full')}
>
{children}
</StyledMain>
)
}
export default LayoutContent
@@ -0,0 +1,39 @@
'use client'
// Third-party Imports
import classnames from 'classnames'
import type { CSSObject } from '@emotion/styled'
// Type Imports
import type { ChildrenType } from '@core/types'
// Util Imports
import { verticalLayoutClasses } from '@layouts/utils/layoutClasses'
// Styled Component Imports
import StyledHeader from '@layouts/styles/vertical/StyledHeader'
type Props = ChildrenType & {
overrideStyles?: CSSObject
}
const Navbar = (props: Props) => {
// Props
const { children, overrideStyles } = props
return (
<StyledHeader
overrideStyles={overrideStyles}
className={classnames(
verticalLayoutClasses.header,
verticalLayoutClasses.headerContentCompact,
verticalLayoutClasses.headerStatic,
verticalLayoutClasses.headerDetached
)}
>
<div className={classnames(verticalLayoutClasses.navbar, 'flex bs-full')}>{children}</div>
</StyledHeader>
)
}
export default Navbar
@@ -0,0 +1,21 @@
// Third-party Imports
import styled from '@emotion/styled'
// Config Imports
import themeConfig from '@configs/themeConfig'
type StyledMainProps = {
isContentCompact: boolean
}
const StyledMain = styled.main<StyledMainProps>`
padding: ${themeConfig.layoutPadding}px;
${({ isContentCompact }) =>
isContentCompact &&
`
margin-inline: auto;
max-inline-size: ${themeConfig.compactContentWidth}px;
`}
`
export default StyledMain
@@ -0,0 +1,27 @@
// Third-party Imports
import styled from '@emotion/styled'
import type { CSSObject } from '@emotion/styled'
// Config Imports
import themeConfig from '@configs/themeConfig'
// Util Imports
import { verticalLayoutClasses } from '@layouts/utils/layoutClasses'
type StyledFooterProps = {
overrideStyles?: CSSObject
}
const StyledFooter = styled.footer<StyledFooterProps>`
margin-inline: auto;
max-inline-size: ${themeConfig.compactContentWidth}px;
& .${verticalLayoutClasses.footerContentWrapper} {
padding-block: 15px;
padding-inline: ${themeConfig.layoutPadding}px;
}
${({ overrideStyles }) => overrideStyles}
`
export default StyledFooter
@@ -0,0 +1,35 @@
// Third-party Imports
import styled from '@emotion/styled'
import type { CSSObject } from '@emotion/styled'
// Config Imports
import themeConfig from '@configs/themeConfig'
// Util Imports
import { verticalLayoutClasses } from '@layouts/utils/layoutClasses'
type StyledHeaderProps = {
overrideStyles?: CSSObject
}
const StyledHeader = styled.header<StyledHeaderProps>`
display: flex;
align-items: center;
justify-content: center;
inline-size: 100%;
flex-shrink: 0;
min-block-size: var(--header-height);
.${verticalLayoutClasses.navbar} {
position: relative;
padding-block: 10px;
padding-inline: ${themeConfig.layoutPadding}px;
inline-size: 100%;
margin-inline: auto;
max-inline-size: ${themeConfig.compactContentWidth}px;
}
${({ overrideStyles }) => overrideStyles}
`
export default StyledHeader
@@ -0,0 +1,24 @@
// Classes for vertical layout
export const verticalLayoutClasses = {
root: 'ts-vertical-layout',
contentWrapper: 'ts-vertical-layout-content-wrapper',
header: 'ts-vertical-layout-header',
headerStatic: 'ts-vertical-layout-header-static',
headerDetached: 'ts-vertical-layout-header-detached',
headerContentCompact: 'ts-vertical-layout-header-content-compact',
navbar: 'ts-vertical-layout-navbar',
navbarContent: 'ts-vertical-layout-navbar-content',
content: 'ts-vertical-layout-content',
contentCompact: 'ts-vertical-layout-content-compact',
footer: 'ts-vertical-layout-footer',
footerStatic: 'ts-vertical-layout-footer-static',
footerDetached: 'ts-vertical-layout-footer-detached',
footerContentWrapper: 'ts-vertical-layout-footer-content-wrapper',
footerContent: 'ts-vertical-layout-footer-content',
footerContentCompact: 'ts-vertical-layout-footer-content-compact'
}
// Classes for blank layout
export const blankLayoutClasses = {
root: 'ts-blank-layout'
}
@@ -0,0 +1,27 @@
'use client'
// React Imports
import { forwardRef } from 'react'
// Next Imports
import Link from 'next/link'
import type { LinkProps } from 'next/link'
// Type Imports
import type { ChildrenType } from '../types'
type RouterLinkProps = LinkProps &
Partial<ChildrenType> & {
className?: string
}
export const RouterLink = forwardRef((props: RouterLinkProps, ref: any) => {
// Props
const { href, className, ...other } = props
return (
<Link ref={ref} href={href.toString() || '/'} className={className} {...other}>
{props.children}
</Link>
)
})
@@ -0,0 +1,193 @@
'use client'
// React Imports
import { createContext, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { ForwardRefRenderFunction, MenuHTMLAttributes, MutableRefObject, ReactElement, ReactNode } from 'react'
// Next Imports
import { usePathname } from 'next/navigation'
// Third-party Imports
import classnames from 'classnames'
import type { CSSObject } from '@emotion/styled'
// Type Imports
import type {
ChildrenType,
MenuItemStyles,
RootStylesType,
RenderExpandIconParams,
RenderExpandedMenuItemIcon
} from '../../types'
// Util Imports
import { menuClasses } from '../../utils/menuClasses'
// Styled Component Imports
import StyledVerticalMenu from '../../styles/vertical/StyledVerticalMenu'
// Style Imports
import styles from '../../styles/styles.module.css'
// Default Config Imports
import { verticalSubMenuToggleDuration } from '../../defaultConfigs'
export type MenuSectionStyles = {
root?: CSSObject
label?: CSSObject
prefix?: CSSObject
suffix?: CSSObject
icon?: CSSObject
}
export type OpenSubmenu = {
level: number
label: ReactNode
active: boolean
id: string
}
export type VerticalMenuContextProps = {
transitionDuration?: number
menuSectionStyles?: MenuSectionStyles
menuItemStyles?: MenuItemStyles
subMenuOpenBehavior?: 'accordion' | 'collapse'
renderExpandIcon?: (params: RenderExpandIconParams) => ReactElement
renderExpandedMenuItemIcon?: RenderExpandedMenuItemIcon
textTruncate?: boolean
/**
* @ignore
*/
openSubmenu?: OpenSubmenu[]
/**
* @ignore
*/
openSubmenusRef?: MutableRefObject<OpenSubmenu[]>
/**
* @ignore
*/
toggleOpenSubmenu?: (...submenus: { level: number; label: ReactNode; active?: boolean; id: string }[]) => void
}
export type MenuProps = VerticalMenuContextProps &
RootStylesType &
Partial<ChildrenType> &
MenuHTMLAttributes<HTMLMenuElement>
export const VerticalMenuContext = createContext({} as VerticalMenuContextProps)
const Menu: ForwardRefRenderFunction<HTMLMenuElement, MenuProps> = (props, ref) => {
// Props
const {
children,
className,
rootStyles,
menuItemStyles,
renderExpandIcon,
renderExpandedMenuItemIcon,
menuSectionStyles,
subMenuOpenBehavior = 'accordion', // accordion, collapse
transitionDuration = verticalSubMenuToggleDuration,
textTruncate = true,
...rest
} = props
// States
const [openSubmenu, setOpenSubmenu] = useState<OpenSubmenu[]>([])
// Refs
const openSubmenusRef = useRef<OpenSubmenu[]>([])
// Hooks
const pathname = usePathname()
const toggleOpenSubmenu = useCallback(
(...submenus: { level: number; label: ReactNode; active?: boolean; id: string }[]): void => {
if (!submenus.length) return
const openSubmenuCopy = [...openSubmenu]
submenus.forEach(({ level, label, active = false, id }) => {
const submenuIndex = openSubmenuCopy.findIndex(submenu => submenu.id === id)
const submenuExists = submenuIndex >= 0
const isAccordion = subMenuOpenBehavior === 'accordion'
const inactiveSubmenuIndex = openSubmenuCopy.findIndex(submenu => !submenu.active && submenu.level === 0)
// Delete submenu if it exists
if (submenuExists) {
openSubmenuCopy.splice(submenuIndex, 1)
}
if (isAccordion) {
// Add submenu if it doesn't exist
if (!submenuExists) {
if (inactiveSubmenuIndex >= 0 && !active && level === 0) {
openSubmenuCopy.splice(inactiveSubmenuIndex, 1, { level, label, active, id })
} else {
openSubmenuCopy.push({ level, label, active, id })
}
}
} else {
// Add submenu if it doesn't exist
if (!submenuExists) {
openSubmenuCopy.push({ level, label, active, id })
}
}
})
setOpenSubmenu(openSubmenuCopy)
},
[openSubmenu, subMenuOpenBehavior]
)
useEffect(() => {
setOpenSubmenu([...openSubmenusRef.current])
openSubmenusRef.current = []
}, [pathname])
const providerValue = useMemo(
() => ({
transitionDuration,
menuItemStyles,
menuSectionStyles,
renderExpandIcon,
renderExpandedMenuItemIcon,
openSubmenu,
openSubmenusRef,
toggleOpenSubmenu,
subMenuOpenBehavior,
textTruncate
}),
[
transitionDuration,
menuItemStyles,
menuSectionStyles,
renderExpandIcon,
renderExpandedMenuItemIcon,
openSubmenu,
openSubmenusRef,
toggleOpenSubmenu,
subMenuOpenBehavior,
textTruncate
]
)
return (
<VerticalMenuContext.Provider value={providerValue}>
<StyledVerticalMenu
ref={ref}
className={classnames(menuClasses.root, className)}
rootStyles={rootStyles}
{...rest}
>
<ul className={styles.ul}>{children}</ul>
</StyledVerticalMenu>
</VerticalMenuContext.Provider>
)
}
export default forwardRef(Menu)
@@ -0,0 +1,76 @@
// React Imports
import { forwardRef } from 'react'
import type { ForwardRefRenderFunction } from 'react'
// Third-party Imports
import { css } from '@emotion/react'
// Type Imports
import type { ChildrenType, MenuButtonProps } from '../../types'
// Component Imports
import { RouterLink } from '../RouterLink'
// Util Imports
import { menuClasses } from '../../utils/menuClasses'
type MenuButtonStylesProps = Partial<ChildrenType> & {
level: number
active?: boolean
disabled?: boolean
}
export const menuButtonStyles = (props: MenuButtonStylesProps) => {
// Props
const { level, disabled, children } = props
return css({
display: 'flex',
alignItems: 'center',
minBlockSize: '30px',
textDecoration: 'none',
color: 'inherit',
boxSizing: 'border-box',
cursor: 'pointer',
paddingInlineEnd: '20px',
paddingInlineStart: `${level === 0 ? 20 : (level + 1) * 20}px`,
'&:hover, &[aria-expanded="true"]': {
backgroundColor: '#f3f3f3'
},
'&:focus-visible': {
outline: 'none',
backgroundColor: '#f3f3f3'
},
...(disabled && {
pointerEvents: 'none',
cursor: 'default',
color: '#adadad'
}),
// All the active styles are applied to the button including menu items or submenu
[`&.${menuClasses.active}`]: {
...(!children && { color: 'white' }),
backgroundColor: children ? '#f3f3f3' : '#765feb'
}
})
}
const MenuButton: ForwardRefRenderFunction<HTMLAnchorElement, MenuButtonProps> = (
{ className, children, ...rest },
ref
) => {
return rest.href ? (
<RouterLink ref={ref} className={className} href={rest.href} {...rest}>
{children}
</RouterLink>
) : (
<a ref={ref} className={className} {...rest}>
{children}
</a>
)
}
export default forwardRef(MenuButton)
@@ -0,0 +1,186 @@
'use client'
// React Imports
import { forwardRef, useEffect, useState } from 'react'
import type { AnchorHTMLAttributes, ForwardRefRenderFunction, ReactElement, ReactNode } from 'react'
// Next Imports
import { usePathname } from 'next/navigation'
// Third-party Imports
import classnames from 'classnames'
import { useUpdateEffect } from 'react-use'
import type { CSSObject } from '@emotion/styled'
// Type Imports
import type { ChildrenType, MenuItemElement, MenuItemExactMatchUrlProps, RootStylesType } from '../../types'
// Component Imports
import MenuButton from './MenuButton'
// Hook Imports
import useVerticalNav from '../../hooks/useVerticalNav'
import useVerticalMenu from '../../hooks/useVerticalMenu'
// Util Imports
import { renderMenuIcon } from '../../utils/menuUtils'
import { menuClasses } from '../../utils/menuClasses'
// Styled Component Imports
import StyledMenuLabel from '../../styles/StyledMenuLabel'
import StyledMenuPrefix from '../../styles/StyledMenuPrefix'
import StyledMenuSuffix from '../../styles/StyledMenuSuffix'
import StyledVerticalMenuItem from '../../styles/vertical/StyledVerticalMenuItem'
export type MenuItemProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'prefix'> &
RootStylesType &
Partial<ChildrenType> &
MenuItemExactMatchUrlProps & {
icon?: ReactElement
prefix?: ReactNode
suffix?: ReactNode
disabled?: boolean
target?: string
rel?: string
onActiveChange?: (active: boolean) => void
/**
* @ignore
*/
level?: number
}
const MenuItem: ForwardRefRenderFunction<HTMLLIElement, MenuItemProps> = (props, ref) => {
// Props
const {
children,
icon,
className,
prefix,
suffix,
level = 0,
disabled = false,
exactMatch = true,
activeUrl,
onActiveChange,
rootStyles,
...rest
} = props
// States
const [active, setActive] = useState(false)
// Hooks
const pathname = usePathname()
const { menuItemStyles, renderExpandedMenuItemIcon, textTruncate } = useVerticalMenu()
const { toggleVerticalNav, isToggled, isBreakpointReached } = useVerticalNav()
// Get the styles for the specified element.
const getMenuItemStyles = (element: MenuItemElement): CSSObject | undefined => {
// If the menuItemStyles prop is provided, get the styles for the specified element.
if (menuItemStyles) {
// Define the parameters that are passed to the style functions.
const params = { level, disabled, active, isSubmenu: false }
// Get the style function for the specified element.
const styleFunction = menuItemStyles[element]
if (styleFunction) {
// If the style function is a function, call it and return the result.
// Otherwise, return the style function itself.
return typeof styleFunction === 'function' ? styleFunction(params) : styleFunction
}
}
}
// Handle the click event.
const handleClick = () => {
if (isToggled) {
toggleVerticalNav()
}
}
// Change active state when the url changes
useEffect(() => {
const href = rest.href
if (href) {
// Check if the current url matches any of the children urls
if (exactMatch ? pathname === href : activeUrl && pathname.includes(activeUrl)) {
setActive(true)
} else {
setActive(false)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname])
// Call the onActiveChange callback when the active state changes.
useUpdateEffect(() => {
onActiveChange?.(active)
}, [active])
return (
<StyledVerticalMenuItem
ref={ref}
className={classnames(
menuClasses.menuItemRoot,
{ [menuClasses.disabled]: disabled },
{ [menuClasses.active]: active },
className
)}
level={level}
disabled={disabled}
buttonStyles={getMenuItemStyles('button')}
menuItemStyles={getMenuItemStyles('root')}
rootStyles={rootStyles}
>
<MenuButton
className={classnames(menuClasses.button, { [menuClasses.active]: active })}
tabIndex={disabled ? -1 : 0}
{...rest}
onClick={e => {
handleClick()
rest.onClick && rest.onClick(e)
}}
>
{/* Menu Item Icon */}
{renderMenuIcon({
icon,
level,
active,
disabled,
renderExpandedMenuItemIcon,
styles: getMenuItemStyles('icon'),
isBreakpointReached
})}
{/* Menu Item Prefix */}
{prefix && (
<StyledMenuPrefix className={menuClasses.prefix} rootStyles={getMenuItemStyles('prefix')}>
{prefix}
</StyledMenuPrefix>
)}
{/* Menu Item Label */}
<StyledMenuLabel
className={menuClasses.label}
rootStyles={getMenuItemStyles('label')}
textTruncate={textTruncate}
>
{children}
</StyledMenuLabel>
{/* Menu Item Suffix */}
{suffix && (
<StyledMenuSuffix className={menuClasses.suffix} rootStyles={getMenuItemStyles('suffix')}>
{suffix}
</StyledMenuSuffix>
)}
</MenuButton>
</StyledVerticalMenuItem>
)
}
export default forwardRef(MenuItem)
@@ -0,0 +1,121 @@
'use client'
// React Imports
import { forwardRef } from 'react'
import type { ForwardRefRenderFunction, CSSProperties, ReactElement, ReactNode } from 'react'
// Third-party Imports
import classnames from 'classnames'
import type { CSSObject } from '@emotion/styled'
// Type Imports
import type { MenuSectionStyles } from './Menu'
import type { ChildrenType, RootStylesType } from '../../types'
// Hook Imports
import useVerticalMenu from '../../hooks/useVerticalMenu'
// Util Imports
import { menuClasses } from '../../utils/menuClasses'
// Styled Component Imports
import StyledMenuIcon from '../../styles/StyledMenuIcon'
import StyledMenuPrefix from '../../styles/StyledMenuPrefix'
import StyledMenuSuffix from '../../styles/StyledMenuSuffix'
import StyledMenuSectionLabel from '../../styles/StyledMenuSectionLabel'
import StyledVerticalMenuSection from '../../styles/vertical/StyledVerticalMenuSection'
export type MenuSectionProps = Partial<ChildrenType> &
RootStylesType & {
label: ReactNode
icon?: ReactElement
prefix?: ReactNode
suffix?: ReactNode
/**
* @ignore
*/
className?: string
}
type MenuSectionElement = keyof MenuSectionStyles
const menuSectionWrapperStyles: CSSProperties = {
display: 'inline-block',
inlineSize: '100%',
position: 'relative',
listStyle: 'none',
padding: 0,
overflow: 'hidden'
}
const menuSectionContentStyles: CSSProperties = {
display: 'flex',
alignItems: 'center',
inlineSize: '100%',
position: 'relative',
paddingBlock: '0.75rem',
paddingInline: '1.25rem',
overflow: 'hidden'
}
const MenuSection: ForwardRefRenderFunction<HTMLLIElement, MenuSectionProps> = (props, ref) => {
// Props
const { children, icon, className, prefix, suffix, label, rootStyles, ...rest } = props
// Hooks
const { menuSectionStyles, textTruncate } = useVerticalMenu()
const getMenuSectionStyles = (element: MenuSectionElement): CSSObject | undefined => {
// If the menuSectionStyles prop is provided, get the styles for the element from the prop
if (menuSectionStyles) {
return menuSectionStyles[element]
}
}
return (
// eslint-disable-next-line lines-around-comment
// Menu Section
<StyledVerticalMenuSection
ref={ref}
rootStyles={rootStyles}
menuSectionStyles={getMenuSectionStyles('root')}
className={classnames(menuClasses.menuSectionRoot, className)}
>
{/* Menu Section Content Wrapper */}
<ul className={menuClasses.menuSectionWrapper} {...rest} style={menuSectionWrapperStyles}>
{/* Menu Section Content */}
<li className={menuClasses.menuSectionContent} style={menuSectionContentStyles}>
{icon && (
<StyledMenuIcon className={menuClasses.icon} rootStyles={getMenuSectionStyles('icon')}>
{icon}
</StyledMenuIcon>
)}
{prefix && (
<StyledMenuPrefix className={menuClasses.prefix} rootStyles={getMenuSectionStyles('prefix')}>
{prefix}
</StyledMenuPrefix>
)}
{label && (
<StyledMenuSectionLabel
className={menuClasses.menuSectionLabel}
rootStyles={getMenuSectionStyles('label')}
textTruncate={textTruncate}
>
{label}
</StyledMenuSectionLabel>
)}
{suffix && (
<StyledMenuSuffix className={menuClasses.suffix} rootStyles={getMenuSectionStyles('suffix')}>
{suffix}
</StyledMenuSuffix>
)}
</li>
{/* Render Child */}
{children}
</ul>
</StyledVerticalMenuSection>
)
}
export default forwardRef<HTMLLIElement, MenuSectionProps>(MenuSection)
@@ -0,0 +1,22 @@
// Third-party Imports
import styled from '@emotion/styled'
// Type Imports
import type { ChildrenType } from '../../types'
// Util Imports
import { verticalNavClasses } from '../../utils/menuClasses'
const StyledNavHeader = styled.div`
padding: 15px;
padding-inline-start: 20px;
display: flex;
align-items: center;
justify-content: space-between;
`
const NavHeader = ({ children }: ChildrenType) => {
return <StyledNavHeader className={verticalNavClasses.header}>{children}</StyledNavHeader>
}
export default NavHeader
@@ -0,0 +1,332 @@
'use client'
// React Imports
import { Children, cloneElement, forwardRef, useEffect, useId, useRef, useState } from 'react'
import type {
AnchorHTMLAttributes,
ForwardRefRenderFunction,
KeyboardEvent,
MouseEvent,
ReactElement,
ReactNode
} from 'react'
// Next Imports
import { usePathname } from 'next/navigation'
// Third-party Imports
import classnames from 'classnames'
import styled from '@emotion/styled'
import type { CSSObject } from '@emotion/styled'
// Type Imports
import type { OpenSubmenu } from './Menu'
import type { MenuItemProps } from './MenuItem'
import type { ChildrenType, RootStylesType, SubMenuItemElement } from '../../types'
// Component Imports
import SubMenuContent from './SubMenuContent'
import MenuButton, { menuButtonStyles } from './MenuButton'
// Icon Imports
import ChevronRight from '../../svg/ChevronRight'
// Hook Imports
import useVerticalNav from '../../hooks/useVerticalNav'
import useVerticalMenu from '../../hooks/useVerticalMenu'
// Util Imports
import { menuClasses } from '../../utils/menuClasses'
import { confirmUrlInChildren, renderMenuIcon } from '../../utils/menuUtils'
// Styled Component Imports
import StyledMenuLabel from '../../styles/StyledMenuLabel'
import StyledMenuPrefix from '../../styles/StyledMenuPrefix'
import StyledMenuSuffix from '../../styles/StyledMenuSuffix'
import StyledVerticalNavExpandIcon, {
StyledVerticalNavExpandIconWrapper
} from '../../styles/vertical/StyledVerticalNavExpandIcon'
export type SubMenuProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'prefix'> &
RootStylesType &
Partial<ChildrenType> & {
label: ReactNode
icon?: ReactElement
prefix?: ReactNode
suffix?: ReactNode
defaultOpen?: boolean
disabled?: boolean
contentClassName?: string
onOpenChange?: (open: boolean) => void
/**
* @ignore
*/
level?: number
}
type StyledSubMenuProps = Pick<SubMenuProps, 'rootStyles' | 'disabled'> & {
level: number
active?: boolean
menuItemStyles?: CSSObject
buttonStyles?: CSSObject
}
const StyledSubMenu = styled.li<StyledSubMenuProps>`
position: relative;
inline-size: 100%;
margin-block-start: 4px;
&.${menuClasses.open} > .${menuClasses.button} {
background-color: #f3f3f3;
}
${({ menuItemStyles }) => menuItemStyles};
${({ rootStyles }) => rootStyles};
> .${menuClasses.button} {
${({ level, disabled, active, children }) =>
menuButtonStyles({
level,
active,
disabled,
children
})};
${({ buttonStyles }) => buttonStyles};
}
`
const SubMenu: ForwardRefRenderFunction<HTMLLIElement, SubMenuProps> = (props, ref) => {
// Props
const {
children,
className,
contentClassName,
label,
icon,
title,
prefix,
suffix,
defaultOpen,
level = 0,
disabled = false,
rootStyles,
onOpenChange,
onClick,
onKeyUp,
...rest
} = props
// States
const [active, setActive] = useState<boolean>(false)
// Refs
const contentRef = useRef<HTMLDivElement>(null)
// Hooks
const id = useId()
const pathname = usePathname()
const { isBreakpointReached } = useVerticalNav()
const {
renderExpandIcon,
renderExpandedMenuItemIcon,
menuItemStyles,
openSubmenu,
toggleOpenSubmenu,
transitionDuration,
openSubmenusRef,
textTruncate
} = useVerticalMenu()
// Vars
// Filter out falsy values from children
const childNodes = Children.toArray(children).filter(Boolean) as [ReactElement<SubMenuProps | MenuItemProps>]
const isSubMenuOpen = openSubmenu?.some((item: OpenSubmenu) => item.id === id) ?? false
const handleSlideToggle = (): void => {
toggleOpenSubmenu?.({ level, label, active, id })
onOpenChange?.(!isSubMenuOpen)
if (openSubmenusRef?.current && openSubmenusRef?.current.length > 0) openSubmenusRef.current = []
}
const handleOnClick = (event: MouseEvent<HTMLAnchorElement, globalThis.MouseEvent>) => {
onClick?.(event)
handleSlideToggle()
}
const handleOnKeyUp = (event: KeyboardEvent<HTMLAnchorElement>) => {
onKeyUp?.(event)
if (event.key === 'Enter') {
handleSlideToggle()
}
}
const getSubMenuItemStyles = (element: SubMenuItemElement): CSSObject | undefined => {
// If the menuItemStyles prop is provided, get the styles for the specified element.
if (menuItemStyles) {
// Define the parameters that are passed to the style functions.
const params = {
level,
disabled,
active,
isSubmenu: true,
open: isSubMenuOpen
}
// Get the style function for the specified element.
const styleFunction = menuItemStyles[element]
if (styleFunction) {
// If the style function is a function, call it and return the result.
// Otherwise, return the style function itself.
return typeof styleFunction === 'function' ? styleFunction(params) : styleFunction
}
}
}
useEffect(() => {
if (confirmUrlInChildren(children, pathname)) {
openSubmenusRef?.current.push({ level, label, active: true, id })
} else {
if (defaultOpen) {
openSubmenusRef?.current.push({ level, label, active: false, id })
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Change active state when the url changes
useEffect(() => {
// Check if the current url matches any of the children urls
if (confirmUrlInChildren(children, pathname)) {
setActive(true)
if (openSubmenusRef?.current.findIndex(submenu => submenu.id === id) === -1) {
openSubmenusRef?.current.push({ level, label, active: true, id })
}
} else {
setActive(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname])
/* useEffect(() => {
console.log(openSubmenu)
}, [openSubmenu]) */
const submenuContent = (
<SubMenuContent
ref={contentRef}
transitionDuration={transitionDuration}
open={isSubMenuOpen}
level={level}
className={classnames(menuClasses.subMenuContent, contentClassName)}
rootStyles={{
...getSubMenuItemStyles('subMenuContent')
}}
>
{childNodes.map(node =>
cloneElement(node, {
level: level + 1
})
)}
</SubMenuContent>
)
return (
// eslint-disable-next-line lines-around-comment
/* Sub Menu */
<StyledSubMenu
ref={ref}
className={classnames(
menuClasses.subMenuRoot,
{ [menuClasses.active]: active },
{ [menuClasses.disabled]: disabled },
{ [menuClasses.open]: isSubMenuOpen },
className
)}
menuItemStyles={getSubMenuItemStyles('root')}
level={level}
disabled={disabled}
active={active}
buttonStyles={getSubMenuItemStyles('button')}
rootStyles={rootStyles}
>
{/* Menu Item */}
<MenuButton
ref={null}
onClick={handleOnClick}
onKeyUp={handleOnKeyUp}
title={title}
className={classnames(menuClasses.button, { [menuClasses.active]: active })}
tabIndex={disabled ? -1 : 0}
{...rest}
>
{/* Sub Menu Icon */}
{renderMenuIcon({
icon,
level,
active,
disabled,
renderExpandedMenuItemIcon,
styles: getSubMenuItemStyles('icon'),
isBreakpointReached
})}
{/* Sub Menu Prefix */}
{prefix && (
<StyledMenuPrefix className={menuClasses.prefix} rootStyles={getSubMenuItemStyles('prefix')}>
{prefix}
</StyledMenuPrefix>
)}
{/* Sub Menu Label */}
<StyledMenuLabel
className={menuClasses.label}
rootStyles={getSubMenuItemStyles('label')}
textTruncate={textTruncate}
>
{label}
</StyledMenuLabel>
{/* Sub Menu Suffix */}
{suffix && (
<StyledMenuSuffix className={menuClasses.suffix} rootStyles={getSubMenuItemStyles('suffix')}>
{suffix}
</StyledMenuSuffix>
)}
{/* Sub Menu Toggle Icon Wrapper */}
{
<StyledVerticalNavExpandIconWrapper
className={menuClasses.subMenuExpandIcon}
rootStyles={getSubMenuItemStyles('subMenuExpandIcon')}
>
{renderExpandIcon ? (
renderExpandIcon({
level,
disabled,
active,
open: isSubMenuOpen
})
) : (
// eslint-disable-next-line lines-around-comment
/* Expanded Arrow Icon */
<StyledVerticalNavExpandIcon open={isSubMenuOpen} transitionDuration={transitionDuration}>
<ChevronRight fontSize='1rem' />
</StyledVerticalNavExpandIcon>
)}
</StyledVerticalNavExpandIconWrapper>
}
</MenuButton>
{/* Sub Menu Content */}
{submenuContent}
</StyledSubMenu>
)
}
export default forwardRef<HTMLLIElement, SubMenuProps>(SubMenu)
@@ -0,0 +1,84 @@
// React Imports
import { forwardRef, useEffect, useState } from 'react'
import type { ForwardRefRenderFunction, HTMLAttributes, MutableRefObject } from 'react'
// Type Imports
import type { VerticalMenuContextProps } from './Menu'
import type { ChildrenType, RootStylesType } from '../../types'
// Styled Component Imports
import StyledSubMenuContent from '../../styles/StyledSubMenuContent'
// Style Imports
import styles from '../../styles/styles.module.css'
export type SubMenuContentProps = HTMLAttributes<HTMLDivElement> &
RootStylesType &
Partial<ChildrenType> & {
open?: boolean
transitionDuration?: VerticalMenuContextProps['transitionDuration']
level?: number
}
const SubMenuContent: ForwardRefRenderFunction<HTMLDivElement, SubMenuContentProps> = (props, ref) => {
// Props
const { children, open, level, transitionDuration, ...rest } = props
// States
const [mounted, setMounted] = useState(false)
// Refs
const SubMenuContentRef = ref as MutableRefObject<HTMLDivElement>
useEffect(() => {
if (mounted) {
if (open) {
const target = SubMenuContentRef?.current
if (target) {
target.style.display = 'block'
target.style.overflow = 'hidden'
target.style.blockSize = 'auto'
const height = target.offsetHeight
target.style.blockSize = '0px'
target.offsetHeight
target.style.blockSize = `${height}px`
setTimeout(() => {
target.style.overflow = 'auto'
target.style.blockSize = 'auto'
}, transitionDuration)
}
} else {
const target = SubMenuContentRef?.current
if (target) {
target.style.overflow = 'hidden'
target.style.blockSize = `${target.offsetHeight}px`
target.offsetHeight
target.style.blockSize = '0px'
setTimeout(() => {
target.style.overflow = 'auto'
target.style.display = 'none'
}, transitionDuration)
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, mounted, SubMenuContentRef])
useEffect(() => {
setMounted(true)
}, [])
return (
<StyledSubMenuContent ref={ref} level={level} open={open} transitionDuration={transitionDuration} {...rest}>
<ul className={styles.ul}>{children}</ul>
</StyledSubMenuContent>
)
}
export default forwardRef(SubMenuContent)
@@ -0,0 +1,138 @@
'use client'
// React Imports
import { useEffect } from 'react'
import type { HTMLAttributes } from 'react'
// Third-party Imports
import classnames from 'classnames'
import type { CSSObject } from '@emotion/styled'
// Type Imports
import type { BreakpointType } from '../../types'
// Context Imports
import type { VerticalNavState } from '../../contexts/verticalNavContext'
// Hook Imports
import useMediaQuery from '../../hooks/useMediaQuery'
import useVerticalNav from '../../hooks/useVerticalNav'
// Util Imports
import { verticalNavClasses } from '../../utils/menuClasses'
// Styled Component Imports
import StyledBackdrop from '../../styles/StyledBackdrop'
import StyledVerticalNav from '../../styles/vertical/StyledVerticalNav'
import StyledVerticalNavContainer from '../../styles/vertical/StyledVerticalNavContainer'
import StyledVerticalNavBgColorContainer from '../../styles/vertical/StyledVerticalNavBgColorContainer'
// Default Config Imports
import { defaultBreakpoints, verticalNavToggleDuration } from '../../defaultConfigs'
export type VerticalNavProps = HTMLAttributes<HTMLHtmlElement> & {
width?: VerticalNavState['width']
breakpoint?: BreakpointType
customBreakpoint?: string
breakpoints?: Partial<typeof defaultBreakpoints>
transitionDuration?: VerticalNavState['transitionDuration']
backdropColor?: string
customStyles?: CSSObject
}
const VerticalNav = (props: VerticalNavProps) => {
// Props
const {
width = 260,
breakpoint = 'lg',
customBreakpoint,
breakpoints,
transitionDuration = verticalNavToggleDuration,
backdropColor,
className,
customStyles,
children,
...rest
} = props
// Vars
const mergedBreakpoints = { ...defaultBreakpoints, ...breakpoints }
// Hooks
const {
updateVerticalNavState,
width: widthContext,
isBreakpointReached: isBreakpointReachedContext,
isToggled: isToggledContext,
transitionDuration: transitionDurationContext
} = useVerticalNav()
// Find the breakpoint from which screen size responsive behavior should enable and if its reached or not
const breakpointReached = useMediaQuery(customBreakpoint ?? (breakpoint ? mergedBreakpoints[breakpoint] : breakpoint))
// UseEffect, update verticalNav state to set initial values and update values on change
useEffect(() => {
updateVerticalNavState({
width,
transitionDuration,
isBreakpointReached: breakpointReached
})
if (!breakpointReached) {
updateVerticalNavState({ isToggled: false })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [width, breakpointReached, updateVerticalNavState])
// Handle Backdrop(Content Overlay) Click
const handleBackdropClick = () => {
// Close the verticalNav
updateVerticalNavState({ isToggled: false })
}
return (
<StyledVerticalNav
width={width}
isBreakpointReached={isBreakpointReachedContext}
customStyles={customStyles}
transitionDuration={transitionDurationContext}
className={classnames(
verticalNavClasses.root,
{
[verticalNavClasses.toggled]: isToggledContext,
[verticalNavClasses.breakpointReached]: isBreakpointReachedContext
},
className
)}
{...rest}
>
<StyledVerticalNavContainer
width={widthContext}
className={verticalNavClasses.container}
transitionDuration={transitionDurationContext}
>
{/* VerticalNav Container to apply styling like background */}
<StyledVerticalNavBgColorContainer className={verticalNavClasses.bgColorContainer}>
{children}
</StyledVerticalNavBgColorContainer>
</StyledVerticalNavContainer>
{/* When verticalNav is toggled on smaller screen, show/hide verticalNav backdrop */}
{isToggledContext && breakpointReached && (
// eslint-disable-next-line lines-around-comment
/* VerticalNav Backdrop */
<StyledBackdrop
role='button'
tabIndex={0}
aria-label='backdrop'
onClick={handleBackdropClick}
onKeyPress={handleBackdropClick}
className={verticalNavClasses.backdrop}
backdropColor={backdropColor}
/>
)}
</StyledVerticalNav>
)
}
export default VerticalNav
@@ -0,0 +1,54 @@
'use client'
// React Imports
import { createContext, useCallback, useMemo, useState } from 'react'
// Type Imports
import type { ChildrenType } from '../types'
export type VerticalNavState = {
width?: number
isToggled?: boolean
isBreakpointReached?: boolean
transitionDuration?: number
}
export type VerticalNavContextProps = VerticalNavState & {
updateVerticalNavState: (values: VerticalNavState) => void
toggleVerticalNav: (value?: VerticalNavState['isToggled']) => void
}
const VerticalNavContext = createContext({} as VerticalNavContextProps)
export const VerticalNavProvider = ({ children }: ChildrenType) => {
// States
const [verticalNavState, setVerticalNavState] = useState<VerticalNavState>()
// Hooks
const updateVerticalNavState = useCallback((values: Partial<VerticalNavState>) => {
setVerticalNavState(prevState => ({
...prevState,
...values
}))
}, [])
const toggleVerticalNav = useCallback((value?: boolean) => {
setVerticalNavState(prevState => ({
...prevState,
isToggled: value !== undefined ? Boolean(value) : !Boolean(prevState?.isToggled)
}))
}, [])
const verticalNavProviderValue = useMemo(
() => ({
...verticalNavState,
updateVerticalNavState,
toggleVerticalNav
}),
[verticalNavState, updateVerticalNavState, toggleVerticalNav]
)
return <VerticalNavContext.Provider value={verticalNavProviderValue}>{children}</VerticalNavContext.Provider>
}
export default VerticalNavContext
+15
View File
@@ -0,0 +1,15 @@
// Type Imports
import type { BreakpointType } from './types'
export const defaultBreakpoints: Record<BreakpointType, string> = {
xs: '480px',
sm: '600px',
md: '900px',
lg: '1200px',
xl: '1536px',
xxl: '1920px',
always: 'always'
}
export const verticalNavToggleDuration = 300
export const verticalSubMenuToggleDuration = 300
@@ -0,0 +1,29 @@
'use client'
// React Imports
import { useEffect, useState } from 'react'
const useMediaQuery = (breakpoint?: string): boolean => {
// States
const [matches, setMatches] = useState(breakpoint === 'always')
useEffect(() => {
if (breakpoint && breakpoint !== 'always') {
const media = window.matchMedia(`(max-width: ${breakpoint})`)
if (media.matches !== matches) {
setMatches(media.matches)
}
const listener = () => setMatches(media.matches)
window.addEventListener('resize', listener)
return () => window.removeEventListener('resize', listener)
}
}, [matches, breakpoint])
return matches
}
export default useMediaQuery
@@ -0,0 +1,21 @@
// React Imports
import { useContext } from 'react'
// Type Imports
import type { VerticalMenuContextProps } from '../components/vertical-menu/Menu'
// Context Imports
import { VerticalMenuContext } from '../components/vertical-menu/Menu'
const useVerticalMenu = (): VerticalMenuContextProps => {
// Hooks
const context = useContext(VerticalMenuContext)
if (context === undefined) {
throw new Error('Menu Component is required!')
}
return context
}
export default useVerticalMenu
@@ -0,0 +1,18 @@
// React Imports
import { useContext } from 'react'
// Context Imports
import VerticalNavContext from '../contexts/verticalNavContext'
const useVerticalNav = () => {
// Hooks
const context = useContext(VerticalNavContext)
if (context === undefined) {
throw new Error('VerticalNav Component is required!')
}
return context
}
export default useVerticalNav

Some files were not shown because too many files have changed in this diff Show More