commit 044371a460ebbee1f88eafef348a40538fb42473 Author: Kim Daniel Engebretsen Date: Wed Apr 2 14:38:15 2025 +0200 test cicd diff --git a/.github/workflows/node-cicd.yml b/.github/workflows/node-cicd.yml new file mode 100644 index 0000000..307a9bc --- /dev/null +++ b/.github/workflows/node-cicd.yml @@ -0,0 +1,64 @@ +name: Release package to Gitea and Deploy to AWS +on: + push: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Check for change + id: diff + run: | + git diff --name-only origin/${{ github.ref_name }}~1 # This prints the list to screen only + echo "src=$(git diff --name-only origin/${{ github.ref_name }}~1 src| tr '\n' ' ')" >> $GITHUB_OUTPUT + + - uses: actions/setup-node@v4 + if: ${{ steps.diff.outputs.src }} + with: + node-version: '22' + registry-url: ${{ github.server_url != 'https://github.com' && format('{0}/api/packages/{1}/npm/', github.server_url, github.repository_owner) || 'https://npm.pkg.github.com' }} + scope: ${{ github.repository_owner }} + token: ${{ secrets.ACTIONS_TOKEN || github.token }} + + - run: npm ci + if: ${{ steps.diff.outputs.src }} + env: + NODE_AUTH_TOKEN: ${{ secrets.ACTIONS_TOKEN || github.token }} + + - run: npm run build + if: ${{ steps.diff.outputs.src }} + + - run: npm publish + if: ${{ steps.diff.outputs.src }} + env: + NODE_AUTH_TOKEN: ${{ secrets.ACTIONS_TOKEN || github.token}} + + - run: | + PACKAGE=$(npm pkg get name version|jq -r '"\(.name)@\(.version)"') + mkdir ../deployment && cd ../deployment && npm i $PACKAGE + env: + NODE_AUTH_TOKEN: ${{ secrets.ACTIONS_TOKEN || github.token }} + + - name: Config AWS creds + uses: fc-actions/aws-login@v0.0.15 + with: + #fireclover-client-id: ${{ vars.FIRECLOVER_CLIENT_ID }} + #fireclover-client-secret: ${{ secrets.FIRECLOVER_CLIENT_SECRET }} + aws-account-id: 515966519418 + + - name: Deploy Login to AWS + uses: fc-actions/deploy-cloudapp@v0.0.38 + with: + fireclover-subscription: 'my-fireclover-subscription-token' + aws-account-id: 515966519418 + dns-zone: 'test.aws.fireclover.cloud' + subdomain: 'test-example' + web-path: '../deployment/dist' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1170717 --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..268580a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM node:latest + +ENV HOME=/home/app + +RUN apt-get update && apt-get install htop -y + +COPY . $HOME/node_docker + +WORKDIR $HOME/node_docker + +RUN npm ci --silent --progress=false + +RUN npm run build + +CMD ["npm", "start"] diff --git a/index.html b/index.html new file mode 100644 index 0000000..286a3f4 --- /dev/null +++ b/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + Vite + Material UI + TS + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..c6ec3f1 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "@fireclover/example-react", + "private": true, + "version": "1.0.0", + "description": "Infrastructure for an HTTPS static site using S3 and CloudFront", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "echo test ok" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^6.4.5", + "@mui/material": "^6.4.5", + "@mui/x-charts": "^7.27.0", + "@mui/x-data-grid": "^7.27.0", + "@mui/x-data-grid-pro": "^7.27.0", + "@mui/x-date-pickers": "^7.27.0", + "@mui/x-date-pickers-pro": "^7.27.0", + "@mui/x-tree-view": "^7.26.0", + "dayjs": "^1.11.13", + "react": "latest", + "react-dom": "latest" + }, + "devDependencies": { + "@types/react": "latest", + "@types/react-dom": "latest", + "@types/node": "latest", + "@vitejs/plugin-react": "latest", + "typescript": "latest", + "vite": "latest" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..80c7cf2 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import ProTip from './ProTip'; + +function Copyright() { + return ( + + {'Copyright © '} + + Your Website + {' '} + {new Date().getFullYear()}. + + ); +} + +export default function App() { + return ( + + + + Material UI Vite.js example in TypeScript + + + + + + ); +} diff --git a/src/ProTip.tsx b/src/ProTip.tsx new file mode 100644 index 0000000..217b5bf --- /dev/null +++ b/src/ProTip.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import Link from '@mui/material/Link'; +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; +import Typography from '@mui/material/Typography'; + +function LightBulbIcon(props: SvgIconProps) { + return ( + + + + ); +} + +export default function ProTip() { + return ( + + + {'Pro tip: See more '} + templates + {' in the Material UI documentation.'} + + ); +} diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx new file mode 100644 index 0000000..d1e3a7a --- /dev/null +++ b/src/dashboard/Dashboard.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import type {} from '@mui/x-date-pickers/themeAugmentation'; +import type {} from '@mui/x-charts/themeAugmentation'; +import type {} from '@mui/x-data-grid-pro/themeAugmentation'; +import type {} from '@mui/x-tree-view/themeAugmentation'; +import { alpha } from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import AppNavbar from './components/AppNavbar'; +import Header from './components/Header'; +import MainGrid from './components/MainGrid'; +import SideMenu from './components/SideMenu'; +import AppTheme from '../shared-theme/AppTheme'; +import { + chartsCustomizations, + dataGridCustomizations, + datePickersCustomizations, + treeViewCustomizations, +} from './theme/customizations'; + +const xThemeComponents = { + ...chartsCustomizations, + ...dataGridCustomizations, + ...datePickersCustomizations, + ...treeViewCustomizations, +}; + +export default function Dashboard(props: { disableCustomTheme?: boolean }) { + return ( + + + + + + {/* Main content */} + ({ + flexGrow: 1, + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.background.defaultChannel} / 1)` + : alpha(theme.palette.background.default, 1), + overflow: 'auto', + })} + > + +
+ + + + + + ); +} diff --git a/src/dashboard/README.md b/src/dashboard/README.md new file mode 100644 index 0000000..b3d87a1 --- /dev/null +++ b/src/dashboard/README.md @@ -0,0 +1,15 @@ +# Dashboard template + +## Usage + + + +1. Copy these folders (`dashboard` and `shared-theme`) into your project, or one of the [example projects](https://github.com/mui/material-ui/tree/v6.x/examples). +2. Make sure your project has the required dependencies: @mui/material, @mui/icons-material, @emotion/styled, @emotion/react, @mui/x-charts, @mui/x-date-pickers, @mui/x-data-grid, @mui/x-tree-view, dayjs +3. Import and use the `Dashboard` component. + +## Demo + + + +View the demo at https://mui.com/material-ui/getting-started/templates/dashboard/. diff --git a/src/dashboard/Title.tsx.preview b/src/dashboard/Title.tsx.preview new file mode 100644 index 0000000..76fc02f --- /dev/null +++ b/src/dashboard/Title.tsx.preview @@ -0,0 +1,3 @@ + + {props.children} + \ No newline at end of file diff --git a/src/dashboard/components/AppNavbar.tsx b/src/dashboard/components/AppNavbar.tsx new file mode 100644 index 0000000..7392735 --- /dev/null +++ b/src/dashboard/components/AppNavbar.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import MuiToolbar from '@mui/material/Toolbar'; +import { tabsClasses } from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; +import MenuRoundedIcon from '@mui/icons-material/MenuRounded'; +import DashboardRoundedIcon from '@mui/icons-material/DashboardRounded'; +import SideMenuMobile from './SideMenuMobile'; +import MenuButton from './MenuButton'; +import ColorModeIconDropdown from '../../shared-theme/ColorModeIconDropdown'; + +const Toolbar = styled(MuiToolbar)({ + width: '100%', + padding: '12px', + display: 'flex', + flexDirection: 'column', + alignItems: 'start', + justifyContent: 'center', + gap: '12px', + flexShrink: 0, + [`& ${tabsClasses.flexContainer}`]: { + gap: '8px', + p: '8px', + pb: 0, + }, +}); + +export default function AppNavbar() { + const [open, setOpen] = React.useState(false); + + const toggleDrawer = (newOpen: boolean) => () => { + setOpen(newOpen); + }; + + return ( + + + + + + + Dashboard + + + + + + + + + + + ); +} + +export function CustomIcon() { + return ( + + + + ); +} diff --git a/src/dashboard/components/CardAlert.tsx b/src/dashboard/components/CardAlert.tsx new file mode 100644 index 0000000..b7d146a --- /dev/null +++ b/src/dashboard/components/CardAlert.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded'; + +export default function CardAlert() { + return ( + + + + + Plan about to expire + + + Enjoy 10% off when renewing your plan today. + + + + + ); +} diff --git a/src/dashboard/components/ChartUserByCountry.tsx b/src/dashboard/components/ChartUserByCountry.tsx new file mode 100644 index 0000000..0946925 --- /dev/null +++ b/src/dashboard/components/ChartUserByCountry.tsx @@ -0,0 +1,200 @@ +import * as React from 'react'; +import { PieChart } from '@mui/x-charts/PieChart'; +import { useDrawingArea } from '@mui/x-charts/hooks'; +import { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress'; + +import { + IndiaFlag, + UsaFlag, + BrazilFlag, + GlobeFlag, +} from '../internals/components/CustomIcons'; + +const data = [ + { label: 'India', value: 50000 }, + { label: 'USA', value: 35000 }, + { label: 'Brazil', value: 10000 }, + { label: 'Other', value: 5000 }, +]; + +const countries = [ + { + name: 'India', + value: 50, + flag: , + color: 'hsl(220, 25%, 65%)', + }, + { + name: 'USA', + value: 35, + flag: , + color: 'hsl(220, 25%, 45%)', + }, + { + name: 'Brazil', + value: 10, + flag: , + color: 'hsl(220, 25%, 30%)', + }, + { + name: 'Other', + value: 5, + flag: , + color: 'hsl(220, 25%, 20%)', + }, +]; + +interface StyledTextProps { + variant: 'primary' | 'secondary'; +} + +const StyledText = styled('text', { + shouldForwardProp: (prop) => prop !== 'variant', +})(({ theme }) => ({ + textAnchor: 'middle', + dominantBaseline: 'central', + fill: (theme.vars || theme).palette.text.secondary, + variants: [ + { + props: { + variant: 'primary', + }, + style: { + fontSize: theme.typography.h5.fontSize, + }, + }, + { + props: ({ variant }) => variant !== 'primary', + style: { + fontSize: theme.typography.body2.fontSize, + }, + }, + { + props: { + variant: 'primary', + }, + style: { + fontWeight: theme.typography.h5.fontWeight, + }, + }, + { + props: ({ variant }) => variant !== 'primary', + style: { + fontWeight: theme.typography.body2.fontWeight, + }, + }, + ], +})); + +interface PieCenterLabelProps { + primaryText: string; + secondaryText: string; +} + +function PieCenterLabel({ primaryText, secondaryText }: PieCenterLabelProps) { + const { width, height, left, top } = useDrawingArea(); + const primaryY = top + height / 2 - 10; + const secondaryY = primaryY + 24; + + return ( + + + {primaryText} + + + {secondaryText} + + + ); +} + +const colors = [ + 'hsl(220, 20%, 65%)', + 'hsl(220, 20%, 42%)', + 'hsl(220, 20%, 35%)', + 'hsl(220, 20%, 25%)', +]; + +export default function ChartUserByCountry() { + return ( + + + + Users by country + + + + + + + {countries.map((country, index) => ( + + {country.flag} + + + + {country.name} + + + {country.value}% + + + + + + ))} + + + ); +} diff --git a/src/dashboard/components/CustomDatePicker.tsx b/src/dashboard/components/CustomDatePicker.tsx new file mode 100644 index 0000000..316f327 --- /dev/null +++ b/src/dashboard/components/CustomDatePicker.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import Button from '@mui/material/Button'; +import CalendarTodayRoundedIcon from '@mui/icons-material/CalendarTodayRounded'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { UseDateFieldProps } from '@mui/x-date-pickers/DateField'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { + BaseSingleInputFieldProps, + DateValidationError, + FieldSection, +} from '@mui/x-date-pickers/models'; + +interface ButtonFieldProps + extends UseDateFieldProps, + BaseSingleInputFieldProps< + Dayjs | null, + Dayjs, + FieldSection, + false, + DateValidationError + > { + setOpen?: React.Dispatch>; +} + +function ButtonField(props: ButtonFieldProps) { + const { + setOpen, + label, + id, + disabled, + InputProps: { ref } = {}, + inputProps: { 'aria-label': ariaLabel } = {}, + } = props; + + return ( + + ); +} + +export default function CustomDatePicker() { + const [value, setValue] = React.useState(dayjs('2023-04-17')); + const [open, setOpen] = React.useState(false); + + return ( + + setValue(newValue)} + slots={{ field: ButtonField }} + slotProps={{ + field: { setOpen } as any, + nextIconButton: { size: 'small' }, + previousIconButton: { size: 'small' }, + }} + open={open} + onClose={() => setOpen(false)} + onOpen={() => setOpen(true)} + views={['day', 'month', 'year']} + /> + + ); +} diff --git a/src/dashboard/components/CustomizedDataGrid.tsx b/src/dashboard/components/CustomizedDataGrid.tsx new file mode 100644 index 0000000..1fedf34 --- /dev/null +++ b/src/dashboard/components/CustomizedDataGrid.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { DataGrid } from '@mui/x-data-grid'; +import { columns, rows } from '../internals/data/gridData'; + +export default function CustomizedDataGrid() { + return ( + + params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd' + } + initialState={{ + pagination: { paginationModel: { pageSize: 20 } }, + }} + pageSizeOptions={[10, 20, 50]} + disableColumnResize + density="compact" + slotProps={{ + filterPanel: { + filterFormProps: { + logicOperatorInputProps: { + variant: 'outlined', + size: 'small', + }, + columnInputProps: { + variant: 'outlined', + size: 'small', + sx: { mt: 'auto' }, + }, + operatorInputProps: { + variant: 'outlined', + size: 'small', + sx: { mt: 'auto' }, + }, + valueInputProps: { + InputComponentProps: { + variant: 'outlined', + size: 'small', + }, + }, + }, + }, + }} + /> + ); +} diff --git a/src/dashboard/components/CustomizedTreeView.tsx b/src/dashboard/components/CustomizedTreeView.tsx new file mode 100644 index 0000000..dc1380f --- /dev/null +++ b/src/dashboard/components/CustomizedTreeView.tsx @@ -0,0 +1,208 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { animated, useSpring } from '@react-spring/web'; +import { TransitionProps } from '@mui/material/transitions'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Collapse from '@mui/material/Collapse'; +import Typography from '@mui/material/Typography'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { + unstable_useTreeItem2 as useTreeItem2, + UseTreeItem2Parameters, +} from '@mui/x-tree-view/useTreeItem2'; +import { + TreeItem2Content, + TreeItem2IconContainer, + TreeItem2Label, + TreeItem2Root, +} from '@mui/x-tree-view/TreeItem2'; +import { TreeItem2Icon } from '@mui/x-tree-view/TreeItem2Icon'; +import { TreeItem2Provider } from '@mui/x-tree-view/TreeItem2Provider'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; +import { useTheme } from '@mui/material/styles'; + +type Color = 'blue' | 'green'; + +type ExtendedTreeItemProps = { + color?: Color; + id: string; + label: string; +}; + +const ITEMS: TreeViewBaseItem[] = [ + { + id: '1', + label: 'Website', + children: [ + { id: '1.1', label: 'Home', color: 'green' }, + { id: '1.2', label: 'Pricing', color: 'green' }, + { id: '1.3', label: 'About us', color: 'green' }, + { + id: '1.4', + label: 'Blog', + children: [ + { id: '1.1.1', label: 'Announcements', color: 'blue' }, + { id: '1.1.2', label: 'April lookahead', color: 'blue' }, + { id: '1.1.3', label: "What's new", color: 'blue' }, + { id: '1.1.4', label: 'Meet the team', color: 'blue' }, + ], + }, + ], + }, + { + id: '2', + label: 'Store', + children: [ + { id: '2.1', label: 'All products', color: 'green' }, + { + id: '2.2', + label: 'Categories', + children: [ + { id: '2.2.1', label: 'Gadgets', color: 'blue' }, + { id: '2.2.2', label: 'Phones', color: 'blue' }, + { id: '2.2.3', label: 'Wearables', color: 'blue' }, + ], + }, + { id: '2.3', label: 'Bestsellers', color: 'green' }, + { id: '2.4', label: 'Sales', color: 'green' }, + ], + }, + { id: '4', label: 'Contact', color: 'blue' }, + { id: '5', label: 'Help', color: 'blue' }, +]; + +function DotIcon({ color }: { color: string }) { + return ( + + + + + + ); +} + +const AnimatedCollapse = animated(Collapse); + +function TransitionComponent(props: TransitionProps) { + const style = useSpring({ + to: { + opacity: props.in ? 1 : 0, + transform: `translate3d(0,${props.in ? 0 : 20}px,0)`, + }, + }); + + return ; +} + +interface CustomLabelProps { + children: React.ReactNode; + color?: Color; + expandable?: boolean; +} + +function CustomLabel({ color, expandable, children, ...other }: CustomLabelProps) { + const theme = useTheme(); + const colors = { + blue: (theme.vars || theme).palette.primary.main, + green: (theme.vars || theme).palette.success.main, + }; + + const iconColor = color ? colors[color] : null; + return ( + + {iconColor && } + + {children} + + + ); +} + +interface CustomTreeItemProps + extends Omit, + Omit, 'onFocus'> {} + +const CustomTreeItem = React.forwardRef(function CustomTreeItem( + props: CustomTreeItemProps, + ref: React.Ref, +) { + const { id, itemId, label, disabled, children, ...other } = props; + + const { + getRootProps, + getContentProps, + getIconContainerProps, + getLabelProps, + getGroupTransitionProps, + status, + publicAPI, + } = useTreeItem2({ id, itemId, children, label, disabled, rootRef: ref }); + + const item = publicAPI.getItem(itemId); + const color = item?.color; + return ( + + + + {status.expandable && ( + + + + )} + + + + {children && ( + + )} + + + ); +}); + +export default function CustomizedTreeView() { + return ( + + + + Product tree + + + + + ); +} diff --git a/src/dashboard/components/Header.tsx b/src/dashboard/components/Header.tsx new file mode 100644 index 0000000..8c26e28 --- /dev/null +++ b/src/dashboard/components/Header.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import NotificationsRoundedIcon from '@mui/icons-material/NotificationsRounded'; +import CustomDatePicker from './CustomDatePicker'; +import NavbarBreadcrumbs from './NavbarBreadcrumbs'; +import MenuButton from './MenuButton'; +import ColorModeIconDropdown from '../../shared-theme/ColorModeIconDropdown'; + +import Search from './Search'; + +export default function Header() { + return ( + + + + + + + + + + + + ); +} diff --git a/src/dashboard/components/HighlightedCard.tsx b/src/dashboard/components/HighlightedCard.tsx new file mode 100644 index 0000000..02c6a67 --- /dev/null +++ b/src/dashboard/components/HighlightedCard.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded'; +import InsightsRoundedIcon from '@mui/icons-material/InsightsRounded'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { useTheme } from '@mui/material/styles'; + +export default function HighlightedCard() { + const theme = useTheme(); + const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); + + return ( + + + + + Explore your data + + + Uncover performance and visitor insights with our data wizardry. + + + + + ); +} diff --git a/src/dashboard/components/MainGrid.tsx b/src/dashboard/components/MainGrid.tsx new file mode 100644 index 0000000..4acaf46 --- /dev/null +++ b/src/dashboard/components/MainGrid.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import Grid from '@mui/material/Grid2'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Copyright from '../internals/components/Copyright'; +import ChartUserByCountry from './ChartUserByCountry'; +import CustomizedTreeView from './CustomizedTreeView'; +import CustomizedDataGrid from './CustomizedDataGrid'; +import HighlightedCard from './HighlightedCard'; +import PageViewsBarChart from './PageViewsBarChart'; +import SessionsChart from './SessionsChart'; +import StatCard, { StatCardProps } from './StatCard'; + +const data: StatCardProps[] = [ + { + title: 'Users', + value: '14k', + interval: 'Last 30 days', + trend: 'up', + data: [ + 200, 24, 220, 260, 240, 380, 100, 240, 280, 240, 300, 340, 320, 360, 340, 380, + 360, 400, 380, 420, 400, 640, 340, 460, 440, 480, 460, 600, 880, 920, + ], + }, + { + title: 'Conversions', + value: '325', + interval: 'Last 30 days', + trend: 'down', + data: [ + 1640, 1250, 970, 1130, 1050, 900, 720, 1080, 900, 450, 920, 820, 840, 600, 820, + 780, 800, 760, 380, 740, 660, 620, 840, 500, 520, 480, 400, 360, 300, 220, + ], + }, + { + title: 'Event count', + value: '200k', + interval: 'Last 30 days', + trend: 'neutral', + data: [ + 500, 400, 510, 530, 520, 600, 530, 520, 510, 730, 520, 510, 530, 620, 510, 530, + 520, 410, 530, 520, 610, 530, 520, 610, 530, 420, 510, 430, 520, 510, + ], + }, +]; + +export default function MainGrid() { + return ( + + {/* cards */} + + Overview + + theme.spacing(2) }} + > + {data.map((card, index) => ( + + + + ))} + + + + + + + + + + + + Details + + + + + + + + + + + + + + + ); +} diff --git a/src/dashboard/components/MenuButton.tsx b/src/dashboard/components/MenuButton.tsx new file mode 100644 index 0000000..e938d6f --- /dev/null +++ b/src/dashboard/components/MenuButton.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import Badge, { badgeClasses } from '@mui/material/Badge'; +import IconButton, { IconButtonProps } from '@mui/material/IconButton'; + +export interface MenuButtonProps extends IconButtonProps { + showBadge?: boolean; +} + +export default function MenuButton({ + showBadge = false, + ...props +}: MenuButtonProps) { + return ( + + + + ); +} diff --git a/src/dashboard/components/MenuContent.tsx b/src/dashboard/components/MenuContent.tsx new file mode 100644 index 0000000..01e189e --- /dev/null +++ b/src/dashboard/components/MenuContent.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Stack from '@mui/material/Stack'; +import HomeRoundedIcon from '@mui/icons-material/HomeRounded'; +import AnalyticsRoundedIcon from '@mui/icons-material/AnalyticsRounded'; +import PeopleRoundedIcon from '@mui/icons-material/PeopleRounded'; +import AssignmentRoundedIcon from '@mui/icons-material/AssignmentRounded'; +import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded'; +import InfoRoundedIcon from '@mui/icons-material/InfoRounded'; +import HelpRoundedIcon from '@mui/icons-material/HelpRounded'; + +const mainListItems = [ + { text: 'Home', icon: }, + { text: 'Analytics', icon: }, + { text: 'Clients', icon: }, + { text: 'Tasks', icon: }, +]; + +const secondaryListItems = [ + { text: 'Settings', icon: }, + { text: 'About', icon: }, + { text: 'Feedback', icon: }, +]; + +export default function MenuContent() { + return ( + + + {mainListItems.map((item, index) => ( + + + {item.icon} + + + + ))} + + + {secondaryListItems.map((item, index) => ( + + + {item.icon} + + + + ))} + + + ); +} diff --git a/src/dashboard/components/NavbarBreadcrumbs.tsx b/src/dashboard/components/NavbarBreadcrumbs.tsx new file mode 100644 index 0000000..9bf8f65 --- /dev/null +++ b/src/dashboard/components/NavbarBreadcrumbs.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; +import Breadcrumbs, { breadcrumbsClasses } from '@mui/material/Breadcrumbs'; +import NavigateNextRoundedIcon from '@mui/icons-material/NavigateNextRounded'; + +const StyledBreadcrumbs = styled(Breadcrumbs)(({ theme }) => ({ + margin: theme.spacing(1, 0), + [`& .${breadcrumbsClasses.separator}`]: { + color: (theme.vars || theme).palette.action.disabled, + margin: 1, + }, + [`& .${breadcrumbsClasses.ol}`]: { + alignItems: 'center', + }, +})); + +export default function NavbarBreadcrumbs() { + return ( + } + > + Dashboard + + Home + + + ); +} diff --git a/src/dashboard/components/OptionsMenu.tsx b/src/dashboard/components/OptionsMenu.tsx new file mode 100644 index 0000000..1f4059d --- /dev/null +++ b/src/dashboard/components/OptionsMenu.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Divider, { dividerClasses } from '@mui/material/Divider'; +import Menu from '@mui/material/Menu'; +import MuiMenuItem from '@mui/material/MenuItem'; +import { paperClasses } from '@mui/material/Paper'; +import { listClasses } from '@mui/material/List'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemIcon, { listItemIconClasses } from '@mui/material/ListItemIcon'; +import LogoutRoundedIcon from '@mui/icons-material/LogoutRounded'; +import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'; +import MenuButton from './MenuButton'; + +const MenuItem = styled(MuiMenuItem)({ + margin: '2px 0', +}); + +export default function OptionsMenu() { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + return ( + + + + + + Profile + My account + + Add another account + Settings + + + Logout + + + + + + + ); +} diff --git a/src/dashboard/components/PageViewsBarChart.tsx b/src/dashboard/components/PageViewsBarChart.tsx new file mode 100644 index 0000000..e7a90bd --- /dev/null +++ b/src/dashboard/components/PageViewsBarChart.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import { BarChart } from '@mui/x-charts/BarChart'; +import { useTheme } from '@mui/material/styles'; + +export default function PageViewsBarChart() { + const theme = useTheme(); + const colorPalette = [ + (theme.vars || theme).palette.primary.dark, + (theme.vars || theme).palette.primary.main, + (theme.vars || theme).palette.primary.light, + ]; + return ( + + + + Page views and downloads + + + + + 1.3M + + + + + Page views and downloads for the last 6 months + + + + + + ); +} diff --git a/src/dashboard/components/Search.tsx b/src/dashboard/components/Search.tsx new file mode 100644 index 0000000..58408e8 --- /dev/null +++ b/src/dashboard/components/Search.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import FormControl from '@mui/material/FormControl'; +import InputAdornment from '@mui/material/InputAdornment'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import SearchRoundedIcon from '@mui/icons-material/SearchRounded'; + +export default function Search() { + return ( + + + + + } + inputProps={{ + 'aria-label': 'search', + }} + /> + + ); +} diff --git a/src/dashboard/components/SelectContent.tsx b/src/dashboard/components/SelectContent.tsx new file mode 100644 index 0000000..4d36f03 --- /dev/null +++ b/src/dashboard/components/SelectContent.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import MuiAvatar from '@mui/material/Avatar'; +import MuiListItemAvatar from '@mui/material/ListItemAvatar'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemText from '@mui/material/ListItemText'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListSubheader from '@mui/material/ListSubheader'; +import Select, { SelectChangeEvent, selectClasses } from '@mui/material/Select'; +import Divider from '@mui/material/Divider'; +import { styled } from '@mui/material/styles'; +import AddRoundedIcon from '@mui/icons-material/AddRounded'; +import DevicesRoundedIcon from '@mui/icons-material/DevicesRounded'; +import SmartphoneRoundedIcon from '@mui/icons-material/SmartphoneRounded'; +import ConstructionRoundedIcon from '@mui/icons-material/ConstructionRounded'; + +const Avatar = styled(MuiAvatar)(({ theme }) => ({ + width: 28, + height: 28, + backgroundColor: (theme.vars || theme).palette.background.paper, + color: (theme.vars || theme).palette.text.secondary, + border: `1px solid ${(theme.vars || theme).palette.divider}`, +})); + +const ListItemAvatar = styled(MuiListItemAvatar)({ + minWidth: 0, + marginRight: 12, +}); + +export default function SelectContent() { + const [company, setCompany] = React.useState(''); + + const handleChange = (event: SelectChangeEvent) => { + setCompany(event.target.value as string); + }; + + return ( + + ); +} diff --git a/src/dashboard/components/SessionsChart.tsx b/src/dashboard/components/SessionsChart.tsx new file mode 100644 index 0000000..546f9aa --- /dev/null +++ b/src/dashboard/components/SessionsChart.tsx @@ -0,0 +1,150 @@ +import * as React from 'react'; +import { useTheme } from '@mui/material/styles'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import { LineChart } from '@mui/x-charts/LineChart'; + +function AreaGradient({ color, id }: { color: string; id: string }) { + return ( + + + + + + + ); +} + +function getDaysInMonth(month: number, year: number) { + const date = new Date(year, month, 0); + const monthName = date.toLocaleDateString('en-US', { + month: 'short', + }); + const daysInMonth = date.getDate(); + const days = []; + let i = 1; + while (days.length < daysInMonth) { + days.push(`${monthName} ${i}`); + i += 1; + } + return days; +} + +export default function SessionsChart() { + const theme = useTheme(); + const data = getDaysInMonth(4, 2024); + + const colorPalette = [ + theme.palette.primary.light, + theme.palette.primary.main, + theme.palette.primary.dark, + ]; + + return ( + + + + Sessions + + + + + 13,277 + + + + + Sessions per day for the last 30 days + + + (i + 1) % 5 === 0, + }, + ]} + series={[ + { + id: 'direct', + label: 'Direct', + showMark: false, + curve: 'linear', + stack: 'total', + area: true, + stackOrder: 'ascending', + data: [ + 300, 900, 600, 1200, 1500, 1800, 2400, 2100, 2700, 3000, 1800, 3300, + 3600, 3900, 4200, 4500, 3900, 4800, 5100, 5400, 4800, 5700, 6000, + 6300, 6600, 6900, 7200, 7500, 7800, 8100, + ], + }, + { + id: 'referral', + label: 'Referral', + showMark: false, + curve: 'linear', + stack: 'total', + area: true, + stackOrder: 'ascending', + data: [ + 500, 900, 700, 1400, 1100, 1700, 2300, 2000, 2600, 2900, 2300, 3200, + 3500, 3800, 4100, 4400, 2900, 4700, 5000, 5300, 5600, 5900, 6200, + 6500, 5600, 6800, 7100, 7400, 7700, 8000, + ], + }, + { + id: 'organic', + label: 'Organic', + showMark: false, + curve: 'linear', + stack: 'total', + stackOrder: 'ascending', + data: [ + 1000, 1500, 1200, 1700, 1300, 2000, 2400, 2200, 2600, 2800, 2500, + 3000, 3400, 3700, 3200, 3900, 4100, 3500, 4300, 4500, 4000, 4700, + 5000, 5200, 4800, 5400, 5600, 5900, 6100, 6300, + ], + area: true, + }, + ]} + height={250} + margin={{ left: 50, right: 20, top: 20, bottom: 20 }} + grid={{ horizontal: true }} + sx={{ + '& .MuiAreaElement-series-organic': { + fill: "url('#organic')", + }, + '& .MuiAreaElement-series-referral': { + fill: "url('#referral')", + }, + '& .MuiAreaElement-series-direct': { + fill: "url('#direct')", + }, + }} + slotProps={{ + legend: { + hidden: true, + }, + }} + > + + + + + + + ); +} diff --git a/src/dashboard/components/SideMenu.tsx b/src/dashboard/components/SideMenu.tsx new file mode 100644 index 0000000..a4eef8d --- /dev/null +++ b/src/dashboard/components/SideMenu.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Avatar from '@mui/material/Avatar'; +import MuiDrawer, { drawerClasses } from '@mui/material/Drawer'; +import Box from '@mui/material/Box'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import SelectContent from './SelectContent'; +import MenuContent from './MenuContent'; +import CardAlert from './CardAlert'; +import OptionsMenu from './OptionsMenu'; + +const drawerWidth = 240; + +const Drawer = styled(MuiDrawer)({ + width: drawerWidth, + flexShrink: 0, + boxSizing: 'border-box', + mt: 10, + [`& .${drawerClasses.paper}`]: { + width: drawerWidth, + boxSizing: 'border-box', + }, +}); + +export default function SideMenu() { + return ( + + + + + + + + + + + + + + Riley Carter + + + riley@email.com + + + + + + ); +} diff --git a/src/dashboard/components/SideMenuMobile.tsx b/src/dashboard/components/SideMenuMobile.tsx new file mode 100644 index 0000000..b259557 --- /dev/null +++ b/src/dashboard/components/SideMenuMobile.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import Drawer, { drawerClasses } from '@mui/material/Drawer'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import LogoutRoundedIcon from '@mui/icons-material/LogoutRounded'; +import NotificationsRoundedIcon from '@mui/icons-material/NotificationsRounded'; +import MenuButton from './MenuButton'; +import MenuContent from './MenuContent'; +import CardAlert from './CardAlert'; + +interface SideMenuMobileProps { + open: boolean | undefined; + toggleDrawer: (newOpen: boolean) => () => void; +} + +export default function SideMenuMobile({ open, toggleDrawer }: SideMenuMobileProps) { + return ( + theme.zIndex.drawer + 1, + [`& .${drawerClasses.paper}`]: { + backgroundImage: 'none', + backgroundColor: 'background.paper', + }, + }} + > + + + + + + Riley Carter + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/dashboard/components/StatCard.tsx b/src/dashboard/components/StatCard.tsx new file mode 100644 index 0000000..d587001 --- /dev/null +++ b/src/dashboard/components/StatCard.tsx @@ -0,0 +1,129 @@ +import * as React from 'react'; +import { useTheme } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { SparkLineChart } from '@mui/x-charts/SparkLineChart'; +import { areaElementClasses } from '@mui/x-charts/LineChart'; + +export type StatCardProps = { + title: string; + value: string; + interval: string; + trend: 'up' | 'down' | 'neutral'; + data: number[]; +}; + +function getDaysInMonth(month: number, year: number) { + const date = new Date(year, month, 0); + const monthName = date.toLocaleDateString('en-US', { + month: 'short', + }); + const daysInMonth = date.getDate(); + const days = []; + let i = 1; + while (days.length < daysInMonth) { + days.push(`${monthName} ${i}`); + i += 1; + } + return days; +} + +function AreaGradient({ color, id }: { color: string; id: string }) { + return ( + + + + + + + ); +} + +export default function StatCard({ + title, + value, + interval, + trend, + data, +}: StatCardProps) { + const theme = useTheme(); + const daysInWeek = getDaysInMonth(4, 2024); + + const trendColors = { + up: + theme.palette.mode === 'light' + ? theme.palette.success.main + : theme.palette.success.dark, + down: + theme.palette.mode === 'light' + ? theme.palette.error.main + : theme.palette.error.dark, + neutral: + theme.palette.mode === 'light' + ? theme.palette.grey[400] + : theme.palette.grey[700], + }; + + const labelColors = { + up: 'success' as const, + down: 'error' as const, + neutral: 'default' as const, + }; + + const color = labelColors[trend]; + const chartColor = trendColors[trend]; + const trendValues = { up: '+25%', down: '-25%', neutral: '+5%' }; + + return ( + + + + {title} + + + + + + {value} + + + + + {interval} + + + + + + + + + + + ); +} diff --git a/src/dashboard/internals/components/Copyright.tsx b/src/dashboard/internals/components/Copyright.tsx new file mode 100644 index 0000000..53d5373 --- /dev/null +++ b/src/dashboard/internals/components/Copyright.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import Link from '@mui/material/Link'; +import Typography from '@mui/material/Typography'; + +export default function Copyright(props: any) { + return ( + + {'Copyright © '} + + Sitemark + {' '} + {new Date().getFullYear()} + {'.'} + + ); +} diff --git a/src/dashboard/internals/components/CustomIcons.tsx b/src/dashboard/internals/components/CustomIcons.tsx new file mode 100644 index 0000000..8cc3462 --- /dev/null +++ b/src/dashboard/internals/components/CustomIcons.tsx @@ -0,0 +1,326 @@ +import * as React from 'react'; +import SvgIcon from '@mui/material/SvgIcon'; + +export function SitemarkIcon() { + return ( + + + + + + + + + + + + + ); +} + +export function IndiaFlag() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export function UsaFlag() { + return ( + + + + + + + + + + + + + + + + + + + + + + ); +} +export function BrazilFlag() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export function GlobeFlag() { + return ( + + + + + + + + + + + + + + ); +} diff --git a/src/dashboard/internals/data/gridData.tsx b/src/dashboard/internals/data/gridData.tsx new file mode 100644 index 0000000..2d2e5ea --- /dev/null +++ b/src/dashboard/internals/data/gridData.tsx @@ -0,0 +1,636 @@ +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Chip from '@mui/material/Chip'; +import { GridCellParams, GridRowsProp, GridColDef } from '@mui/x-data-grid'; +import { SparkLineChart } from '@mui/x-charts/SparkLineChart'; + +type SparkLineData = number[]; + +function getDaysInMonth(month: number, year: number) { + const date = new Date(year, month, 0); + const monthName = date.toLocaleDateString('en-US', { + month: 'short', + }); + const daysInMonth = date.getDate(); + const days = []; + let i = 1; + while (days.length < daysInMonth) { + days.push(`${monthName} ${i}`); + i += 1; + } + return days; +} + +function renderSparklineCell(params: GridCellParams) { + const data = getDaysInMonth(4, 2024); + const { value, colDef } = params; + + if (!value || value.length === 0) { + return null; + } + + return ( +
+ +
+ ); +} + +function renderStatus(status: 'Online' | 'Offline') { + const colors: { [index: string]: 'success' | 'default' } = { + Online: 'success', + Offline: 'default', + }; + + return ; +} + +export function renderAvatar( + params: GridCellParams<{ name: string; color: string }, any, any>, +) { + if (params.value == null) { + return ''; + } + + return ( + + {params.value.name.toUpperCase().substring(0, 1)} + + ); +} + +export const columns: GridColDef[] = [ + { field: 'pageTitle', headerName: 'Page Title', flex: 1.5, minWidth: 200 }, + { + field: 'status', + headerName: 'Status', + flex: 0.5, + minWidth: 80, + renderCell: (params) => renderStatus(params.value as any), + }, + { + field: 'users', + headerName: 'Users', + headerAlign: 'right', + align: 'right', + flex: 1, + minWidth: 80, + }, + { + field: 'eventCount', + headerName: 'Event Count', + headerAlign: 'right', + align: 'right', + flex: 1, + minWidth: 100, + }, + { + field: 'viewsPerUser', + headerName: 'Views per User', + headerAlign: 'right', + align: 'right', + flex: 1, + minWidth: 120, + }, + { + field: 'averageTime', + headerName: 'Average Time', + headerAlign: 'right', + align: 'right', + flex: 1, + minWidth: 100, + }, + { + field: 'conversions', + headerName: 'Daily Conversions', + flex: 1, + minWidth: 150, + renderCell: renderSparklineCell, + }, +]; + +export const rows: GridRowsProp = [ + { + id: 1, + pageTitle: 'Homepage Overview', + status: 'Online', + eventCount: 8345, + users: 212423, + viewsPerUser: 18.5, + averageTime: '2m 15s', + conversions: [ + 469172, 488506, 592287, 617401, 640374, 632751, 668638, 807246, 749198, 944863, + 911787, 844815, 992022, 1143838, 1446926, 1267886, 1362511, 1348746, 1560533, + 1670690, 1695142, 1916613, 1823306, 1683646, 2025965, 2529989, 3263473, + 3296541, 3041524, 2599497, + ], + }, + { + id: 2, + pageTitle: 'Product Details - Gadgets', + status: 'Online', + eventCount: 5653, + users: 172240, + viewsPerUser: 9.7, + averageTime: '2m 30s', + conversions: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 557488, 1341471, 2044561, 2206438, + ], + }, + { + id: 3, + pageTitle: 'Checkout Process - Step 1', + status: 'Offline', + eventCount: 3455, + users: 58240, + viewsPerUser: 15.2, + averageTime: '2m 10s', + conversions: [ + 166896, 190041, 248686, 226746, 261744, 271890, 332176, 381123, 396435, 495620, + 520278, 460839, 704158, 559134, 681089, 712384, 765381, 771374, 851314, 907947, + 903675, 1049642, 1003160, 881573, 1072283, 1139115, 1382701, 1395655, 1355040, + 1381571, + ], + }, + { + id: 4, + pageTitle: 'User Profile Dashboard', + status: 'Online', + eventCount: 112543, + users: 96240, + viewsPerUser: 4.5, + averageTime: '2m 40s', + conversions: [ + 264651, 311845, 436558, 439385, 520413, 533380, 562363, 533793, 558029, 791126, + 649082, 566792, 723451, 737827, 890859, 935554, 1044397, 1022973, 1129827, + 1145309, 1195630, 1358925, 1373160, 1172679, 1340106, 1396974, 1623641, + 1687545, 1581634, 1550291, + ], + }, + { + id: 5, + pageTitle: 'Article Listing - Tech News', + status: 'Offline', + eventCount: 3653, + users: 142240, + viewsPerUser: 3.1, + averageTime: '2m 55s', + conversions: [ + 251871, 262216, 402383, 396459, 378793, 406720, 447538, 451451, 457111, 589821, + 640744, 504879, 626099, 662007, 754576, 768231, 833019, 851537, 972306, + 1014831, 1027570, 1189068, 1119099, 987244, 1197954, 1310721, 1480816, 1577547, + 1854053, 1791831, + ], + }, + { + id: 6, + pageTitle: 'FAQs - Customer Support', + status: 'Online', + eventCount: 106543, + users: 15240, + viewsPerUser: 7.2, + averageTime: '2m 20s', + conversions: [ + 13671, 16918, 27272, 34315, 42212, 56369, 64241, 77857, 70680, 91093, 108306, + 94734, 132289, 133860, 147706, 158504, 192578, 207173, 220052, 233496, 250091, + 285557, 268555, 259482, 274019, 321648, 359801, 399502, 447249, 497403, + ], + }, + { + id: 7, + pageTitle: 'Product Comparison - Laptops', + status: 'Offline', + eventCount: 7853, + users: 32240, + viewsPerUser: 6.5, + averageTime: '2m 50s', + conversions: [ + 93682, 107901, 144919, 151769, 170804, 183736, 201752, 219792, 227887, 295382, + 309600, 278050, 331964, 356826, 404896, 428090, 470245, 485582, 539056, 582112, + 594289, 671915, 649510, 574911, 713843, 754965, 853020, 916793, 960158, 984265, + ], + }, + { + id: 8, + pageTitle: 'Shopping Cart - Electronics', + status: 'Online', + eventCount: 8563, + users: 48240, + viewsPerUser: 4.3, + averageTime: '3m 10s', + conversions: [ + 52394, 63357, 82800, 105466, 128729, 144472, 172148, 197919, 212302, 278153, + 290499, 249824, 317499, 333024, 388925, 410576, 462099, 488477, 533956, 572307, + 591019, 681506, 653332, 581234, 719038, 783496, 911609, 973328, 1056071, + 1112940, + ], + }, + { + id: 9, + pageTitle: 'Payment Confirmation - Bank Transfer', + status: 'Offline', + eventCount: 4563, + users: 18240, + viewsPerUser: 2.7, + averageTime: '3m 25s', + conversions: [ + 15372, 16901, 25489, 30148, 40857, 51136, 64627, 75804, 89633, 100407, 114908, + 129957, 143568, 158509, 174822, 192488, 211512, 234702, 258812, 284328, 310431, + 338186, 366582, 396749, 428788, 462880, 499125, 537723, 578884, 622825, + ], + }, + { + id: 10, + pageTitle: 'Product Reviews - Smartphones', + status: 'Online', + eventCount: 9863, + users: 28240, + viewsPerUser: 5.1, + averageTime: '3m 05s', + conversions: [ + 70211, 89234, 115676, 136021, 158744, 174682, 192890, 218073, 240926, 308190, + 317552, 279834, 334072, 354955, 422153, 443911, 501486, 538091, 593724, 642882, + 686539, 788615, 754813, 687955, 883645, 978347, 1142551, 1233074, 1278155, + 1356724, + ], + }, + { + id: 11, + pageTitle: 'Subscription Management - Services', + status: 'Offline', + eventCount: 6563, + users: 24240, + viewsPerUser: 4.8, + averageTime: '3m 15s', + conversions: [ + 49662, 58971, 78547, 93486, 108722, 124901, 146422, 167883, 189295, 230090, + 249837, 217828, 266494, 287537, 339586, 363299, 412855, 440900, 490111, 536729, + 580591, 671635, 655812, 576431, 741632, 819296, 971762, 1052605, 1099234, + 1173591, + ], + }, + { + id: 12, + pageTitle: 'Order Tracking - Shipments', + status: 'Online', + eventCount: 12353, + users: 38240, + viewsPerUser: 3.5, + averageTime: '3m 20s', + conversions: [ + 29589, 37965, 55800, 64672, 77995, 91126, 108203, 128900, 148232, 177159, + 193489, 164471, 210765, 229977, 273802, 299381, 341092, 371567, 413812, 457693, + 495920, 564785, 541022, 491680, 618096, 704926, 833365, 904313, 974622, + 1036567, + ], + }, + { + id: 13, + pageTitle: 'Customer Feedback - Surveys', + status: 'Offline', + eventCount: 5863, + users: 13240, + viewsPerUser: 2.3, + averageTime: '3m 30s', + conversions: [ + 8472, 9637, 14892, 19276, 23489, 28510, 33845, 39602, 45867, 52605, 59189, + 65731, 76021, 85579, 96876, 108515, 119572, 131826, 145328, 160192, 176528, + 196662, 217929, 239731, 262920, 289258, 315691, 342199, 370752, 402319, + ], + }, + { + id: 14, + pageTitle: 'Account Settings - Preferences', + status: 'Online', + eventCount: 7853, + users: 18240, + viewsPerUser: 3.2, + averageTime: '3m 15s', + conversions: [ + 15792, 16948, 22728, 25491, 28412, 31268, 34241, 37857, 42068, 46893, 51098, + 55734, 60780, 66421, 72680, 79584, 87233, 95711, 105285, 115814, 127509, + 140260, 154086, 169495, 186445, 205109, 225580, 247983, 272484, 299280, + ], + }, + { + id: 15, + pageTitle: 'Login Page - Authentication', + status: 'Offline', + eventCount: 9563, + users: 24240, + viewsPerUser: 2.5, + averageTime: '3m 35s', + conversions: [ + 25638, 28355, 42089, 53021, 66074, 80620, 97989, 118202, 142103, 166890, + 193869, 225467, 264089, 307721, 358059, 417835, 488732, 573924, 674878, 794657, + 938542, 1111291, 1313329, 1543835, 1812156, 2123349, 2484926, 2907023, 3399566, + 3973545, + ], + }, + { + id: 16, + pageTitle: 'Promotions - Seasonal Sales', + status: 'Online', + eventCount: 13423, + users: 54230, + viewsPerUser: 7.8, + averageTime: '2m 45s', + conversions: [ + 241732, 256384, 289465, 321423, 345672, 378294, 398472, 420364, 436278, 460192, + 495374, 510283, 532489, 559672, 587312, 610982, 629385, 654732, 678925, 704362, + 725182, 749384, 772361, 798234, 819472, 846291, 872183, 894673, 919283, 945672, + ], + }, + { + id: 17, + pageTitle: 'Tutorials - How to Guides', + status: 'Offline', + eventCount: 4234, + users: 19342, + viewsPerUser: 5.2, + averageTime: '3m 05s', + conversions: [ + 12345, 14567, 16789, 18901, 21023, 23145, 25267, 27389, 29501, 31623, 33745, + 35867, 37989, 40101, 42223, 44345, 46467, 48589, 50701, 52823, 54945, 57067, + 59189, 61301, 63423, 65545, 67667, 69789, 71901, 74023, + ], + }, + { + id: 18, + pageTitle: 'Blog Posts - Tech Insights', + status: 'Online', + eventCount: 8567, + users: 34234, + viewsPerUser: 6.3, + averageTime: '2m 50s', + conversions: [ + 23456, 25678, 27890, 30102, 32324, 34546, 36768, 38980, 41202, 43424, 45646, + 47868, 50080, 52302, 54524, 56746, 58968, 61180, 63402, 65624, 67846, 70068, + 72290, 74502, 76724, 78946, 81168, 83380, 85602, 87824, + ], + }, + { + id: 19, + pageTitle: 'Events - Webinars', + status: 'Offline', + eventCount: 3456, + users: 19234, + viewsPerUser: 4.5, + averageTime: '3m 20s', + conversions: [ + 123456, 145678, 167890, 190012, 212324, 234546, 256768, 278980, 301202, 323424, + 345646, 367868, 390080, 412302, 434524, 456746, 478968, 501180, 523402, 545624, + 567846, 590068, 612290, 634502, 656724, 678946, 701168, 723380, 745602, 767824, + ], + }, + { + id: 20, + pageTitle: 'Support - Contact Us', + status: 'Online', + eventCount: 6734, + users: 27645, + viewsPerUser: 3.9, + averageTime: '2m 55s', + conversions: [ + 234567, 256789, 278901, 301023, 323245, 345467, 367689, 389801, 412023, 434245, + 456467, 478689, 500801, 523023, 545245, 567467, 589689, 611801, 634023, 656245, + 678467, 700689, 722801, 745023, 767245, 789467, 811689, 833801, 856023, 878245, + ], + }, + { + id: 21, + pageTitle: 'Case Studies - Success Stories', + status: 'Offline', + eventCount: 4567, + users: 19345, + viewsPerUser: 6.1, + averageTime: '3m 10s', + conversions: [ + 34567, 36789, 38901, 41023, 43145, 45267, 47389, 49501, 51623, 53745, 55867, + 57989, 60101, 62223, 64345, 66467, 68589, 70701, 72823, 74945, 77067, 79189, + 81301, 83423, 85545, 87667, 89789, 91901, 94023, 96145, + ], + }, + { + id: 22, + pageTitle: 'News - Industry Updates', + status: 'Online', + eventCount: 7856, + users: 34567, + viewsPerUser: 5.7, + averageTime: '3m 05s', + conversions: [ + 45678, 47890, 50102, 52324, 54546, 56768, 58980, 61202, 63424, 65646, 67868, + 70080, 72302, 74524, 76746, 78968, 81180, 83402, 85624, 87846, 90068, 92290, + 94502, 96724, 98946, 101168, 103380, 105602, 107824, 110046, + ], + }, + { + id: 23, + pageTitle: 'Forum - User Discussions', + status: 'Offline', + eventCount: 5678, + users: 23456, + viewsPerUser: 4.2, + averageTime: '2m 40s', + conversions: [ + 56789, 58901, 61023, 63145, 65267, 67389, 69501, 71623, 73745, 75867, 77989, + 80101, 82223, 84345, 86467, 88589, 90701, 92823, 94945, 97067, 99189, 101301, + 103423, 105545, 107667, 109789, 111901, 114023, 116145, 118267, + ], + }, + { + id: 24, + pageTitle: 'Documentation - API Reference', + status: 'Online', + eventCount: 6789, + users: 27689, + viewsPerUser: 5.0, + averageTime: '3m 00s', + conversions: [ + 67890, 70102, 72324, 74546, 76768, 78980, 81202, 83424, 85646, 87868, 90080, + 92302, 94524, 96746, 98968, 101180, 103402, 105624, 107846, 110068, 112290, + 114502, 116724, 118946, 121168, 123380, 125602, 127824, 130046, 132268, + ], + }, + { + id: 25, + pageTitle: 'Services - Consulting', + status: 'Offline', + eventCount: 4563, + users: 19240, + viewsPerUser: 6.4, + averageTime: '3m 25s', + conversions: [ + 345678, 367890, 390012, 412324, 434546, 456768, 478980, 501202, 523424, 545646, + 567868, 590080, 612302, 634524, 656746, 678968, 701180, 723402, 745624, 767846, + 790068, 812290, 834502, 856724, 878946, 901168, 923380, 945602, 967824, 990046, + ], + }, + { + id: 26, + pageTitle: 'Feedback - User Reviews', + status: 'Online', + eventCount: 8564, + users: 34240, + viewsPerUser: 6.2, + averageTime: '3m 15s', + conversions: [ + 123478, 145690, 167912, 190134, 212356, 234578, 256790, 279012, 301234, 323456, + 345678, 367890, 390012, 412234, 434456, 456678, 478890, 501012, 523234, 545456, + 567678, 589890, 612012, 634234, 656456, 678678, 700890, 723012, 745234, 767456, + ], + }, + { + id: 27, + pageTitle: 'Profiles - Team Members', + status: 'Offline', + eventCount: 5634, + users: 23423, + viewsPerUser: 5.5, + averageTime: '2m 45s', + conversions: [ + 345123, 367345, 389567, 411789, 434012, 456234, 478456, 500678, 522901, 545123, + 567345, 589567, 611789, 634012, 656234, 678456, 700678, 722901, 745123, 767345, + 789567, 811789, 834012, 856234, 878456, 900678, 922901, 945123, 967345, 989567, + ], + }, + { + id: 28, + pageTitle: 'Notifications - Alerts', + status: 'Online', + eventCount: 6745, + users: 27654, + viewsPerUser: 4.9, + averageTime: '3m 10s', + conversions: [ + 456123, 478345, 500567, 522789, 545012, 567234, 589456, 611678, 633901, 656123, + 678345, 700567, 722789, 745012, 767234, 789456, 811678, 833901, 856123, 878345, + 900567, 922789, 945012, 967234, 989456, 1011678, 1033901, 1056123, 1078345, + 1100567, + ], + }, + { + id: 29, + pageTitle: 'Dashboard - Metrics', + status: 'Offline', + eventCount: 5678, + users: 23456, + viewsPerUser: 6.3, + averageTime: '2m 50s', + conversions: [ + 567890, 590112, 612334, 634556, 656778, 678990, 701212, 723434, 745656, 767878, + 790100, 812322, 834544, 856766, 878988, 901210, 923432, 945654, 967876, 990098, + 1012320, 1034542, 1056764, 1078986, 1101208, 1123430, 1145652, 1167874, + 1190096, 1212318, + ], + }, + { + id: 30, + pageTitle: 'Reports - Monthly Analysis', + status: 'Online', + eventCount: 7890, + users: 34567, + viewsPerUser: 5.9, + averageTime: '3m 20s', + conversions: [ + 678901, 701123, 723345, 745567, 767789, 790011, 812233, 834455, 856677, 878899, + 901121, 923343, 945565, 967787, 990009, 1012231, 1034453, 1056675, 1078897, + 1101119, 1123341, 1145563, 1167785, 1190007, 1212229, 1234451, 1256673, + 1278895, 1301117, 1323339, + ], + }, + { + id: 31, + pageTitle: 'Training - Employee Onboarding', + status: 'Offline', + eventCount: 3456, + users: 19234, + viewsPerUser: 6.1, + averageTime: '3m 10s', + conversions: [ + 789012, 811234, 833456, 855678, 877890, 900112, 922334, 944556, 966778, 989000, + 1011222, 1033444, 1055666, 1077888, 1100110, 1122332, 1144554, 1166776, + 1188998, 1211220, 1233442, 1255664, 1277886, 1300108, 1322330, 1344552, + 1366774, 1388996, 1411218, 1433440, + ], + }, + { + id: 32, + pageTitle: 'Resources - Knowledge Base', + status: 'Online', + eventCount: 5678, + users: 23456, + viewsPerUser: 4.7, + averageTime: '3m 25s', + conversions: [ + 890123, 912345, 934567, 956789, 979012, 1001234, 1023456, 1045678, 1067890, + 1090123, 1112345, 1134567, 1156789, 1179012, 1201234, 1223456, 1245678, + 1267890, 1290123, 1312345, 1334567, 1356789, 1379012, 1401234, 1423456, + 1445678, 1467890, 1490123, 1512345, 1534567, + ], + }, + { + id: 33, + pageTitle: 'Settings - Privacy Controls', + status: 'Offline', + eventCount: 6789, + users: 27689, + viewsPerUser: 5.8, + averageTime: '3m 05s', + conversions: [ + 901234, 923456, 945678, 967890, 990112, 1012334, 1034556, 1056778, 1079000, + 1101222, 1123444, 1145666, 1167888, 1190110, 1212332, 1234554, 1256776, + 1278998, 1301220, 1323442, 1345664, 1367886, 1390108, 1412330, 1434552, + 1456774, 1478996, 1501218, 1523440, 1545662, + ], + }, + { + id: 34, + pageTitle: 'Integrations - Third-Party Services', + status: 'Online', + eventCount: 4567, + users: 19345, + viewsPerUser: 4.4, + averageTime: '2m 50s', + conversions: [ + 123457, 145679, 167891, 190113, 212335, 234557, 256779, 279001, 301223, 323445, + 345667, 367889, 390011, 412233, 434455, 456677, 478899, 501121, 523343, 545565, + 567787, 590009, 612231, 634453, 656675, 678897, 701119, 723341, 745563, 767785, + ], + }, + { + id: 35, + pageTitle: 'Account - Billing Information', + status: 'Offline', + eventCount: 7890, + users: 34567, + viewsPerUser: 5.4, + averageTime: '3m 00s', + conversions: [ + 234568, 256790, 278912, 301134, 323356, 345578, 367790, 390012, 412234, 434456, + 456678, 478890, 501112, 523334, 545556, 567778, 590000, 612222, 634444, 656666, + 678888, 701110, 723332, 745554, 767776, 789998, 812220, 834442, 856664, 878886, + ], + }, +]; diff --git a/src/dashboard/theme/customizations/charts.ts b/src/dashboard/theme/customizations/charts.ts new file mode 100644 index 0000000..9c2714c --- /dev/null +++ b/src/dashboard/theme/customizations/charts.ts @@ -0,0 +1,76 @@ +import { Theme } from '@mui/material/styles'; +import { axisClasses, legendClasses, chartsGridClasses } from '@mui/x-charts'; +import type { ChartsComponents } from '@mui/x-charts/themeAugmentation'; +import { gray } from '../../../shared-theme/themePrimitives'; + +/* eslint-disable import/prefer-default-export */ +export const chartsCustomizations: ChartsComponents = { + MuiChartsAxis: { + styleOverrides: { + root: ({ theme }) => ({ + [`& .${axisClasses.line}`]: { + stroke: gray[300], + }, + [`& .${axisClasses.tick}`]: { stroke: gray[300] }, + [`& .${axisClasses.tickLabel}`]: { + fill: gray[500], + fontWeight: 500, + }, + ...theme.applyStyles('dark', { + [`& .${axisClasses.line}`]: { + stroke: gray[700], + }, + [`& .${axisClasses.tick}`]: { stroke: gray[700] }, + [`& .${axisClasses.tickLabel}`]: { + fill: gray[300], + fontWeight: 500, + }, + }), + }), + }, + }, + MuiChartsTooltip: { + styleOverrides: { + mark: ({ theme }) => ({ + ry: 6, + boxShadow: 'none', + border: `1px solid ${(theme.vars || theme).palette.divider}`, + }), + table: ({ theme }) => ({ + border: `1px solid ${(theme.vars || theme).palette.divider}`, + borderRadius: theme.shape.borderRadius, + background: 'hsl(0, 0%, 100%)', + ...theme.applyStyles('dark', { + background: gray[900], + }), + }), + }, + }, + MuiChartsLegend: { + styleOverrides: { + root: { + [`& .${legendClasses.mark}`]: { + ry: 6, + }, + }, + }, + }, + MuiChartsGrid: { + styleOverrides: { + root: ({ theme }) => ({ + [`& .${chartsGridClasses.line}`]: { + stroke: gray[200], + strokeDasharray: '4 2', + strokeWidth: 0.8, + }, + ...theme.applyStyles('dark', { + [`& .${chartsGridClasses.line}`]: { + stroke: gray[700], + strokeDasharray: '4 2', + strokeWidth: 0.8, + }, + }), + }), + }, + }, +}; diff --git a/src/dashboard/theme/customizations/dataGrid.ts b/src/dashboard/theme/customizations/dataGrid.ts new file mode 100644 index 0000000..822b513 --- /dev/null +++ b/src/dashboard/theme/customizations/dataGrid.ts @@ -0,0 +1,132 @@ +import { paperClasses } from '@mui/material/Paper'; +import { alpha, Theme } from '@mui/material/styles'; +import type { DataGridProComponents } from '@mui/x-data-grid-pro/themeAugmentation'; +import { menuItemClasses } from '@mui/material/MenuItem'; +import { listItemIconClasses } from '@mui/material/ListItemIcon'; +import { iconButtonClasses } from '@mui/material/IconButton'; +import { checkboxClasses } from '@mui/material/Checkbox'; +import { listClasses } from '@mui/material/List'; +import { gridClasses } from '@mui/x-data-grid'; +import { tablePaginationClasses } from '@mui/material/TablePagination'; +import { gray } from '../../../shared-theme/themePrimitives'; + +/* eslint-disable import/prefer-default-export */ +export const dataGridCustomizations: DataGridProComponents & DataGridProComponents = { + MuiDataGrid: { + styleOverrides: { + root: ({ theme }) => ({ + '--DataGrid-overlayHeight': '300px', + overflow: 'clip', + borderColor: (theme.vars || theme).palette.divider, + backgroundColor: (theme.vars || theme).palette.background.default, + [`& .${gridClasses.columnHeader}`]: { + backgroundColor: (theme.vars || theme).palette.background.paper, + }, + [`& .${gridClasses.footerContainer}`]: { + backgroundColor: (theme.vars || theme).palette.background.paper, + }, + [`& .${checkboxClasses.root}`]: { + padding: theme.spacing(0.5), + '& > svg': { + fontSize: '1rem', + }, + }, + [`& .${tablePaginationClasses.root}`]: { + marginRight: theme.spacing(1), + '& .MuiIconButton-root': { + maxHeight: 32, + maxWidth: 32, + '& > svg': { + fontSize: '1rem', + }, + }, + }, + }), + cell: ({ theme }) => ({ borderTopColor: (theme.vars || theme).palette.divider }), + menu: ({ theme }) => ({ + borderRadius: theme.shape.borderRadius, + backgroundImage: 'none', + [`& .${paperClasses.root}`]: { + border: `1px solid ${(theme.vars || theme).palette.divider}`, + }, + + [`& .${menuItemClasses.root}`]: { + margin: '0 4px', + }, + [`& .${listItemIconClasses.root}`]: { + marginRight: 0, + }, + [`& .${listClasses.root}`]: { + paddingLeft: 0, + paddingRight: 0, + }, + }), + + row: ({ theme }) => ({ + '&:last-of-type': { borderBottom: `1px solid ${(theme.vars || theme).palette.divider}` }, + '&:hover': { + backgroundColor: (theme.vars || theme).palette.action.hover, + }, + '&.Mui-selected': { + background: (theme.vars || theme).palette.action.selected, + '&:hover': { + backgroundColor: (theme.vars || theme).palette.action.hover, + }, + }, + }), + iconButtonContainer: ({ theme }) => ({ + [`& .${iconButtonClasses.root}`]: { + border: 'none', + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: alpha(theme.palette.action.selected, 0.3), + }, + '&:active': { + backgroundColor: gray[200], + }, + ...theme.applyStyles('dark', { + color: gray[50], + '&:hover': { + backgroundColor: gray[800], + }, + '&:active': { + backgroundColor: gray[900], + }, + }), + }, + }), + menuIconButton: ({ theme }) => ({ + border: 'none', + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: gray[100], + }, + '&:active': { + backgroundColor: gray[200], + }, + ...theme.applyStyles('dark', { + color: gray[50], + '&:hover': { + backgroundColor: gray[800], + }, + '&:active': { + backgroundColor: gray[900], + }, + }), + }), + filterForm: ({ theme }) => ({ + gap: theme.spacing(1), + alignItems: 'flex-end', + }), + columnsManagementHeader: ({ theme }) => ({ + paddingRight: theme.spacing(3), + paddingLeft: theme.spacing(3), + }), + columnHeaderTitleContainer: { + flexGrow: 1, + justifyContent: 'space-between', + }, + columnHeaderDraggableContainer: { paddingRight: 2 }, + }, + }, +}; diff --git a/src/dashboard/theme/customizations/datePickers.ts b/src/dashboard/theme/customizations/datePickers.ts new file mode 100644 index 0000000..be5e3b3 --- /dev/null +++ b/src/dashboard/theme/customizations/datePickers.ts @@ -0,0 +1,173 @@ +import { alpha, Theme } from '@mui/material/styles'; +import type { PickersProComponents } from '@mui/x-date-pickers-pro/themeAugmentation'; +import type { PickerComponents } from '@mui/x-date-pickers/themeAugmentation'; +import { pickersYearClasses, pickersMonthClasses, pickersDayClasses } from '@mui/x-date-pickers'; +import { menuItemClasses } from '@mui/material/MenuItem'; +import { gray, brand } from '../../../shared-theme/themePrimitives'; + +/* eslint-disable import/prefer-default-export */ +export const datePickersCustomizations: PickersProComponents & PickerComponents = { + MuiPickersPopper: { + styleOverrides: { + paper: ({ theme }) => ({ + marginTop: 4, + borderRadius: theme.shape.borderRadius, + border: `1px solid ${(theme.vars || theme).palette.divider}`, + backgroundImage: 'none', + background: 'hsl(0, 0%, 100%)', + boxShadow: + 'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px', + [`& .${menuItemClasses.root}`]: { + borderRadius: 6, + margin: '0 6px', + }, + ...theme.applyStyles('dark', { + background: gray[900], + boxShadow: + 'hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px', + }), + }), + }, + }, + MuiPickersArrowSwitcher: { + styleOverrides: { + spacer: { width: 16 }, + button: ({ theme }) => ({ + backgroundColor: 'transparent', + color: (theme.vars || theme).palette.grey[500], + ...theme.applyStyles('dark', { + color: (theme.vars || theme).palette.grey[400], + }), + }), + }, + }, + MuiPickersCalendarHeader: { + styleOverrides: { + switchViewButton: { + padding: 0, + border: 'none', + }, + }, + }, + MuiPickersMonth: { + styleOverrides: { + monthButton: ({ theme }) => ({ + fontSize: theme.typography.body1.fontSize, + color: (theme.vars || theme).palette.grey[600], + padding: theme.spacing(0.5), + borderRadius: theme.shape.borderRadius, + '&:hover': { + backgroundColor: (theme.vars || theme).palette.action.hover, + }, + [`&.${pickersMonthClasses.selected}`]: { + backgroundColor: gray[700], + fontWeight: theme.typography.fontWeightMedium, + }, + '&:focus': { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '2px', + backgroundColor: 'transparent', + [`&.${pickersMonthClasses.selected}`]: { backgroundColor: gray[700] }, + }, + ...theme.applyStyles('dark', { + color: (theme.vars || theme).palette.grey[300], + '&:hover': { + backgroundColor: (theme.vars || theme).palette.action.hover, + }, + [`&.${pickersMonthClasses.selected}`]: { + color: (theme.vars || theme).palette.common.black, + fontWeight: theme.typography.fontWeightMedium, + backgroundColor: gray[300], + }, + '&:focus': { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '2px', + backgroundColor: 'transparent', + [`&.${pickersMonthClasses.selected}`]: { backgroundColor: gray[300] }, + }, + }), + }), + }, + }, + MuiPickersYear: { + styleOverrides: { + yearButton: ({ theme }) => ({ + fontSize: theme.typography.body1.fontSize, + color: (theme.vars || theme).palette.grey[600], + padding: theme.spacing(0.5), + borderRadius: theme.shape.borderRadius, + height: 'fit-content', + '&:hover': { + backgroundColor: (theme.vars || theme).palette.action.hover, + }, + [`&.${pickersYearClasses.selected}`]: { + backgroundColor: gray[700], + fontWeight: theme.typography.fontWeightMedium, + }, + '&:focus': { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '2px', + backgroundColor: 'transparent', + [`&.${pickersYearClasses.selected}`]: { backgroundColor: gray[700] }, + }, + ...theme.applyStyles('dark', { + color: (theme.vars || theme).palette.grey[300], + '&:hover': { + backgroundColor: (theme.vars || theme).palette.action.hover, + }, + [`&.${pickersYearClasses.selected}`]: { + color: (theme.vars || theme).palette.common.black, + fontWeight: theme.typography.fontWeightMedium, + backgroundColor: gray[300], + }, + '&:focus': { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '2px', + backgroundColor: 'transparent', + [`&.${pickersYearClasses.selected}`]: { backgroundColor: gray[300] }, + }, + }), + }), + }, + }, + MuiPickersDay: { + styleOverrides: { + root: ({ theme }) => ({ + fontSize: theme.typography.body1.fontSize, + color: (theme.vars || theme).palette.grey[600], + padding: theme.spacing(0.5), + borderRadius: theme.shape.borderRadius, + '&:hover': { + backgroundColor: (theme.vars || theme).palette.action.hover, + }, + [`&.${pickersDayClasses.selected}`]: { + backgroundColor: gray[700], + fontWeight: theme.typography.fontWeightMedium, + }, + '&:focus': { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '2px', + backgroundColor: 'transparent', + [`&.${pickersDayClasses.selected}`]: { backgroundColor: gray[700] }, + }, + ...theme.applyStyles('dark', { + color: (theme.vars || theme).palette.grey[300], + '&:hover': { + backgroundColor: (theme.vars || theme).palette.action.hover, + }, + [`&.${pickersDayClasses.selected}`]: { + color: (theme.vars || theme).palette.common.black, + fontWeight: theme.typography.fontWeightMedium, + backgroundColor: gray[300], + }, + '&:focus': { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '2px', + backgroundColor: 'transparent', + [`&.${pickersDayClasses.selected}`]: { backgroundColor: gray[300] }, + }, + }), + }), + }, + }, +}; diff --git a/src/dashboard/theme/customizations/index.ts b/src/dashboard/theme/customizations/index.ts new file mode 100644 index 0000000..ef97812 --- /dev/null +++ b/src/dashboard/theme/customizations/index.ts @@ -0,0 +1,4 @@ +export { chartsCustomizations } from './charts'; +export { dataGridCustomizations } from './dataGrid'; +export { datePickersCustomizations } from './datePickers'; +export { treeViewCustomizations } from './treeView'; diff --git a/src/dashboard/theme/customizations/treeView.ts b/src/dashboard/theme/customizations/treeView.ts new file mode 100644 index 0000000..b41c431 --- /dev/null +++ b/src/dashboard/theme/customizations/treeView.ts @@ -0,0 +1,62 @@ +import { alpha, Theme } from '@mui/material/styles'; +import type { TreeViewComponents } from '@mui/x-tree-view/themeAugmentation'; +import { gray, brand } from '../../../shared-theme/themePrimitives'; + +/* eslint-disable import/prefer-default-export */ +export const treeViewCustomizations: TreeViewComponents = { + MuiTreeItem2: { + styleOverrides: { + root: ({ theme }) => ({ + position: 'relative', + boxSizing: 'border-box', + padding: theme.spacing(0, 1), + '& .groupTransition': { + marginLeft: theme.spacing(2), + padding: theme.spacing(0), + borderLeft: '1px solid', + borderColor: (theme.vars || theme).palette.divider, + }, + '&:focus-visible .focused': { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '2px', + '&:hover': { + backgroundColor: alpha(gray[300], 0.2), + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '2px', + }, + }, + }), + content: ({ theme }) => ({ + marginTop: theme.spacing(1), + padding: theme.spacing(0.5, 1), + overflow: 'clip', + '&:hover': { + backgroundColor: alpha(gray[300], 0.2), + }, + + '&.selected': { + backgroundColor: alpha(gray[300], 0.4), + '&:hover': { + backgroundColor: alpha(gray[300], 0.6), + }, + }, + ...theme.applyStyles('dark', { + '&:hover': { + backgroundColor: alpha(gray[500], 0.2), + }, + '&:focus-visible': { + '&:hover': { + backgroundColor: alpha(gray[500], 0.2), + }, + }, + '&.selected': { + backgroundColor: alpha(gray[500], 0.4), + '&:hover': { + backgroundColor: alpha(gray[500], 0.6), + }, + }, + }), + }), + }, + }, +}; diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..8cae119 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import type {} from '@mui/material/themeCssVarsAugmentation'; +import { StyledEngineProvider } from '@mui/material/styles'; +import App from './dashboard/Dashboard'; + +ReactDOM.createRoot(document.querySelector("#root")!).render( + + + + + +); diff --git a/src/shared-theme/AppTheme.tsx b/src/shared-theme/AppTheme.tsx new file mode 100644 index 0000000..a4a512c --- /dev/null +++ b/src/shared-theme/AppTheme.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import type { ThemeOptions } from '@mui/material/styles'; +import { inputsCustomizations } from './customizations/inputs'; +import { dataDisplayCustomizations } from './customizations/dataDisplay'; +import { feedbackCustomizations } from './customizations/feedback'; +import { navigationCustomizations } from './customizations/navigation'; +import { surfacesCustomizations } from './customizations/surfaces'; +import { colorSchemes, typography, shadows, shape } from './themePrimitives'; + +interface AppThemeProps { + children: React.ReactNode; + /** + * This is for the docs site. You can ignore it or remove it. + */ + disableCustomTheme?: boolean; + themeComponents?: ThemeOptions['components']; +} + +export default function AppTheme(props: AppThemeProps) { + const { children, disableCustomTheme, themeComponents } = props; + const theme = React.useMemo(() => { + return disableCustomTheme + ? {} + : createTheme({ + // For more details about CSS variables configuration, see https://mui.com/material-ui/customization/css-theme-variables/configuration/ + cssVariables: { + colorSchemeSelector: 'data-mui-color-scheme', + cssVarPrefix: 'template', + }, + colorSchemes, // Recently added in v6 for building light & dark mode app, see https://mui.com/material-ui/customization/palette/#color-schemes + typography, + shadows, + shape, + components: { + ...inputsCustomizations, + ...dataDisplayCustomizations, + ...feedbackCustomizations, + ...navigationCustomizations, + ...surfacesCustomizations, + ...themeComponents, + }, + }); + }, [disableCustomTheme, themeComponents]); + if (disableCustomTheme) { + return {children}; + } + return ( + + {children} + + ); +} diff --git a/src/shared-theme/ColorModeIconDropdown.tsx b/src/shared-theme/ColorModeIconDropdown.tsx new file mode 100644 index 0000000..3af1e07 --- /dev/null +++ b/src/shared-theme/ColorModeIconDropdown.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; +import DarkModeIcon from '@mui/icons-material/DarkModeRounded'; +import LightModeIcon from '@mui/icons-material/LightModeRounded'; +import Box from '@mui/material/Box'; +import IconButton, { IconButtonOwnProps } from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import { useColorScheme } from '@mui/material/styles'; + +export default function ColorModeIconDropdown(props: IconButtonOwnProps) { + const { mode, systemMode, setMode } = useColorScheme(); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const handleMode = (targetMode: 'system' | 'light' | 'dark') => () => { + setMode(targetMode); + handleClose(); + }; + if (!mode) { + return ( + ({ + verticalAlign: 'bottom', + display: 'inline-flex', + width: '2.25rem', + height: '2.25rem', + borderRadius: (theme.vars || theme).shape.borderRadius, + border: '1px solid', + borderColor: (theme.vars || theme).palette.divider, + })} + /> + ); + } + const resolvedMode = (systemMode || mode) as 'light' | 'dark'; + const icon = { + light: , + dark: , + }[resolvedMode]; + return ( + + + {icon} + + + + System + + + Light + + + Dark + + + + ); +} diff --git a/src/shared-theme/ColorModeSelect.tsx b/src/shared-theme/ColorModeSelect.tsx new file mode 100644 index 0000000..6e71b9b --- /dev/null +++ b/src/shared-theme/ColorModeSelect.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { useColorScheme } from '@mui/material/styles'; +import MenuItem from '@mui/material/MenuItem'; +import Select, { SelectProps } from '@mui/material/Select'; + +export default function ColorModeSelect(props: SelectProps) { + const { mode, setMode } = useColorScheme(); + if (!mode) { + return null; + } + return ( + + ); +} diff --git a/src/shared-theme/customizations/dataDisplay.tsx b/src/shared-theme/customizations/dataDisplay.tsx new file mode 100644 index 0000000..b6b2b46 --- /dev/null +++ b/src/shared-theme/customizations/dataDisplay.tsx @@ -0,0 +1,233 @@ +import { Theme, alpha, Components } from '@mui/material/styles'; +import { svgIconClasses } from '@mui/material/SvgIcon'; +import { typographyClasses } from '@mui/material/Typography'; +import { buttonBaseClasses } from '@mui/material/ButtonBase'; +import { chipClasses } from '@mui/material/Chip'; +import { iconButtonClasses } from '@mui/material/IconButton'; +import { gray, red, green } from '../themePrimitives'; + +/* eslint-disable import/prefer-default-export */ +export const dataDisplayCustomizations: Components = { + MuiList: { + styleOverrides: { + root: { + padding: '8px', + display: 'flex', + flexDirection: 'column', + gap: 0, + }, + }, + }, + MuiListItem: { + styleOverrides: { + root: ({ theme }) => ({ + [`& .${svgIconClasses.root}`]: { + width: '1rem', + height: '1rem', + color: (theme.vars || theme).palette.text.secondary, + }, + [`& .${typographyClasses.root}`]: { + fontWeight: 500, + }, + [`& .${buttonBaseClasses.root}`]: { + display: 'flex', + gap: 8, + padding: '2px 8px', + borderRadius: (theme.vars || theme).shape.borderRadius, + opacity: 0.7, + '&.Mui-selected': { + opacity: 1, + backgroundColor: alpha(theme.palette.action.selected, 0.3), + [`& .${svgIconClasses.root}`]: { + color: (theme.vars || theme).palette.text.primary, + }, + '&:focus-visible': { + backgroundColor: alpha(theme.palette.action.selected, 0.3), + }, + '&:hover': { + backgroundColor: alpha(theme.palette.action.selected, 0.5), + }, + }, + '&:focus-visible': { + backgroundColor: 'transparent', + }, + }, + }), + }, + }, + MuiListItemText: { + styleOverrides: { + primary: ({ theme }) => ({ + fontSize: theme.typography.body2.fontSize, + fontWeight: 500, + lineHeight: theme.typography.body2.lineHeight, + }), + secondary: ({ theme }) => ({ + fontSize: theme.typography.caption.fontSize, + lineHeight: theme.typography.caption.lineHeight, + }), + }, + }, + MuiListSubheader: { + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: 'transparent', + padding: '4px 8px', + fontSize: theme.typography.caption.fontSize, + fontWeight: 500, + lineHeight: theme.typography.caption.lineHeight, + }), + }, + }, + MuiListItemIcon: { + styleOverrides: { + root: { + minWidth: 0, + }, + }, + }, + MuiChip: { + defaultProps: { + size: 'small', + }, + styleOverrides: { + root: ({ theme }) => ({ + border: '1px solid', + borderRadius: '999px', + [`& .${chipClasses.label}`]: { + fontWeight: 600, + }, + variants: [ + { + props: { + color: 'default', + }, + style: { + borderColor: gray[200], + backgroundColor: gray[100], + [`& .${chipClasses.label}`]: { + color: gray[500], + }, + [`& .${chipClasses.icon}`]: { + color: gray[500], + }, + ...theme.applyStyles('dark', { + borderColor: gray[700], + backgroundColor: gray[800], + [`& .${chipClasses.label}`]: { + color: gray[300], + }, + [`& .${chipClasses.icon}`]: { + color: gray[300], + }, + }), + }, + }, + { + props: { + color: 'success', + }, + style: { + borderColor: green[200], + backgroundColor: green[50], + [`& .${chipClasses.label}`]: { + color: green[500], + }, + [`& .${chipClasses.icon}`]: { + color: green[500], + }, + ...theme.applyStyles('dark', { + borderColor: green[800], + backgroundColor: green[900], + [`& .${chipClasses.label}`]: { + color: green[300], + }, + [`& .${chipClasses.icon}`]: { + color: green[300], + }, + }), + }, + }, + { + props: { + color: 'error', + }, + style: { + borderColor: red[100], + backgroundColor: red[50], + [`& .${chipClasses.label}`]: { + color: red[500], + }, + [`& .${chipClasses.icon}`]: { + color: red[500], + }, + ...theme.applyStyles('dark', { + borderColor: red[800], + backgroundColor: red[900], + [`& .${chipClasses.label}`]: { + color: red[200], + }, + [`& .${chipClasses.icon}`]: { + color: red[300], + }, + }), + }, + }, + { + props: { size: 'small' }, + style: { + maxHeight: 20, + [`& .${chipClasses.label}`]: { + fontSize: theme.typography.caption.fontSize, + }, + [`& .${svgIconClasses.root}`]: { + fontSize: theme.typography.caption.fontSize, + }, + }, + }, + { + props: { size: 'medium' }, + style: { + [`& .${chipClasses.label}`]: { + fontSize: theme.typography.caption.fontSize, + }, + }, + }, + ], + }), + }, + }, + MuiTablePagination: { + styleOverrides: { + actions: { + display: 'flex', + gap: 8, + marginRight: 6, + [`& .${iconButtonClasses.root}`]: { + minWidth: 0, + width: 36, + height: 36, + }, + }, + }, + }, + MuiIcon: { + defaultProps: { + fontSize: 'small', + }, + styleOverrides: { + root: { + variants: [ + { + props: { + fontSize: 'small', + }, + style: { + fontSize: '1rem', + }, + }, + ], + }, + }, + }, +}; diff --git a/src/shared-theme/customizations/feedback.tsx b/src/shared-theme/customizations/feedback.tsx new file mode 100644 index 0000000..6d475c9 --- /dev/null +++ b/src/shared-theme/customizations/feedback.tsx @@ -0,0 +1,46 @@ +import { Theme, alpha, Components } from '@mui/material/styles'; +import { gray, orange } from '../themePrimitives'; + +/* eslint-disable import/prefer-default-export */ +export const feedbackCustomizations: Components = { + MuiAlert: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: 10, + backgroundColor: orange[100], + color: (theme.vars || theme).palette.text.primary, + border: `1px solid ${alpha(orange[300], 0.5)}`, + '& .MuiAlert-icon': { + color: orange[500], + }, + ...theme.applyStyles('dark', { + backgroundColor: `${alpha(orange[900], 0.5)}`, + border: `1px solid ${alpha(orange[800], 0.5)}`, + }), + }), + }, + }, + MuiDialog: { + styleOverrides: { + root: ({ theme }) => ({ + '& .MuiDialog-paper': { + borderRadius: '10px', + border: '1px solid', + borderColor: (theme.vars || theme).palette.divider, + }, + }), + }, + }, + MuiLinearProgress: { + styleOverrides: { + root: ({ theme }) => ({ + height: 8, + borderRadius: 8, + backgroundColor: gray[200], + ...theme.applyStyles('dark', { + backgroundColor: gray[800], + }), + }), + }, + }, +}; diff --git a/src/shared-theme/customizations/inputs.tsx b/src/shared-theme/customizations/inputs.tsx new file mode 100644 index 0000000..b384563 --- /dev/null +++ b/src/shared-theme/customizations/inputs.tsx @@ -0,0 +1,445 @@ +import * as React from 'react'; +import { alpha, Theme, Components } from '@mui/material/styles'; +import { outlinedInputClasses } from '@mui/material/OutlinedInput'; +import { svgIconClasses } from '@mui/material/SvgIcon'; +import { toggleButtonGroupClasses } from '@mui/material/ToggleButtonGroup'; +import { toggleButtonClasses } from '@mui/material/ToggleButton'; +import CheckBoxOutlineBlankRoundedIcon from '@mui/icons-material/CheckBoxOutlineBlankRounded'; +import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; +import RemoveRoundedIcon from '@mui/icons-material/RemoveRounded'; +import { gray, brand } from '../themePrimitives'; + +/* eslint-disable import/prefer-default-export */ +export const inputsCustomizations: Components = { + MuiButtonBase: { + defaultProps: { + disableTouchRipple: true, + disableRipple: true, + }, + styleOverrides: { + root: ({ theme }) => ({ + boxSizing: 'border-box', + transition: 'all 100ms ease-in', + '&:focus-visible': { + outline: `3px solid ${alpha(theme.palette.primary.main, 0.5)}`, + outlineOffset: '2px', + }, + }), + }, + }, + MuiButton: { + styleOverrides: { + root: ({ theme }) => ({ + boxShadow: 'none', + borderRadius: (theme.vars || theme).shape.borderRadius, + textTransform: 'none', + variants: [ + { + props: { + size: 'small', + }, + style: { + height: '2.25rem', + padding: '8px 12px', + }, + }, + { + props: { + size: 'medium', + }, + style: { + height: '2.5rem', // 40px + }, + }, + { + props: { + color: 'primary', + variant: 'contained', + }, + style: { + color: 'white', + backgroundColor: gray[900], + backgroundImage: `linear-gradient(to bottom, ${gray[700]}, ${gray[800]})`, + boxShadow: `inset 0 1px 0 ${gray[600]}, inset 0 -1px 0 1px hsl(220, 0%, 0%)`, + border: `1px solid ${gray[700]}`, + '&:hover': { + backgroundImage: 'none', + backgroundColor: gray[700], + boxShadow: 'none', + }, + '&:active': { + backgroundColor: gray[800], + }, + ...theme.applyStyles('dark', { + color: 'black', + backgroundColor: gray[50], + backgroundImage: `linear-gradient(to bottom, ${gray[100]}, ${gray[50]})`, + boxShadow: 'inset 0 -1px 0 hsl(220, 30%, 80%)', + border: `1px solid ${gray[50]}`, + '&:hover': { + backgroundImage: 'none', + backgroundColor: gray[300], + boxShadow: 'none', + }, + '&:active': { + backgroundColor: gray[400], + }, + }), + }, + }, + { + props: { + color: 'secondary', + variant: 'contained', + }, + style: { + color: 'white', + backgroundColor: brand[300], + backgroundImage: `linear-gradient(to bottom, ${alpha(brand[400], 0.8)}, ${brand[500]})`, + boxShadow: `inset 0 2px 0 ${alpha(brand[200], 0.2)}, inset 0 -2px 0 ${alpha(brand[700], 0.4)}`, + border: `1px solid ${brand[500]}`, + '&:hover': { + backgroundColor: brand[700], + boxShadow: 'none', + }, + '&:active': { + backgroundColor: brand[700], + backgroundImage: 'none', + }, + }, + }, + { + props: { + variant: 'outlined', + }, + style: { + color: (theme.vars || theme).palette.text.primary, + border: '1px solid', + borderColor: gray[200], + backgroundColor: alpha(gray[50], 0.3), + '&:hover': { + backgroundColor: gray[100], + borderColor: gray[300], + }, + '&:active': { + backgroundColor: gray[200], + }, + ...theme.applyStyles('dark', { + backgroundColor: gray[800], + borderColor: gray[700], + + '&:hover': { + backgroundColor: gray[900], + borderColor: gray[600], + }, + '&:active': { + backgroundColor: gray[900], + }, + }), + }, + }, + { + props: { + color: 'secondary', + variant: 'outlined', + }, + style: { + color: brand[700], + border: '1px solid', + borderColor: brand[200], + backgroundColor: brand[50], + '&:hover': { + backgroundColor: brand[100], + borderColor: brand[400], + }, + '&:active': { + backgroundColor: alpha(brand[200], 0.7), + }, + ...theme.applyStyles('dark', { + color: brand[50], + border: '1px solid', + borderColor: brand[900], + backgroundColor: alpha(brand[900], 0.3), + '&:hover': { + borderColor: brand[700], + backgroundColor: alpha(brand[900], 0.6), + }, + '&:active': { + backgroundColor: alpha(brand[900], 0.5), + }, + }), + }, + }, + { + props: { + variant: 'text', + }, + style: { + color: gray[600], + '&:hover': { + backgroundColor: gray[100], + }, + '&:active': { + backgroundColor: gray[200], + }, + ...theme.applyStyles('dark', { + color: gray[50], + '&:hover': { + backgroundColor: gray[700], + }, + '&:active': { + backgroundColor: alpha(gray[700], 0.7), + }, + }), + }, + }, + { + props: { + color: 'secondary', + variant: 'text', + }, + style: { + color: brand[700], + '&:hover': { + backgroundColor: alpha(brand[100], 0.5), + }, + '&:active': { + backgroundColor: alpha(brand[200], 0.7), + }, + ...theme.applyStyles('dark', { + color: brand[100], + '&:hover': { + backgroundColor: alpha(brand[900], 0.5), + }, + '&:active': { + backgroundColor: alpha(brand[900], 0.3), + }, + }), + }, + }, + ], + }), + }, + }, + MuiIconButton: { + styleOverrides: { + root: ({ theme }) => ({ + boxShadow: 'none', + borderRadius: (theme.vars || theme).shape.borderRadius, + textTransform: 'none', + fontWeight: theme.typography.fontWeightMedium, + letterSpacing: 0, + color: (theme.vars || theme).palette.text.primary, + border: '1px solid ', + borderColor: gray[200], + backgroundColor: alpha(gray[50], 0.3), + '&:hover': { + backgroundColor: gray[100], + borderColor: gray[300], + }, + '&:active': { + backgroundColor: gray[200], + }, + ...theme.applyStyles('dark', { + backgroundColor: gray[800], + borderColor: gray[700], + '&:hover': { + backgroundColor: gray[900], + borderColor: gray[600], + }, + '&:active': { + backgroundColor: gray[900], + }, + }), + variants: [ + { + props: { + size: 'small', + }, + style: { + width: '2.25rem', + height: '2.25rem', + padding: '0.25rem', + [`& .${svgIconClasses.root}`]: { fontSize: '1rem' }, + }, + }, + { + props: { + size: 'medium', + }, + style: { + width: '2.5rem', + height: '2.5rem', + }, + }, + ], + }), + }, + }, + MuiToggleButtonGroup: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: '10px', + boxShadow: `0 4px 16px ${alpha(gray[400], 0.2)}`, + [`& .${toggleButtonGroupClasses.selected}`]: { + color: brand[500], + }, + ...theme.applyStyles('dark', { + [`& .${toggleButtonGroupClasses.selected}`]: { + color: '#fff', + }, + boxShadow: `0 4px 16px ${alpha(brand[700], 0.5)}`, + }), + }), + }, + }, + MuiToggleButton: { + styleOverrides: { + root: ({ theme }) => ({ + padding: '12px 16px', + textTransform: 'none', + borderRadius: '10px', + fontWeight: 500, + ...theme.applyStyles('dark', { + color: gray[400], + boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5)', + [`&.${toggleButtonClasses.selected}`]: { + color: brand[300], + }, + }), + }), + }, + }, + MuiCheckbox: { + defaultProps: { + disableRipple: true, + icon: ( + + ), + checkedIcon: , + indeterminateIcon: , + }, + styleOverrides: { + root: ({ theme }) => ({ + margin: 10, + height: 16, + width: 16, + borderRadius: 5, + border: '1px solid ', + borderColor: alpha(gray[300], 0.8), + boxShadow: '0 0 0 1.5px hsla(210, 0%, 0%, 0.04) inset', + backgroundColor: alpha(gray[100], 0.4), + transition: 'border-color, background-color, 120ms ease-in', + '&:hover': { + borderColor: brand[300], + }, + '&.Mui-focusVisible': { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '2px', + borderColor: brand[400], + }, + '&.Mui-checked': { + color: 'white', + backgroundColor: brand[500], + borderColor: brand[500], + boxShadow: `none`, + '&:hover': { + backgroundColor: brand[600], + }, + }, + ...theme.applyStyles('dark', { + borderColor: alpha(gray[700], 0.8), + boxShadow: '0 0 0 1.5px hsl(210, 0%, 0%) inset', + backgroundColor: alpha(gray[900], 0.8), + '&:hover': { + borderColor: brand[300], + }, + '&.Mui-focusVisible': { + borderColor: brand[400], + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '2px', + }, + }), + }), + }, + }, + MuiInputBase: { + styleOverrides: { + root: { + border: 'none', + }, + input: { + '&::placeholder': { + opacity: 0.7, + color: gray[500], + }, + }, + }, + }, + MuiOutlinedInput: { + styleOverrides: { + input: { + padding: 0, + }, + root: ({ theme }) => ({ + padding: '8px 12px', + color: (theme.vars || theme).palette.text.primary, + borderRadius: (theme.vars || theme).shape.borderRadius, + border: `1px solid ${(theme.vars || theme).palette.divider}`, + backgroundColor: (theme.vars || theme).palette.background.default, + transition: 'border 120ms ease-in', + '&:hover': { + borderColor: gray[400], + }, + [`&.${outlinedInputClasses.focused}`]: { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + borderColor: brand[400], + }, + ...theme.applyStyles('dark', { + '&:hover': { + borderColor: gray[500], + }, + }), + variants: [ + { + props: { + size: 'small', + }, + style: { + height: '2.25rem', + }, + }, + { + props: { + size: 'medium', + }, + style: { + height: '2.5rem', + }, + }, + ], + }), + notchedOutline: { + border: 'none', + }, + }, + }, + MuiInputAdornment: { + styleOverrides: { + root: ({ theme }) => ({ + color: (theme.vars || theme).palette.grey[500], + ...theme.applyStyles('dark', { + color: (theme.vars || theme).palette.grey[400], + }), + }), + }, + }, + MuiFormLabel: { + styleOverrides: { + root: ({ theme }) => ({ + typography: theme.typography.caption, + marginBottom: 8, + }), + }, + }, +}; diff --git a/src/shared-theme/customizations/navigation.tsx b/src/shared-theme/customizations/navigation.tsx new file mode 100644 index 0000000..3cb9713 --- /dev/null +++ b/src/shared-theme/customizations/navigation.tsx @@ -0,0 +1,279 @@ +import * as React from 'react'; +import { Theme, alpha, Components } from '@mui/material/styles'; +import { SvgIconProps } from '@mui/material/SvgIcon'; +import { buttonBaseClasses } from '@mui/material/ButtonBase'; +import { dividerClasses } from '@mui/material/Divider'; +import { menuItemClasses } from '@mui/material/MenuItem'; +import { selectClasses } from '@mui/material/Select'; +import { tabClasses } from '@mui/material/Tab'; +import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded'; +import { gray, brand } from '../themePrimitives'; + +/* eslint-disable import/prefer-default-export */ +export const navigationCustomizations: Components = { + MuiMenuItem: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: (theme.vars || theme).shape.borderRadius, + padding: '6px 8px', + [`&.${menuItemClasses.focusVisible}`]: { + backgroundColor: 'transparent', + }, + [`&.${menuItemClasses.selected}`]: { + [`&.${menuItemClasses.focusVisible}`]: { + backgroundColor: alpha(theme.palette.action.selected, 0.3), + }, + }, + }), + }, + }, + MuiMenu: { + styleOverrides: { + list: { + gap: '0px', + [`&.${dividerClasses.root}`]: { + margin: '0 -8px', + }, + }, + paper: ({ theme }) => ({ + marginTop: '4px', + borderRadius: (theme.vars || theme).shape.borderRadius, + border: `1px solid ${(theme.vars || theme).palette.divider}`, + backgroundImage: 'none', + background: 'hsl(0, 0%, 100%)', + boxShadow: + 'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px', + [`& .${buttonBaseClasses.root}`]: { + '&.Mui-selected': { + backgroundColor: alpha(theme.palette.action.selected, 0.3), + }, + }, + ...theme.applyStyles('dark', { + background: gray[900], + boxShadow: + 'hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px', + }), + }), + }, + }, + MuiSelect: { + defaultProps: { + IconComponent: React.forwardRef((props, ref) => ( + + )), + }, + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: (theme.vars || theme).shape.borderRadius, + border: '1px solid', + borderColor: gray[200], + backgroundColor: (theme.vars || theme).palette.background.paper, + boxShadow: `inset 0 1px 0 1px hsla(220, 0%, 100%, 0.6), inset 0 -1px 0 1px hsla(220, 35%, 90%, 0.5)`, + '&:hover': { + borderColor: gray[300], + backgroundColor: (theme.vars || theme).palette.background.paper, + boxShadow: 'none', + }, + [`&.${selectClasses.focused}`]: { + outlineOffset: 0, + borderColor: gray[400], + }, + '&:before, &:after': { + display: 'none', + }, + + ...theme.applyStyles('dark', { + borderRadius: (theme.vars || theme).shape.borderRadius, + borderColor: gray[700], + backgroundColor: (theme.vars || theme).palette.background.paper, + boxShadow: `inset 0 1px 0 1px ${alpha(gray[700], 0.15)}, inset 0 -1px 0 1px hsla(220, 0%, 0%, 0.7)`, + '&:hover': { + borderColor: alpha(gray[700], 0.7), + backgroundColor: (theme.vars || theme).palette.background.paper, + boxShadow: 'none', + }, + [`&.${selectClasses.focused}`]: { + outlineOffset: 0, + borderColor: gray[900], + }, + '&:before, &:after': { + display: 'none', + }, + }), + }), + select: ({ theme }) => ({ + display: 'flex', + alignItems: 'center', + ...theme.applyStyles('dark', { + display: 'flex', + alignItems: 'center', + '&:focus-visible': { + backgroundColor: gray[900], + }, + }), + }), + }, + }, + MuiLink: { + defaultProps: { + underline: 'none', + }, + styleOverrides: { + root: ({ theme }) => ({ + color: (theme.vars || theme).palette.text.primary, + fontWeight: 500, + position: 'relative', + textDecoration: 'none', + width: 'fit-content', + '&::before': { + content: '""', + position: 'absolute', + width: '100%', + height: '1px', + bottom: 0, + left: 0, + backgroundColor: (theme.vars || theme).palette.text.secondary, + opacity: 0.3, + transition: 'width 0.3s ease, opacity 0.3s ease', + }, + '&:hover::before': { + width: 0, + }, + '&:focus-visible': { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: '4px', + borderRadius: '2px', + }, + }), + }, + }, + MuiDrawer: { + styleOverrides: { + paper: ({ theme }) => ({ + backgroundColor: (theme.vars || theme).palette.background.default, + }), + }, + }, + MuiPaginationItem: { + styleOverrides: { + root: ({ theme }) => ({ + '&.Mui-selected': { + color: 'white', + backgroundColor: (theme.vars || theme).palette.grey[900], + }, + ...theme.applyStyles('dark', { + '&.Mui-selected': { + color: 'black', + backgroundColor: (theme.vars || theme).palette.grey[50], + }, + }), + }), + }, + }, + MuiTabs: { + styleOverrides: { + root: { minHeight: 'fit-content' }, + indicator: ({ theme }) => ({ + backgroundColor: (theme.vars || theme).palette.grey[800], + ...theme.applyStyles('dark', { + backgroundColor: (theme.vars || theme).palette.grey[200], + }), + }), + }, + }, + MuiTab: { + styleOverrides: { + root: ({ theme }) => ({ + padding: '6px 8px', + marginBottom: '8px', + textTransform: 'none', + minWidth: 'fit-content', + minHeight: 'fit-content', + color: (theme.vars || theme).palette.text.secondary, + borderRadius: (theme.vars || theme).shape.borderRadius, + border: '1px solid', + borderColor: 'transparent', + ':hover': { + color: (theme.vars || theme).palette.text.primary, + backgroundColor: gray[100], + borderColor: gray[200], + }, + [`&.${tabClasses.selected}`]: { + color: gray[900], + }, + ...theme.applyStyles('dark', { + ':hover': { + color: (theme.vars || theme).palette.text.primary, + backgroundColor: gray[800], + borderColor: gray[700], + }, + [`&.${tabClasses.selected}`]: { + color: '#fff', + }, + }), + }), + }, + }, + MuiStepConnector: { + styleOverrides: { + line: ({ theme }) => ({ + borderTop: '1px solid', + borderColor: (theme.vars || theme).palette.divider, + flex: 1, + borderRadius: '99px', + }), + }, + }, + MuiStepIcon: { + styleOverrides: { + root: ({ theme }) => ({ + color: 'transparent', + border: `1px solid ${gray[400]}`, + width: 12, + height: 12, + borderRadius: '50%', + '& text': { + display: 'none', + }, + '&.Mui-active': { + border: 'none', + color: (theme.vars || theme).palette.primary.main, + }, + '&.Mui-completed': { + border: 'none', + color: (theme.vars || theme).palette.success.main, + }, + ...theme.applyStyles('dark', { + border: `1px solid ${gray[700]}`, + '&.Mui-active': { + border: 'none', + color: (theme.vars || theme).palette.primary.light, + }, + '&.Mui-completed': { + border: 'none', + color: (theme.vars || theme).palette.success.light, + }, + }), + variants: [ + { + props: { completed: true }, + style: { + width: 12, + height: 12, + }, + }, + ], + }), + }, + }, + MuiStepLabel: { + styleOverrides: { + label: ({ theme }) => ({ + '&.Mui-completed': { + opacity: 0.6, + ...theme.applyStyles('dark', { opacity: 0.5 }), + }, + }), + }, + }, +}; diff --git a/src/shared-theme/customizations/surfaces.ts b/src/shared-theme/customizations/surfaces.ts new file mode 100644 index 0000000..f47a6d8 --- /dev/null +++ b/src/shared-theme/customizations/surfaces.ts @@ -0,0 +1,113 @@ +import { alpha, Theme, Components } from '@mui/material/styles'; +import { gray } from '../themePrimitives'; + +/* eslint-disable import/prefer-default-export */ +export const surfacesCustomizations: Components = { + MuiAccordion: { + defaultProps: { + elevation: 0, + disableGutters: true, + }, + styleOverrides: { + root: ({ theme }) => ({ + padding: 4, + overflow: 'clip', + backgroundColor: (theme.vars || theme).palette.background.default, + border: '1px solid', + borderColor: (theme.vars || theme).palette.divider, + ':before': { + backgroundColor: 'transparent', + }, + '&:not(:last-of-type)': { + borderBottom: 'none', + }, + '&:first-of-type': { + borderTopLeftRadius: (theme.vars || theme).shape.borderRadius, + borderTopRightRadius: (theme.vars || theme).shape.borderRadius, + }, + '&:last-of-type': { + borderBottomLeftRadius: (theme.vars || theme).shape.borderRadius, + borderBottomRightRadius: (theme.vars || theme).shape.borderRadius, + }, + }), + }, + }, + MuiAccordionSummary: { + styleOverrides: { + root: ({ theme }) => ({ + border: 'none', + borderRadius: 8, + '&:hover': { backgroundColor: gray[50] }, + '&:focus-visible': { backgroundColor: 'transparent' }, + ...theme.applyStyles('dark', { + '&:hover': { backgroundColor: gray[800] }, + }), + }), + }, + }, + MuiAccordionDetails: { + styleOverrides: { + root: { mb: 20, border: 'none' }, + }, + }, + MuiPaper: { + defaultProps: { + elevation: 0, + }, + }, + MuiCard: { + styleOverrides: { + root: ({ theme }) => { + return { + padding: 16, + gap: 16, + transition: 'all 100ms ease', + backgroundColor: gray[50], + borderRadius: (theme.vars || theme).shape.borderRadius, + border: `1px solid ${(theme.vars || theme).palette.divider}`, + boxShadow: 'none', + ...theme.applyStyles('dark', { + backgroundColor: gray[800], + }), + variants: [ + { + props: { + variant: 'outlined', + }, + style: { + border: `1px solid ${(theme.vars || theme).palette.divider}`, + boxShadow: 'none', + background: 'hsl(0, 0%, 100%)', + ...theme.applyStyles('dark', { + background: alpha(gray[900], 0.4), + }), + }, + }, + ], + }; + }, + }, + }, + MuiCardContent: { + styleOverrides: { + root: { + padding: 0, + '&:last-child': { paddingBottom: 0 }, + }, + }, + }, + MuiCardHeader: { + styleOverrides: { + root: { + padding: 0, + }, + }, + }, + MuiCardActions: { + styleOverrides: { + root: { + padding: 0, + }, + }, + }, +}; diff --git a/src/shared-theme/themePrimitives.ts b/src/shared-theme/themePrimitives.ts new file mode 100644 index 0000000..a12eb21 --- /dev/null +++ b/src/shared-theme/themePrimitives.ts @@ -0,0 +1,403 @@ +import { createTheme, alpha, PaletteMode, Shadows } from '@mui/material/styles'; + +declare module '@mui/material/Paper' { + interface PaperPropsVariantOverrides { + highlighted: true; + } +} +declare module '@mui/material/styles/createPalette' { + interface ColorRange { + 50: string; + 100: string; + 200: string; + 300: string; + 400: string; + 500: string; + 600: string; + 700: string; + 800: string; + 900: string; + } + + interface PaletteColor extends ColorRange {} + + interface Palette { + baseShadow: string; + } +} + +const defaultTheme = createTheme(); + +const customShadows: Shadows = [...defaultTheme.shadows]; + +export const brand = { + 50: 'hsl(210, 100%, 95%)', + 100: 'hsl(210, 100%, 92%)', + 200: 'hsl(210, 100%, 80%)', + 300: 'hsl(210, 100%, 65%)', + 400: 'hsl(210, 98%, 48%)', + 500: 'hsl(210, 98%, 42%)', + 600: 'hsl(210, 98%, 55%)', + 700: 'hsl(210, 100%, 35%)', + 800: 'hsl(210, 100%, 16%)', + 900: 'hsl(210, 100%, 21%)', +}; + +export const gray = { + 50: 'hsl(220, 35%, 97%)', + 100: 'hsl(220, 30%, 94%)', + 200: 'hsl(220, 20%, 88%)', + 300: 'hsl(220, 20%, 80%)', + 400: 'hsl(220, 20%, 65%)', + 500: 'hsl(220, 20%, 42%)', + 600: 'hsl(220, 20%, 35%)', + 700: 'hsl(220, 20%, 25%)', + 800: 'hsl(220, 30%, 6%)', + 900: 'hsl(220, 35%, 3%)', +}; + +export const green = { + 50: 'hsl(120, 80%, 98%)', + 100: 'hsl(120, 75%, 94%)', + 200: 'hsl(120, 75%, 87%)', + 300: 'hsl(120, 61%, 77%)', + 400: 'hsl(120, 44%, 53%)', + 500: 'hsl(120, 59%, 30%)', + 600: 'hsl(120, 70%, 25%)', + 700: 'hsl(120, 75%, 16%)', + 800: 'hsl(120, 84%, 10%)', + 900: 'hsl(120, 87%, 6%)', +}; + +export const orange = { + 50: 'hsl(45, 100%, 97%)', + 100: 'hsl(45, 92%, 90%)', + 200: 'hsl(45, 94%, 80%)', + 300: 'hsl(45, 90%, 65%)', + 400: 'hsl(45, 90%, 40%)', + 500: 'hsl(45, 90%, 35%)', + 600: 'hsl(45, 91%, 25%)', + 700: 'hsl(45, 94%, 20%)', + 800: 'hsl(45, 95%, 16%)', + 900: 'hsl(45, 93%, 12%)', +}; + +export const red = { + 50: 'hsl(0, 100%, 97%)', + 100: 'hsl(0, 92%, 90%)', + 200: 'hsl(0, 94%, 80%)', + 300: 'hsl(0, 90%, 65%)', + 400: 'hsl(0, 90%, 40%)', + 500: 'hsl(0, 90%, 30%)', + 600: 'hsl(0, 91%, 25%)', + 700: 'hsl(0, 94%, 18%)', + 800: 'hsl(0, 95%, 12%)', + 900: 'hsl(0, 93%, 6%)', +}; + +export const getDesignTokens = (mode: PaletteMode) => { + customShadows[1] = + mode === 'dark' + ? 'hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px' + : 'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px'; + + return { + palette: { + mode, + primary: { + light: brand[200], + main: brand[400], + dark: brand[700], + contrastText: brand[50], + ...(mode === 'dark' && { + contrastText: brand[50], + light: brand[300], + main: brand[400], + dark: brand[700], + }), + }, + info: { + light: brand[100], + main: brand[300], + dark: brand[600], + contrastText: gray[50], + ...(mode === 'dark' && { + contrastText: brand[300], + light: brand[500], + main: brand[700], + dark: brand[900], + }), + }, + warning: { + light: orange[300], + main: orange[400], + dark: orange[800], + ...(mode === 'dark' && { + light: orange[400], + main: orange[500], + dark: orange[700], + }), + }, + error: { + light: red[300], + main: red[400], + dark: red[800], + ...(mode === 'dark' && { + light: red[400], + main: red[500], + dark: red[700], + }), + }, + success: { + light: green[300], + main: green[400], + dark: green[800], + ...(mode === 'dark' && { + light: green[400], + main: green[500], + dark: green[700], + }), + }, + grey: { + ...gray, + }, + divider: mode === 'dark' ? alpha(gray[700], 0.6) : alpha(gray[300], 0.4), + background: { + default: 'hsl(0, 0%, 99%)', + paper: 'hsl(220, 35%, 97%)', + ...(mode === 'dark' && { default: gray[900], paper: 'hsl(220, 30%, 7%)' }), + }, + text: { + primary: gray[800], + secondary: gray[600], + warning: orange[400], + ...(mode === 'dark' && { primary: 'hsl(0, 0%, 100%)', secondary: gray[400] }), + }, + action: { + hover: alpha(gray[200], 0.2), + selected: `${alpha(gray[200], 0.3)}`, + ...(mode === 'dark' && { + hover: alpha(gray[600], 0.2), + selected: alpha(gray[600], 0.3), + }), + }, + }, + typography: { + fontFamily: 'Inter, sans-serif', + h1: { + fontSize: defaultTheme.typography.pxToRem(48), + fontWeight: 600, + lineHeight: 1.2, + letterSpacing: -0.5, + }, + h2: { + fontSize: defaultTheme.typography.pxToRem(36), + fontWeight: 600, + lineHeight: 1.2, + }, + h3: { + fontSize: defaultTheme.typography.pxToRem(30), + lineHeight: 1.2, + }, + h4: { + fontSize: defaultTheme.typography.pxToRem(24), + fontWeight: 600, + lineHeight: 1.5, + }, + h5: { + fontSize: defaultTheme.typography.pxToRem(20), + fontWeight: 600, + }, + h6: { + fontSize: defaultTheme.typography.pxToRem(18), + fontWeight: 600, + }, + subtitle1: { + fontSize: defaultTheme.typography.pxToRem(18), + }, + subtitle2: { + fontSize: defaultTheme.typography.pxToRem(14), + fontWeight: 500, + }, + body1: { + fontSize: defaultTheme.typography.pxToRem(14), + }, + body2: { + fontSize: defaultTheme.typography.pxToRem(14), + fontWeight: 400, + }, + caption: { + fontSize: defaultTheme.typography.pxToRem(12), + fontWeight: 400, + }, + }, + shape: { + borderRadius: 8, + }, + shadows: customShadows, + }; +}; + +export const colorSchemes = { + light: { + palette: { + primary: { + light: brand[200], + main: brand[400], + dark: brand[700], + contrastText: brand[50], + }, + info: { + light: brand[100], + main: brand[300], + dark: brand[600], + contrastText: gray[50], + }, + warning: { + light: orange[300], + main: orange[400], + dark: orange[800], + }, + error: { + light: red[300], + main: red[400], + dark: red[800], + }, + success: { + light: green[300], + main: green[400], + dark: green[800], + }, + grey: { + ...gray, + }, + divider: alpha(gray[300], 0.4), + background: { + default: 'hsl(0, 0%, 99%)', + paper: 'hsl(220, 35%, 97%)', + }, + text: { + primary: gray[800], + secondary: gray[600], + warning: orange[400], + }, + action: { + hover: alpha(gray[200], 0.2), + selected: `${alpha(gray[200], 0.3)}`, + }, + baseShadow: + 'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px', + }, + }, + dark: { + palette: { + primary: { + contrastText: brand[50], + light: brand[300], + main: brand[400], + dark: brand[700], + }, + info: { + contrastText: brand[300], + light: brand[500], + main: brand[700], + dark: brand[900], + }, + warning: { + light: orange[400], + main: orange[500], + dark: orange[700], + }, + error: { + light: red[400], + main: red[500], + dark: red[700], + }, + success: { + light: green[400], + main: green[500], + dark: green[700], + }, + grey: { + ...gray, + }, + divider: alpha(gray[700], 0.6), + background: { + default: gray[900], + paper: 'hsl(220, 30%, 7%)', + }, + text: { + primary: 'hsl(0, 0%, 100%)', + secondary: gray[400], + }, + action: { + hover: alpha(gray[600], 0.2), + selected: alpha(gray[600], 0.3), + }, + baseShadow: + 'hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px', + }, + }, +}; + +export const typography = { + fontFamily: 'Inter, sans-serif', + h1: { + fontSize: defaultTheme.typography.pxToRem(48), + fontWeight: 600, + lineHeight: 1.2, + letterSpacing: -0.5, + }, + h2: { + fontSize: defaultTheme.typography.pxToRem(36), + fontWeight: 600, + lineHeight: 1.2, + }, + h3: { + fontSize: defaultTheme.typography.pxToRem(30), + lineHeight: 1.2, + }, + h4: { + fontSize: defaultTheme.typography.pxToRem(24), + fontWeight: 600, + lineHeight: 1.5, + }, + h5: { + fontSize: defaultTheme.typography.pxToRem(20), + fontWeight: 600, + }, + h6: { + fontSize: defaultTheme.typography.pxToRem(18), + fontWeight: 600, + }, + subtitle1: { + fontSize: defaultTheme.typography.pxToRem(18), + }, + subtitle2: { + fontSize: defaultTheme.typography.pxToRem(14), + fontWeight: 500, + }, + body1: { + fontSize: defaultTheme.typography.pxToRem(14), + }, + body2: { + fontSize: defaultTheme.typography.pxToRem(14), + fontWeight: 400, + }, + caption: { + fontSize: defaultTheme.typography.pxToRem(12), + fontWeight: 400, + }, +}; + +export const shape = { + borderRadius: 8, +}; + +// @ts-ignore +const defaultShadows: Shadows = [ + 'none', + 'var(--template-palette-baseShadow)', + ...defaultTheme.shadows.slice(2), +]; +export const shadows = defaultShadows; diff --git a/src/theme.tsx b/src/theme.tsx new file mode 100644 index 0000000..7ba9892 --- /dev/null +++ b/src/theme.tsx @@ -0,0 +1,20 @@ +import { createTheme } from '@mui/material/styles'; +import { red } from '@mui/material/colors'; + +// A custom theme for this app +const theme = createTheme({ + cssVariables: true, + palette: { + primary: { + main: '#556cd6', + }, + secondary: { + main: '#19857b', + }, + error: { + main: red.A400, + }, + }, +}); + +export default theme; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3d0a51a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..9d31e2a --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..4a5def4 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +});