├── .gitignore ├── LICENSE ├── README.md ├── backend └── index.ts ├── eslint.config.js ├── frontend ├── components │ ├── avatar │ │ └── index.tsx │ ├── badge │ │ └── index.tsx │ ├── button │ │ └── index.tsx │ ├── card │ │ └── simple-card.tsx │ ├── checkbox │ │ └── index.tsx │ ├── drawer │ │ └── index.tsx │ ├── input │ │ ├── base-input.tsx │ │ └── input-with-icons.tsx │ ├── pill │ │ ├── base.tsx │ │ └── colorful.tsx │ ├── sidebar │ │ ├── navigation-groups.tsx │ │ ├── navigation-items.tsx │ │ └── sidebar-layouts.tsx │ ├── table │ │ └── index.tsx │ ├── toggle │ │ └── index.tsx │ └── tooltip │ │ └── index.tsx ├── icons │ ├── bell-icon.tsx │ ├── checkbox-icon.tsx │ ├── chevron-down-icon.tsx │ ├── chevron-right-icon.tsx │ ├── clipboard-icon.tsx │ ├── close-circle-icon.tsx │ ├── dashboard-icon.tsx │ ├── dollar-icon.tsx │ ├── dots-vertical-icon.tsx │ ├── double-chevrons-left-icon.tsx │ ├── double-chevrons-right-icon.tsx │ ├── download-icon.tsx │ ├── edit-icon.tsx │ ├── eye-icon.tsx │ ├── file-icon.tsx │ ├── help-circle-icon.tsx │ ├── home-icon.tsx │ ├── inbox-icon.tsx │ ├── log-out-icon.tsx │ ├── logos │ │ └── product-logo.tsx │ ├── menu-icon.tsx │ ├── percentage-icon.tsx │ ├── plus-icon.tsx │ ├── search-icon.tsx │ └── settings-icon.tsx ├── pages │ └── dashboard.tsx ├── patterns │ ├── fixed-width-primary-sidebar.tsx │ ├── primary-sidebar-bottom-group.tsx │ ├── primary-sidebar-create-button.tsx │ ├── primary-sidebar-heading.tsx │ ├── primary-sidebar-primary-group.tsx │ ├── primary-sidebar-secondary-group.tsx │ └── topbar-for-sidebar-content-layout.tsx └── utils │ ├── merge-classnames.tsx │ └── variations-components.tsx ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── App.tsx ├── assets │ └── react.svg ├── index.css ├── main.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 developerway 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Study project for the article "Initial load performance for React developers: investigative deep dive" 2 | 3 | Article: [Initial load performance for React developers: investigative deep dive](http://developerway.com/posts/initial-load-performance) 4 | 5 | ## How to run the project 6 | 7 | 1. Clone the repository 8 | 2. Run `npm install` 9 | 3. Run `npm run build` 10 | 4. Run `npm run start` 11 | -------------------------------------------------------------------------------- /backend/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server"; 2 | import { Hono } from "hono"; 3 | import path from "path"; 4 | import * as fs from "node:fs"; 5 | 6 | const app = new Hono(); 7 | 8 | const etag = 'W/"12345-67890"'; 9 | 10 | const dist = path.join(process.cwd(), "dist"); 11 | 12 | app.get("/assets/*", async (c) => { 13 | const resourcePath = path.join(dist, c.req.path); 14 | const resourceContent = fs.readFileSync(resourcePath).toString(); 15 | 16 | // Check browser cache headers 17 | const ifNoneMatchHeader = c.req.header("If-None-Match"); 18 | 19 | const lastMonth = new Date(); 20 | lastMonth.setMonth(lastMonth.getMonth() - 1); 21 | 22 | // Set cache headers 23 | c.header("Cache-Control", "max-age=0,must-revalidate"); 24 | c.header("ETag", `"${etag}"`); 25 | 26 | // Set MIME type 27 | if (resourcePath.endsWith(".css")) { 28 | c.header("Content-Type", "text/css"); 29 | } else if (resourcePath.endsWith(".js")) { 30 | c.header("Content-Type", "application/javascript"); 31 | } 32 | 33 | if (ifNoneMatchHeader === `"${etag}"`) { 34 | c.status(304); 35 | return c.body(""); 36 | } 37 | 38 | return c.body(resourceContent, 200); 39 | }); 40 | 41 | export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); 42 | 43 | app.get("/", async (c) => { 44 | const html = fs.readFileSync(path.join(dist, "index.html")).toString(); 45 | 46 | // await sleep(500); 47 | 48 | return c.html(html); 49 | }); 50 | 51 | const port = 3000; 52 | console.log(`Server is running on http://localhost:${port}`); 53 | 54 | serve({ 55 | fetch: app.fetch, 56 | port, 57 | }); 58 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default tseslint.config( 8 | { ignores: ["dist"] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ["**/*.{ts,tsx}"], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | "react-hooks": reactHooks, 18 | "react-refresh": reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | "react-refresh/only-export-components": [ 23 | "warn", 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ); 29 | -------------------------------------------------------------------------------- /frontend/components/avatar/index.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, ReactNode } from "react"; 2 | 3 | import { merge } from "@fe/utils/merge-classnames"; 4 | 5 | type AvatarImageProps = { 6 | src: string; 7 | alt: string; 8 | className?: string; 9 | }; 10 | 11 | export const AvatarImage = ({ 12 | src, 13 | alt, 14 | className, 15 | ...props 16 | }: AvatarImageProps) => { 17 | return {alt}; 18 | }; 19 | 20 | type AvatarFallbackProps = { 21 | className?: string; 22 | children?: ReactNode; 23 | }; 24 | 25 | export const AvatarFallback = ({ 26 | className, 27 | children, 28 | ...props 29 | }: AvatarFallbackProps & HTMLAttributes) => { 30 | return ( 31 |
38 | {children} 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /frontend/components/badge/index.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | import { merge } from "@fe/utils/merge-classnames"; 4 | 5 | export type BadgeProps = { 6 | className?: string; 7 | size?: "default" | "small" | "xsmall"; 8 | text?: string; 9 | }; 10 | 11 | export const BadgeBase = ({ 12 | className, 13 | size = "default", 14 | text, 15 | ...rest 16 | }: BadgeProps & HTMLAttributes) => { 17 | return ( 18 | 28 | {text} 29 | 30 | ); 31 | }; 32 | 33 | export const Badge = ({ 34 | className, 35 | ...props 36 | }: BadgeProps & HTMLAttributes) => { 37 | return ( 38 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/components/button/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ButtonHTMLAttributes, ReactElement, ReactNode } from "react"; 2 | 3 | import { merge } from "@fe/utils/merge-classnames"; 4 | 5 | const primaryLightCls = 6 | "bg-blinkNeutral900 hover:bg-blinkGray900 text-blinkGray50 disabled:bg-blinkNeutral300 disabled:text-blinkNeutral50 focus-visible:ring-offset-2"; 7 | const primaryDarkCls = 8 | "dark:bg-blinkGray50 dark:hover:bg-blinkGray400 dark:text-blinkGreen900 dark:disabled:bg-blinkGray400 dark:disabled:text-blinkNeutral400"; 9 | 10 | const secondaryLightCls = 11 | "bg-blinkGray600 hover:bg-blinkGray900 text-blinkGray50 disabled:bg-blinkNeutral300 disabled:text-blinkNeutral50 focus-visible:ring-offset-2"; 12 | const secondaryDarkCls = 13 | "dark:bg-blinkGray400 dark:hover:bg-blinkGray100 dark:text-blinkGreen900 dark:disabled:bg-blinkGray400 dark:disabled:text-blinkNeutral400"; 14 | 15 | const textLightCls = 16 | "bg-transparent hover:bg-blinkGray200 text-blinkGreen900b disabled:text-blinkGray400 disabled:hover:bg-transparent focus-visible:ring-offset-0"; 17 | const textDarkCls = 18 | "dark:hover:bg-blinkGray800 dark:text-blinkNeutral50 dark:disabled:text-blinkNeutral500 dark:disabled:hover:bg-transparent"; 19 | 20 | const linkLightCls = 21 | "underline underline-offset-2 bg-transparent hover:bg-blinkGray200 text-blinkGreen900b disabled:hover:bg-transparent disabled:text-blinkGray400 focus-visible:ring-offset-0"; 22 | const linkDarkCls = 23 | "dark:hover:bg-blinkGray800 dark:text-blinkNeutral50 dark:disabled:hover:bg-transparent dark:disabled:text-blinkNeutral500"; 24 | 25 | const dangerLightCls = 26 | "bg-blinkNeutral50 hover:bg-blinkCoral50/20 text-blinkCoral400 disabled:bg-blinkNeutral300 disabled:text-blinkNeutral50 border border-blinkCoral400 focus-visible:ring-offset-2"; 27 | const dangerDarkCls = 28 | "dark:bg-blinkGray900 dark:hover:bg-blinkGray800 dark:text-blinkNeutral50 dark:disabled:bg-blinkGray400 dark:disabled:text-blinkNeutral400 dark:border-blinkCoral300"; 29 | 30 | const appearanceCls = { 31 | primary: merge(primaryLightCls, primaryDarkCls), 32 | secondary: merge(secondaryLightCls, secondaryDarkCls), 33 | text: merge(textLightCls, textDarkCls), 34 | link: merge(linkLightCls, linkDarkCls), 35 | danger: merge(dangerLightCls, dangerDarkCls), 36 | }; 37 | 38 | type Appearance = keyof typeof appearanceCls; 39 | 40 | type ButtonProps = { 41 | as?: "button" | "span"; 42 | appearance?: Appearance; 43 | children?: ReactNode; 44 | before?: ReactElement; 45 | after?: ReactElement; 46 | }; 47 | 48 | const BaseButton = React.forwardRef( 49 | ( 50 | { 51 | as = "button", 52 | appearance = "primary", 53 | children, 54 | before, 55 | after, 56 | className, 57 | ...rest 58 | }: ButtonProps & ButtonHTMLAttributes, 59 | ref: React.Ref, 60 | ) => { 61 | const Component = as; 62 | return ( 63 | 73 | {before} 74 | {children} 75 | {after} 76 | 77 | ); 78 | }, 79 | ); 80 | 81 | export const Button = React.forwardRef( 82 | ( 83 | { 84 | className, 85 | ...props 86 | }: ButtonProps & ButtonHTMLAttributes, 87 | ref: React.Ref, 88 | ) => { 89 | return ( 90 | 95 | ); 96 | }, 97 | ); 98 | 99 | export const SmallButton = React.forwardRef( 100 | ( 101 | { 102 | className, 103 | ...props 104 | }: ButtonProps & ButtonHTMLAttributes, 105 | ref: React.Ref, 106 | ) => { 107 | return ( 108 | 113 | ); 114 | }, 115 | ); 116 | 117 | export const LargeButton = React.forwardRef( 118 | ( 119 | { 120 | className, 121 | ...props 122 | }: ButtonProps & ButtonHTMLAttributes, 123 | ref: React.Ref, 124 | ) => { 125 | return ( 126 | 131 | ); 132 | }, 133 | ); 134 | 135 | export const XLargeButton = React.forwardRef( 136 | ( 137 | { 138 | className, 139 | ...props 140 | }: ButtonProps & ButtonHTMLAttributes, 141 | ref: React.Ref, 142 | ) => { 143 | return ( 144 | 149 | ); 150 | }, 151 | ); 152 | 153 | export const SmallToLargeButton = React.forwardRef( 154 | ( 155 | { 156 | className, 157 | ...props 158 | }: ButtonProps & ButtonHTMLAttributes, 159 | ref: React.Ref, 160 | ) => { 161 | return ( 162 | 170 | ); 171 | }, 172 | ); 173 | 174 | export const NormalToLargeButton = React.forwardRef( 175 | ( 176 | { 177 | className, 178 | ...props 179 | }: ButtonProps & ButtonHTMLAttributes, 180 | ref: React.Ref, 181 | ) => { 182 | return ( 183 | 191 | ); 192 | }, 193 | ); 194 | -------------------------------------------------------------------------------- /frontend/components/card/simple-card.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLAttributes, LinkHTMLAttributes } from "react"; 2 | 3 | import { merge } from "@fe/utils/merge-classnames"; 4 | 5 | export const Card = ({ 6 | className, 7 | ...props 8 | }: HTMLAttributes) => { 9 | return ( 10 |
17 | ); 18 | }; 19 | 20 | export const CardHeader = ({ 21 | className, 22 | ...props 23 | }: HTMLAttributes) => { 24 | return ( 25 |
32 | ); 33 | }; 34 | 35 | export const CardContent = ({ 36 | className, 37 | ...props 38 | }: HTMLAttributes) => { 39 | return
; 40 | }; 41 | 42 | export const CardContentLink = ({ 43 | className, 44 | ...props 45 | }: LinkHTMLAttributes) => { 46 | return ( 47 | 54 | ); 55 | }; 56 | 57 | export const CardFooterFull = ({ 58 | className, 59 | ...props 60 | }: HTMLAttributes) => { 61 | return ( 62 |
69 | ); 70 | }; 71 | 72 | export const CardFooterLight = ({ 73 | className, 74 | ...props 75 | }: HTMLAttributes) => { 76 | return ( 77 |
81 | ); 82 | }; 83 | 84 | export const CardImage = ({ 85 | className, 86 | src, 87 | alt, 88 | ...props 89 | }: HTMLAttributes & { src: string; alt?: string }) => { 90 | return ( 91 | {alt} 97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /frontend/components/checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | 4 | import { 5 | CheckmarkIcon, 6 | CheckmarkIndeterminateIcon, 7 | } from "@fe/icons/checkbox-icon"; 8 | import { merge } from "@fe/utils/merge-classnames"; 9 | import * as CheckboxPrimitives from "@radix-ui/react-checkbox"; 10 | 11 | function CheckboxBase({ 12 | className, 13 | ...props 14 | }: CheckboxPrimitives.CheckboxProps) { 15 | return ( 16 | 42 | ); 43 | } 44 | 45 | export const Checkbox = ({ 46 | className, 47 | ...props 48 | }: CheckboxPrimitives.CheckboxProps) => { 49 | return ( 50 | 54 | 55 | {props.checked === "indeterminate" ? ( 56 | 57 | ) : ( 58 | 59 | )} 60 | 61 | 62 | ); 63 | }; 64 | 65 | export const NormalToLargeCheckbox = ({ 66 | className, 67 | ...props 68 | }: CheckboxPrimitives.CheckboxProps) => { 69 | return ( 70 | 74 | 75 | {props.checked === "indeterminate" ? ( 76 | 79 | ) : ( 80 | 81 | )} 82 | 83 | 84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /frontend/components/drawer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { merge } from "@fe/utils/merge-classnames"; 4 | import * as DialogPrimitives from "@radix-ui/react-dialog"; 5 | 6 | type DrawerProps = { 7 | children: React.ReactNode; 8 | trigger: React.ReactNode; 9 | position?: "left" | "right"; 10 | }; 11 | 12 | export const Drawer = ({ 13 | trigger, 14 | position = "left", 15 | ...props 16 | }: DialogPrimitives.DialogProps & DrawerProps) => { 17 | return ( 18 | <> 19 | 20 | {trigger} 21 | 22 | 28 | 29 | 30 | 39 | {props.children} 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/components/input/base-input.tsx: -------------------------------------------------------------------------------- 1 | import { InputHTMLAttributes } from "react"; 2 | 3 | import { merge } from "@fe/utils/merge-classnames"; 4 | 5 | const Base = ({ 6 | className, 7 | type = "text", 8 | ...props 9 | }: InputHTMLAttributes) => { 10 | return ( 11 | 21 | ); 22 | }; 23 | 24 | export const BaseInput = ({ 25 | className, 26 | ...props 27 | }: InputHTMLAttributes) => { 28 | return ; 29 | }; 30 | 31 | export const NormalToLargeInput = ({ 32 | className, 33 | ...props 34 | }: InputHTMLAttributes) => { 35 | return ( 36 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /frontend/components/input/input-with-icons.tsx: -------------------------------------------------------------------------------- 1 | import { InputHTMLAttributes, ReactElement } from "react"; 2 | 3 | import { BaseInput, NormalToLargeInput } from "@fe/components/input/base-input"; 4 | import { merge } from "@fe/utils/merge-classnames"; 5 | 6 | type InputWithIconsProps = { 7 | className?: string; 8 | before?: ReactElement; 9 | after?: ReactElement; 10 | }; 11 | 12 | export const InputWithIcons = ({ 13 | before, 14 | after, 15 | className, 16 | ...props 17 | }: InputWithIconsProps & InputHTMLAttributes) => { 18 | return ( 19 |
20 | {before && ( 21 |
22 | {before} 23 |
24 | )} 25 | 29 | 30 | {after && ( 31 |
32 | {after} 33 |
34 | )} 35 |
36 | ); 37 | }; 38 | 39 | export const InputWithIconsNormalToLarge = ({ 40 | before, 41 | after, 42 | className, 43 | ...props 44 | }: InputWithIconsProps & InputHTMLAttributes) => { 45 | return ( 46 |
47 | {before && ( 48 |
49 | {before} 50 |
51 | )} 52 | 60 | 61 | {after && ( 62 |
63 | {after} 64 |
65 | )} 66 |
67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /frontend/components/pill/base.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, ReactElement, ReactNode } from "react"; 2 | 3 | import { CloseCircleIcon } from "@fe/icons/close-circle-icon"; 4 | import { merge } from "@fe/utils/merge-classnames"; 5 | 6 | export type PillProps = { 7 | children?: ReactNode; 8 | className?: string; 9 | before?: ReactElement; 10 | after?: ReactElement; 11 | onDelete?: () => void; 12 | deleteClassName?: string; 13 | }; 14 | 15 | export function Pill({ 16 | children, 17 | before, 18 | after, 19 | onDelete, 20 | deleteClassName, 21 | className, 22 | ...rest 23 | }: PillProps & HTMLAttributes) { 24 | return ( 25 | 33 | {before} 34 | {children} 35 | {after} 36 | {onDelete ? ( 37 | 45 | ) : null} 46 | 47 | ); 48 | } 49 | 50 | Pill.displayName = "Pill"; 51 | -------------------------------------------------------------------------------- /frontend/components/pill/colorful.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | import { Pill, PillProps } from "@fe/components/pill/base"; 4 | import { merge } from "@fe/utils/merge-classnames"; 5 | 6 | export const PillLightGold = ({ 7 | className, 8 | ...props 9 | }: PillProps & HTMLAttributes) => { 10 | return ( 11 | 19 | ); 20 | }; 21 | 22 | export const PillStrongGold = ({ 23 | className, 24 | ...props 25 | }: PillProps & HTMLAttributes) => { 26 | return ( 27 | 35 | ); 36 | }; 37 | 38 | export const PillLightGreen = ({ 39 | className, 40 | ...props 41 | }: PillProps & HTMLAttributes) => { 42 | return ( 43 | 51 | ); 52 | }; 53 | 54 | export const PillStrongGreen = ({ 55 | className, 56 | ...props 57 | }: PillProps & HTMLAttributes) => { 58 | return ( 59 | 67 | ); 68 | }; 69 | 70 | export const PillLightPeach = ({ 71 | className, 72 | ...props 73 | }: PillProps & HTMLAttributes) => { 74 | return ( 75 | 83 | ); 84 | }; 85 | 86 | export const PillStrongPeach = ({ 87 | className, 88 | ...props 89 | }: PillProps & HTMLAttributes) => { 90 | return ( 91 | 99 | ); 100 | }; 101 | 102 | export const PillLightOrange = ({ 103 | className, 104 | ...props 105 | }: PillProps & HTMLAttributes) => { 106 | return ( 107 | 115 | ); 116 | }; 117 | 118 | export const PillStrongOrange = ({ 119 | className, 120 | ...props 121 | }: PillProps & HTMLAttributes) => { 122 | return ( 123 | 131 | ); 132 | }; 133 | 134 | export const PillLightCoral = ({ 135 | className, 136 | ...props 137 | }: PillProps & HTMLAttributes) => { 138 | return ( 139 | 147 | ); 148 | }; 149 | 150 | export const PillStrongCoral = ({ 151 | className, 152 | ...props 153 | }: PillProps & HTMLAttributes) => { 154 | return ( 155 | 163 | ); 164 | }; 165 | 166 | export const PillLightPink = ({ 167 | className, 168 | ...props 169 | }: PillProps & HTMLAttributes) => { 170 | return ( 171 | 179 | ); 180 | }; 181 | 182 | export const PillStrongPink = ({ 183 | className, 184 | ...props 185 | }: PillProps & HTMLAttributes) => { 186 | return ( 187 | 195 | ); 196 | }; 197 | 198 | export const PillLightBlue = ({ 199 | className, 200 | ...props 201 | }: PillProps & HTMLAttributes) => { 202 | return ( 203 | 211 | ); 212 | }; 213 | 214 | export const PillStrongBlue = ({ 215 | className, 216 | ...props 217 | }: PillProps & HTMLAttributes) => { 218 | return ( 219 | 227 | ); 228 | }; 229 | -------------------------------------------------------------------------------- /frontend/components/sidebar/navigation-groups.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | 3 | import { ChevronDownIcon } from "@fe/icons/chevron-down-icon"; 4 | import { ChevronRightIcon } from "@fe/icons/chevron-right-icon"; 5 | import { SidebarRegularItem } from "@fe/components/sidebar/navigation-items"; 6 | import { merge } from "@fe/utils/merge-classnames"; 7 | import * as CollapsiblePrimitives from "@radix-ui/react-collapsible"; 8 | 9 | type NavigationGroupProps = { 10 | left?: ReactNode; 11 | right?: ReactNode; 12 | header?: string; 13 | children: ReactNode; 14 | className?: string; 15 | divider?: "top" | "bottom"; 16 | }; 17 | 18 | export const NavigationGroup = ({ 19 | header, 20 | children, 21 | right, 22 | left, 23 | divider, 24 | className, 25 | ...props 26 | }: NavigationGroupProps) => { 27 | return ( 28 |
41 | {header || left || right ? ( 42 |
43 | {left} 44 | 45 | {header} 46 | 47 |
48 | ) : null} 49 | {children} 50 |
51 | ); 52 | }; 53 | 54 | type ItemWithSubNavigationProps = { 55 | text: string; 56 | children: ReactNode; 57 | initialOpen?: boolean; 58 | }; 59 | 60 | export const CollapsibleSubgroupLeft = ({ 61 | text, 62 | children, 63 | initialOpen = false, 64 | }: ItemWithSubNavigationProps) => { 65 | const [open, setOpen] = React.useState(initialOpen); 66 | 67 | return ( 68 | 73 | 74 | 79 | ) : ( 80 | 81 | ) 82 | } 83 | role="button" 84 | aria-expanded={open} 85 | aria-controls="collapsible-content-left" 86 | > 87 | {text} 88 | 89 | 90 | 91 | 95 | {children} 96 | 97 | 98 | ); 99 | }; 100 | 101 | export const CollapsibleSubgroupRight = ({ 102 | text, 103 | icon, 104 | children, 105 | initialOpen = false, 106 | }: ItemWithSubNavigationProps & { icon?: ReactNode }) => { 107 | const [open, setOpen] = React.useState(initialOpen); 108 | 109 | return ( 110 | 115 | 116 | 122 | ) : ( 123 | 124 | ) 125 | } 126 | role="button" 127 | aria-expanded={open} 128 | aria-controls="collapsible-content-right" 129 | > 130 | {text} 131 | 132 | 133 | 134 | 138 | {children} 139 | 140 | 141 | ); 142 | }; 143 | -------------------------------------------------------------------------------- /frontend/components/sidebar/navigation-items.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLAttributes, ReactNode } from "react"; 2 | 3 | import { Tooltip } from "@fe/components/tooltip"; 4 | import { merge } from "@fe/utils/merge-classnames"; 5 | 6 | type SidebarItemProps = { 7 | before?: ReactNode; 8 | after?: ReactNode; 9 | children?: ReactNode; 10 | href?: string; 11 | onClick?: () => void; 12 | isActive?: boolean; 13 | className?: string; 14 | ariaLabel?: string; 15 | } & HTMLAttributes; 16 | 17 | export const SidebarBaseItem = React.forwardRef( 18 | ( 19 | { 20 | before, 21 | after, 22 | children, 23 | href, 24 | onClick, 25 | isActive, 26 | className, 27 | ...rest 28 | }: SidebarItemProps, 29 | ref: React.ForwardedRef, 30 | ) => { 31 | const Component = href ? "a" : onClick ? "button" : "span"; 32 | return ( 33 | 49 | {before && {before}} 50 | 51 | {children} 52 | 53 | {after && ( 54 | 55 | {after} 56 | 57 | )} 58 | 59 | ); 60 | }, 61 | ); 62 | 63 | export const SidebarIconItem = ({ 64 | className, 65 | title, 66 | ...props 67 | }: SidebarItemProps & { title: string }) => { 68 | return ( 69 | 70 | 78 | 79 | ); 80 | }; 81 | 82 | export const SidebarRegularItem = React.forwardRef( 83 | ( 84 | { className, ...props }: SidebarItemProps, 85 | ref: React.ForwardedRef, 86 | ) => { 87 | return ( 88 | 93 | ); 94 | }, 95 | ); 96 | 97 | type SidebarHeadingProps = { 98 | children?: ReactNode; 99 | before?: ReactNode; 100 | after?: ReactNode; 101 | className?: string; 102 | }; 103 | 104 | export const SidebarHeading = ({ 105 | before, 106 | after, 107 | children, 108 | className, 109 | ...rest 110 | }: SidebarHeadingProps) => { 111 | return ( 112 |
113 | {before && {before}} 114 | {children} 115 | {after && {after}} 116 |
117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /frontend/components/sidebar/sidebar-layouts.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import { Transition } from "react-transition-group"; 3 | 4 | import { DoubleChevronsLeftIcon } from "@fe/icons/double-chevrons-left-icon"; 5 | import { DoubleChevronsRightIcon } from "@fe/icons/double-chevrons-right-icon"; 6 | import { MenuIcon } from "@fe/icons/menu-icon"; 7 | import { Button } from "@fe/components/button"; 8 | import { merge } from "@fe/utils/merge-classnames"; 9 | import * as DialogPrimitives from "@radix-ui/react-dialog"; 10 | 11 | export const SecondarySidebar = ({ 12 | children, 13 | className, 14 | ...rest 15 | }: { 16 | children?: ReactNode; 17 | className?: string; 18 | }) => { 19 | return ( 20 |
27 | {children} 28 |
29 | ); 30 | }; 31 | 32 | export const PrimarySidebar = ({ 33 | children, 34 | className, 35 | }: { 36 | children: ReactNode; 37 | className?: string; 38 | }) => { 39 | return ( 40 | <> 41 | 42 |
43 | 44 | 52 | 53 |
54 | 60 | 61 | 62 | 69 | {children} 70 | 71 | 72 |
73 | 74 | 82 | 83 | ); 84 | }; 85 | 86 | export const CollapsiblePrimarySidebar = ({ 87 | children, 88 | collapsedElements, 89 | className, 90 | }: { 91 | children: ReactNode; 92 | collapsedElements: ReactNode; 93 | className?: string; 94 | }) => { 95 | const [isOpen, setIsOpen] = React.useState(true); 96 | 97 | return ( 98 | <> 99 | 100 |
101 | 102 | 110 | 111 |
112 | 118 | 119 | 120 | 127 | {children} 128 | 129 | 130 |
131 | 132 | {(state) => { 133 | return ( 134 |
144 | 167 |
168 | ); 169 | }} 170 |
171 | 172 | ); 173 | }; 174 | -------------------------------------------------------------------------------- /frontend/components/table/index.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from "react"; 2 | 3 | import { merge } from "@fe/utils/merge-classnames"; 4 | 5 | export const TableCell = ({ 6 | children, 7 | className, 8 | ...props 9 | }: TdHTMLAttributes) => { 10 | return ( 11 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | export const TableHeadCell = ({ 21 | children, 22 | className, 23 | ...props 24 | }: ThHTMLAttributes) => { 25 | return ( 26 | 33 | {children} 34 | 35 | ); 36 | }; 37 | 38 | export const TableRow = ({ 39 | children, 40 | className, 41 | ...props 42 | }: HTMLAttributes) => { 43 | return ( 44 | 51 | {children} 52 | 53 | ); 54 | }; 55 | 56 | export const Table = ({ 57 | children, 58 | className, 59 | ...props 60 | }: HTMLAttributes) => { 61 | return ( 62 | 63 | {children} 64 |
65 | ); 66 | }; 67 | 68 | export const TableBody = ({ 69 | children, 70 | className, 71 | ...props 72 | }: HTMLAttributes) => { 73 | return ( 74 | 75 | {children} 76 | 77 | ); 78 | }; 79 | 80 | export const TableHead = ({ 81 | children, 82 | className, 83 | ...props 84 | }: HTMLAttributes) => { 85 | return ( 86 | 87 | {children} 88 | 89 | ); 90 | }; 91 | 92 | export const TableFoot = ({ 93 | children, 94 | className, 95 | ...props 96 | }: HTMLAttributes) => { 97 | return ( 98 | 99 | {children} 100 | 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /frontend/components/toggle/index.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes } from "react"; 2 | 3 | import { merge } from "@fe/utils/merge-classnames"; 4 | import * as Switch from "@radix-ui/react-switch"; 5 | 6 | const ToggleBase = ({ 7 | className, 8 | ...props 9 | }: Switch.SwitchProps & ButtonHTMLAttributes) => { 10 | return ( 11 | 19 | ); 20 | }; 21 | 22 | const ThumbBase = ({ className, ...props }: Switch.SwitchThumbProps) => { 23 | return ( 24 | 31 | ); 32 | }; 33 | 34 | export const NarrowToggleNormal = ({ 35 | className, 36 | ...props 37 | }: Switch.SwitchProps & ButtonHTMLAttributes) => { 38 | return ( 39 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export const NarrowToggleLarge = ({ 49 | className, 50 | ...props 51 | }: Switch.SwitchProps & ButtonHTMLAttributes) => { 52 | return ( 53 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | export const WideToggleNormal = ({ 63 | className, 64 | ...props 65 | }: Switch.SwitchProps & ButtonHTMLAttributes) => { 66 | return ( 67 | 68 | 69 | 70 | ); 71 | }; 72 | 73 | export const WideToggleLarge = ({ 74 | className, 75 | ...props 76 | }: Switch.SwitchProps & ButtonHTMLAttributes) => { 77 | return ( 78 | 79 | 80 | 81 | ); 82 | }; 83 | 84 | export const NormalToLargeWideToggle = ({ 85 | className, 86 | ...props 87 | }: Switch.SwitchProps & ButtonHTMLAttributes) => { 88 | return ( 89 | 93 | 94 | 95 | ); 96 | }; 97 | 98 | export const LargerToXLargeWideToggle = ({ 99 | className, 100 | ...props 101 | }: Switch.SwitchProps & ButtonHTMLAttributes) => { 102 | return ( 103 | 107 | 108 | 109 | ); 110 | }; 111 | 112 | export const NormalToLargeNarrowToggle = ({ 113 | className, 114 | ...props 115 | }: Switch.SwitchProps & ButtonHTMLAttributes) => { 116 | return ( 117 | 124 | 125 | 126 | ); 127 | }; 128 | 129 | export const LargerToXLargeNarrowToggle = ({ 130 | className, 131 | ...props 132 | }: Switch.SwitchProps & ButtonHTMLAttributes) => { 133 | return ( 134 | 141 | 142 | 143 | ); 144 | }; 145 | -------------------------------------------------------------------------------- /frontend/components/tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | import { merge } from "@fe/utils/merge-classnames"; 4 | import * as TooltipPrimitives from "@radix-ui/react-tooltip"; 5 | 6 | type TooltipProps = { 7 | text: string; 8 | children: ReactNode; 9 | position?: "top" | "right" | "bottom" | "left"; 10 | }; 11 | 12 | export const Tooltip = ({ text, children, position }: TooltipProps) => { 13 | return ( 14 | 15 | 16 | 17 | {children} 18 | 19 | 25 | 32 | {text} 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/icons/bell-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const BellIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/icons/checkbox-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const CheckmarkIndeterminateIcon = (props: SVGProps) => { 4 | return ( 5 | 13 | 19 | 20 | 24 | 25 | ); 26 | }; 27 | 28 | export const CheckmarkIcon = (props: SVGProps) => { 29 | return ( 30 | 38 | 42 | 43 | 48 | 49 | 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /frontend/icons/chevron-down-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const ChevronDownIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/icons/chevron-right-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const ChevronRightIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/icons/clipboard-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const ClipboardIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 27 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/icons/close-circle-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const CloseCircleIcon = (props: SVGProps) => { 4 | return ( 5 | 13 | 20 | 21 | 28 | 29 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/icons/dashboard-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const DashboardIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/icons/dollar-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const DollarIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 15 | 22 | 29 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/icons/dots-vertical-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const DotsVerticalIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 10 | 14 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /frontend/icons/double-chevrons-left-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const DoubleChevronsLeftIcon = (props: SVGProps) => { 4 | return ( 5 | 13 | 20 | 21 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/icons/double-chevrons-right-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const DoubleChevronsRightIcon = (props: SVGProps) => { 4 | return ( 5 | 13 | 20 | 21 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/icons/download-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const DownloadIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/icons/edit-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const EditIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/icons/eye-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const EyeIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/icons/file-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const FileIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/icons/help-circle-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const HelpCircleIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /frontend/icons/home-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const HomeIcon = (props: SVGProps) => { 4 | return ( 5 | 13 | 20 | 21 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/icons/inbox-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const InboxIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/icons/log-out-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const LogOutIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/icons/logos/product-logo.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const ProductLogo = (props: SVGProps) => { 4 | return ( 5 | 13 | 14 | 15 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/icons/menu-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const MenuIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/icons/percentage-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const PercentageIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 22 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/icons/plus-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const PlusIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/icons/search-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const SearchIcon = (props: SVGProps) => ( 4 | 12 | 18 | 19 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /frontend/icons/settings-icon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const SettingsIcon = (props: SVGProps) => { 4 | return ( 5 | 6 | 13 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { FixedWidthPrimarySidebar } from "@fe/patterns/fixed-width-primary-sidebar"; 2 | import { TopbarForSidebarContentLayout } from "@fe/patterns/topbar-for-sidebar-content-layout"; 3 | import { Card, CardContent } from "@fe/components/card/simple-card"; 4 | import { EyeIcon } from "@fe/icons/eye-icon"; 5 | import { DollarIcon } from "@fe/icons/dollar-icon"; 6 | import { PillLightCoral, PillLightGreen } from "@fe/components/pill/colorful"; 7 | import { DotsVerticalIcon } from "@fe/icons/dots-vertical-icon"; 8 | import { Button } from "@fe/components/button"; 9 | import { 10 | Table, 11 | TableBody, 12 | TableCell, 13 | TableHead, 14 | TableHeadCell, 15 | TableRow, 16 | } from "@fe/components/table"; 17 | 18 | export const DashboardPage = () => { 19 | return ( 20 |
21 | 22 | 23 |
24 | 25 | 26 |
27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 |

38 | 32 567{" "} 39 |

40 |
41 |

42 | Views last month 43 |

44 | 45 | 10% ↑ 46 | 47 |
48 |
49 |
50 | 51 | 52 | 53 |
54 | 55 |
56 |

60 | 11 334{" "} 61 |

62 |
63 |

64 | Views last 7 days 65 |

66 | 67 | 23% ↑ 68 | 69 |
70 |
71 |
72 | 73 | 74 | 75 |
76 | 77 |
78 |

82 | 11 035 83 |

84 |
85 |

86 | Revenue last year 87 |

88 | 89 | 12% ↓ 90 | 91 |
92 |
93 |
94 | 95 | 96 | 97 |
98 | 99 |
100 |

104 | 800 105 |

106 |
107 |

108 | Revenue last month 109 |

110 | 111 | 6% ↓ 112 | 113 |
114 |
115 |
116 |
117 |
118 |

119 | Website statistics last three month 120 |

121 |
122 | 123 | 124 | 125 | Source 126 | Visitors 127 | Revenue 128 | Status 129 | Action 130 | 131 | 132 | 133 | 134 | Unknown 135 | 11 355 136 | 2,123 $ 137 | 138 | 139 | 23% ↑ 140 | 141 | 142 | 143 | 146 | 147 | 148 | 149 | Google search 150 | 4 235 151 | 999 $ 152 | 153 | 154 | 3% ↑ 155 | 156 | 157 | 158 | 161 | 162 | 163 | 164 | Google ads 165 | 4 560 166 | 884 $ 167 | 168 | 169 | 6% ↓ 170 | 171 | 172 | 173 | 176 | 177 | 178 | 179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 | ); 187 | }; 188 | -------------------------------------------------------------------------------- /frontend/patterns/fixed-width-primary-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { PrimarySidebar } from "@fe/components/sidebar/sidebar-layouts"; 2 | 3 | import { PrimarySidebarBottomGroup } from "@fe/patterns/primary-sidebar-bottom-group"; 4 | import { PrimarySidebarCreateButton } from "@fe/patterns/primary-sidebar-create-button"; 5 | import { PrimarySidebarHeading } from "@fe/patterns/primary-sidebar-heading"; 6 | import { PrimarySidebarPrimaryGroup } from "@fe/patterns/primary-sidebar-primary-group"; 7 | import { PrimarySidebarSecondaryGroup } from "@fe/patterns/primary-sidebar-secondary-group"; 8 | 9 | export const FixedWidthPrimarySidebar = () => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/patterns/primary-sidebar-bottom-group.tsx: -------------------------------------------------------------------------------- 1 | import { HelpCircleIcon } from "@fe/icons/help-circle-icon"; 2 | import { LogOutIcon } from "@fe/icons/log-out-icon"; 3 | import { AvatarImage } from "@fe/components/avatar"; 4 | import { NormalToLargeButton } from "@fe/components/button"; 5 | import { NavigationGroup } from "@fe/components/sidebar/navigation-groups"; 6 | import { SidebarRegularItem } from "@fe/components/sidebar/navigation-items"; 7 | import { Tooltip } from "@fe/components/tooltip"; 8 | 9 | export const PrimarySidebarBottomGroup = () => { 10 | return ( 11 | 12 | } 15 | > 16 | Help 17 | 18 | 25 | } 26 | after={ 27 | 28 | 33 | 34 | 35 | 36 | } 37 | > 38 | Sheera Gottstein 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/patterns/primary-sidebar-create-button.tsx: -------------------------------------------------------------------------------- 1 | import { EditIcon } from "@fe/icons/edit-icon"; 2 | import { NormalToLargeButton } from "@fe/components/button"; 3 | 4 | export const PrimarySidebarCreateButton = () => { 5 | return ( 6 | } 9 | > 10 | Create 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/patterns/primary-sidebar-heading.tsx: -------------------------------------------------------------------------------- 1 | import { BellIcon } from "@fe/icons/bell-icon"; 2 | import { ProductLogo } from "@fe/icons/logos/product-logo"; 3 | import { Badge } from "@fe/components/badge"; 4 | import { Button } from "@fe/components/button"; 5 | import { SidebarHeading } from "@fe/components/sidebar/navigation-items"; 6 | import { Tooltip } from "@fe/components/tooltip"; 7 | 8 | export const PrimarySidebarHeading = () => { 9 | return ( 10 | } 12 | after={ 13 | 14 | 25 | 26 | } 27 | > 28 | Settings 29 | 30 | ); 31 | }; 32 | 33 | export const PrimarySidebarHeading2 = () => { 34 | return ( 35 | } 38 | after={ 39 | 40 | 51 | 52 | } 53 | > 54 | Settings 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /frontend/patterns/primary-sidebar-primary-group.tsx: -------------------------------------------------------------------------------- 1 | import { ClipboardIcon } from "@fe/icons/clipboard-icon"; 2 | import { DashboardIcon } from "@fe/icons/dashboard-icon"; 3 | import { FileIcon } from "@fe/icons/file-icon"; 4 | import { HomeIcon } from "@fe/icons/home-icon"; 5 | import { InboxIcon } from "@fe/icons/inbox-icon"; 6 | import { PercentageIcon } from "@fe/icons/percentage-icon"; 7 | import { SettingsIcon } from "@fe/icons/settings-icon"; 8 | import { Badge } from "@fe/components/badge"; 9 | import { 10 | CollapsibleSubgroupRight, 11 | NavigationGroup, 12 | } from "@fe/components/sidebar/navigation-groups"; 13 | import { SidebarRegularItem } from "@fe/components/sidebar/navigation-items"; 14 | 15 | export const PrimarySidebarPrimaryGroup = ({ ...props }) => { 16 | return ( 17 | 18 | } 21 | > 22 | Home 23 | 24 | } 27 | after={} 28 | > 29 | Inbox 30 | 31 | } 34 | > 35 | Reporting 36 | 37 | } 40 | > 41 | Dashboard 42 | 43 | } 46 | > 47 | } 51 | > 52 | Todo 53 | 54 | } 58 | > 59 | In progress 60 | 61 | } 65 | > 66 | Done 67 | 68 | 69 | } 72 | > 73 | Documents 74 | 75 | } 79 | > 80 | Settings 81 | 82 | 83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /frontend/patterns/primary-sidebar-secondary-group.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@fe/components/badge"; 2 | import { 3 | CollapsibleSubgroupLeft, 4 | NavigationGroup, 5 | } from "@fe/components/sidebar/navigation-groups"; 6 | import { SidebarRegularItem } from "@fe/components/sidebar/navigation-items"; 7 | 8 | export const PrimarySidebarSecondaryGroup = () => { 9 | return ( 10 | 11 | 12 | } 16 | > 17 | Stories 18 | 19 | } 23 | > 24 | Tasks 25 | 26 | 27 | Resources 28 | 29 | 30 | 31 | 32 | 33 | Stories 34 | 35 | 36 | Tasks 37 | 38 | } 42 | > 43 | Resources 44 | 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /frontend/patterns/topbar-for-sidebar-content-layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { DotsVerticalIcon } from "@fe/icons/dots-vertical-icon"; 4 | import { DownloadIcon } from "@fe/icons/download-icon"; 5 | import { PlusIcon } from "@fe/icons/plus-icon"; 6 | import { SearchIcon } from "@fe/icons/search-icon"; 7 | import { Button, NormalToLargeButton } from "@fe/components/button"; 8 | import { Drawer } from "@fe/components/drawer"; 9 | import { InputWithIconsNormalToLarge } from "@fe/components/input/input-with-icons"; 10 | import { WideToggleLarge } from "@fe/components/toggle"; 11 | 12 | export const TopbarForSidebarContentLayout = () => { 13 | return ( 14 |
15 | 72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /frontend/utils/merge-classnames.tsx: -------------------------------------------------------------------------------- 1 | import { twMerge } from "tailwind-merge"; 2 | 3 | export function merge(...cls: (string | undefined | boolean)[]) { 4 | return twMerge(cls.filter(Boolean).join(" ")); 5 | } 6 | -------------------------------------------------------------------------------- /frontend/utils/variations-components.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes, ReactNode } from "react"; 2 | 3 | import { merge } from "@fe/utils/merge-classnames"; 4 | 5 | export const VariationBase = ({ 6 | children, 7 | title, 8 | className, 9 | ...props 10 | }: { 11 | children: ReactNode; 12 | title?: string; 13 | className?: string; 14 | } & HTMLAttributes) => { 15 | return ( 16 |
23 | {title ?

{title}

: null} 24 |
{children}
25 |
26 | ); 27 | }; 28 | 29 | export const Variation = ({ 30 | className, 31 | ...props 32 | }: { 33 | children: ReactNode; 34 | title?: string; 35 | className?: string; 36 | } & HTMLAttributes) => { 37 | return ( 38 | 45 | ); 46 | }; 47 | export const VariationDarker = ({ 48 | className, 49 | ...props 50 | }: { 51 | children: ReactNode; 52 | title?: string; 53 | className?: string; 54 | } & HTMLAttributes) => { 55 | return ( 56 | 63 | ); 64 | }; 65 | 66 | export const VariationsContainer = ({ 67 | children, 68 | className, 69 | ...props 70 | }: { 71 | children: ReactNode; 72 | className?: string; 73 | }) => { 74 | return ( 75 |
76 | {children} 77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beyond-react-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "preview": "vite preview", 10 | "start": "tsx watch backend/index.ts", 11 | "ts": "npx tsc --noEmit", 12 | "lint": "npx prettier . --check && npx eslint .", 13 | "lint:fix": "npx prettier . --write && npx eslint . --fix" 14 | }, 15 | "dependencies": { 16 | "@radix-ui/react-checkbox": "^1.1.3", 17 | "@radix-ui/react-collapsible": "^1.1.2", 18 | "@radix-ui/react-dialog": "^1.1.4", 19 | "@radix-ui/react-dropdown-menu": "^2.1.4", 20 | "@radix-ui/react-switch": "^1.1.2", 21 | "@radix-ui/react-tooltip": "^1.1.6", 22 | "@types/react-transition-group": "^4.4.12", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "react-transition-group": "^4.4.5", 26 | "tailwind-merge": "^2.5.5" 27 | }, 28 | "devDependencies": { 29 | "@hono/node-server": "^1.13.7", 30 | "hono": "^4.6.15", 31 | "tsx": "^4.7.1", 32 | "@eslint/js": "^9.17.0", 33 | "@types/node": "^22.10.2", 34 | "@types/react": "^18.3.17", 35 | "@types/react-dom": "^18.3.5", 36 | "@vitejs/plugin-react": "^4.3.4", 37 | "autoprefixer": "^10.4.20", 38 | "eslint": "^9.17.0", 39 | "eslint-plugin-react-hooks": "^5.0.0", 40 | "eslint-plugin-react-refresh": "^0.4.16", 41 | "globals": "^15.13.0", 42 | "postcss": "^8.4.49", 43 | "prettier": "3.4.2", 44 | "tailwindcss": "^3.4.17", 45 | "typescript": "~5.6.2", 46 | "typescript-eslint": "^8.18.1", 47 | "vite": "^6.0.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardPage } from "@fe/pages/dashboard"; 2 | 3 | function App() { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body, 6 | html { 7 | min-height: 100%; 8 | height: 100vh; 9 | overflow-y: auto; 10 | 11 | @apply font-blink-text bg-blinkNeutral50 blink-text-primary; 12 | } 13 | 14 | @layer components { 15 | .blink-double-focus-ring { 16 | @apply focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-blinkNeutral50 focus-visible:ring-blinkNeutral900; 17 | @apply dark:focus-visible:ring-offset-blinkGray900 dark:focus-visible:ring-blinkNeutral50; 18 | } 19 | 20 | /** 21 | * Text color combinations 22 | */ 23 | .blink-text-primary { 24 | @apply text-blinkNeutral900 dark:text-blinkNeutral50; 25 | } 26 | 27 | .blink-text-inverse { 28 | @apply text-blinkNeutral50 dark:text-blinkNeutral900; 29 | } 30 | 31 | .blink-text-secondary { 32 | @apply text-blinkNeutral700 dark:text-blinkNeutral300; 33 | } 34 | 35 | .blink-text-subdued { 36 | @apply text-blinkNeutral500 dark:text-blinkGray400; 37 | } 38 | 39 | .blink-text-body { 40 | @apply text-blinkGreen900b dark:text-blinkNeutral100; 41 | } 42 | 43 | .blink-text-disabled { 44 | @apply text-blinkGray300 dark:text-blinkGray600; 45 | } 46 | 47 | .blink-text-destructive { 48 | @apply text-blinkCoral400 dark:text-blinkCoral300; 49 | } 50 | 51 | /** 52 | * Background color combinations 53 | */ 54 | .blink-surface-background { 55 | @apply bg-blinkNeutral50 dark:bg-blinkNeutral900; 56 | } 57 | 58 | .blink-surface-default { 59 | @apply bg-blinkNeutral50 dark:bg-blinkNeutral800; 60 | } 61 | 62 | .blink-surface-inverse { 63 | @apply bg-blinkNeutral800 dark:bg-blinkNeutral50; 64 | } 65 | 66 | .blink-surface-light { 67 | @apply bg-blinkGreen50 dark:bg-blinkGray900; 68 | } 69 | 70 | .blink-surface-strong { 71 | @apply bg-blinkGray300 dark:bg-blinkNeutral900; 72 | } 73 | 74 | .blink-surface-on-strong { 75 | @apply bg-blinkNeutral50 dark:bg-blinkGray700; 76 | } 77 | 78 | /** 79 | * Border color combinations 80 | */ 81 | .blink-border-container-white { 82 | @apply border-blinkGray100 dark:border-blinkNeutral900; 83 | } 84 | 85 | .blink-border-on-color { 86 | @apply border-blinkGray500 dark:border-blinkNeutral900; 87 | } 88 | } 89 | 90 | *:focus:not(.focus-visible) { 91 | outline: none; 92 | } 93 | *:focus-visible { 94 | @apply blink-double-focus-ring; 95 | } 96 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{html,js,ts,tsx}", "./frontend/**/*.{html,js,ts,tsx}"], 4 | theme: { 5 | extend: { 6 | keyframes: { 7 | slideInLeft: { 8 | "0%": { transform: "translateX(-100%)" }, 9 | "100%": { transform: "translateX(0)" }, 10 | }, 11 | slideInRight: { 12 | "0%": { transform: "translateX(100%)" }, 13 | "100%": { transform: "translateX(0)" }, 14 | }, 15 | slideInTop: { 16 | "0%": { transform: "translateY(-100%)" }, 17 | "100%": { transform: "translateY(0)" }, 18 | }, 19 | slideInBottom: { 20 | "0%": { transform: "translateY(100%)" }, 21 | "100%": { transform: "translateY(0)" }, 22 | }, 23 | overlayShow: { 24 | from: { opacity: "0" }, 25 | to: { opacity: "1" }, 26 | }, 27 | contentShow: { 28 | from: { 29 | opacity: "0", 30 | transform: "scale(0.96)", 31 | }, 32 | to: { opacity: "1", transform: "scale(1)" }, 33 | }, 34 | }, 35 | animation: { 36 | "slide-in-left": "slideInLeft 0.3s ease-out", 37 | "slide-in-top": "slideInTop 0.3s ease-out", 38 | "slide-in-bottom": "slideInBottom 0.3s ease-out", 39 | "spin-slow": "spin 3s linear infinite", 40 | overlayShow: "overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1)", 41 | contentShow: "contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)", 42 | }, 43 | }, 44 | fontFamily: { 45 | "blink-title": '"Crimson Text", "Times New Roman", serif', 46 | "blink-text": 47 | '"Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif', 48 | }, 49 | colors: { 50 | transparent: "transparent", 51 | current: "currentColor", 52 | 53 | blinkGray50: "#F5F7F6", 54 | blinkGray100: "#ECF1EF", 55 | blinkGray200: "#E3E8E6", 56 | blinkGray300: "#DADEDC", 57 | blinkGray400: "#B7BDBA", 58 | blinkGray500: "#9CA19E", 59 | blinkGray600: "#898F8B", 60 | blinkGray700: "#6B706C", 61 | blinkGray800: "#595E5A", 62 | blinkGray900: "#3B403C", 63 | 64 | blinkNeutral50: "#FFFFFF", 65 | blinkNeutral100: "#F2F2F2", 66 | blinkNeutral200: "#E9E9E9", 67 | blinkNeutral300: "#D6D6D6", 68 | blinkNeutral400: "#8A8A8A", 69 | blinkNeutral500: "#757575", 70 | blinkNeutral600: "#5A5A5A", 71 | blinkNeutral700: "#383838", 72 | blinkNeutral800: "#1F1F1F", 73 | blinkNeutral900: "#090909", 74 | 75 | blinkGreen50: "#F3F7F5", 76 | blinkGreen100: "#C3DCCF", 77 | blinkGreen200: "#AAC3B6", 78 | blinkGreen300: "#8CA497", 79 | blinkGreen400: "#90BCA5", 80 | blinkGreen500: "#4D9771", 81 | blinkGreen600: "#3C7A5A", 82 | blinkGreen700: "#516649", 83 | blinkGreen800: "#122F07", 84 | blinkGreen900b: "#2A3028", 85 | blinkGreen900: "#0F2407", 86 | 87 | blinkCoral50: "#FFD6D5", 88 | blinkCoral100: "#FFB9B7", 89 | blinkCoral200: "#FB8583", 90 | blinkCoral300: "#F95E5A", 91 | blinkCoral400: "#E64743", 92 | blinkCoral500: "#E2A9A8", 93 | blinkCoral600: "#C75F5F", 94 | blinkCoral700: "#AE4343", 95 | blinkCoral800: "#9F3434", 96 | blinkCoral900: "#730A0A", 97 | 98 | blinkBlue50: "#C4F4FF", 99 | blinkBlue100: "#84DCF0", 100 | blinkBlue200: "#52C4DF", 101 | blinkBlue300: "#2BBCDF", 102 | blinkBlue400: "#03A5CC", 103 | blinkBlue500: "#61B8CC", 104 | blinkBlue600: "#5B93A0", 105 | blinkBlue700: "#1E8097", 106 | blinkBlue800: "#126477", 107 | blinkBlue900: "#03333E", 108 | 109 | blinkPink50: "#FFE6ED", 110 | blinkPink100: "#F7BBCB", 111 | blinkPink200: "#FC95B1", 112 | blinkPink300: "#FF5D84", 113 | blinkPink400: "#E4456E", 114 | blinkPink500: "#C02047", 115 | blinkPink600: "#C9042B", 116 | blinkPink700: "#A24E65", 117 | blinkPink800: "#880324", 118 | blinkPink900: "#5F051B", 119 | 120 | blinkOrange50: "#FFCAB7", 121 | blinkOrange100: "#FF8458", 122 | blinkOrange200: "#E3521D", 123 | blinkOrange300: "#DE3B00", 124 | blinkOrange400: "#D18B71", 125 | blinkOrange500: "#C46A4A", 126 | blinkOrange600: "#B5512D", 127 | blinkOrange700: "#9C4627", 128 | blinkOrange800: "#7D3116", 129 | blinkOrange900: "#65220A", 130 | 131 | blinkPeach50: "#F7E3D6", 132 | blinkPeach100: "#F4CCB1", 133 | blinkPeach200: "#FAA66F", 134 | blinkPeach300: "#F3873F", 135 | blinkPeach400: "#E9AD85", 136 | blinkPeach500: "#DF9666", 137 | blinkPeach600: "#CC7A43", 138 | blinkPeach700: "#97623F", 139 | blinkPeach800: "#773508", 140 | blinkPeach900: "#3A1801", 141 | 142 | blinkGold50: "#F9E8C3", 143 | blinkGold100: "#F4D592", 144 | blinkGold200: "#EAB747", 145 | blinkGold300: "#E8AA22", 146 | blinkGold400: "#EAA202", 147 | blinkGold500: "#D79401", 148 | blinkGold600: "#D2AE5E", 149 | blinkGold700: "#C09942", 150 | blinkGold800: "#A2781C", 151 | blinkGold900: "#755000", 152 | }, 153 | }, 154 | plugins: [], 155 | }; 156 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": false, 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "alwaysStrict": true, 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noUnusedParameters": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "bundler", 17 | "preserveConstEnums": true, 18 | "removeComments": true, 19 | "resolveJsonModule": true, 20 | "sourceMap": true, 21 | "strictNullChecks": true, 22 | "strictFunctionTypes": true, 23 | "strictBindCallApply": true, 24 | "strictPropertyInitialization": true, 25 | "isolatedModules": true, 26 | "jsx": "preserve", 27 | "incremental": true, 28 | "useUnknownInCatchVariables": false, 29 | "paths": { 30 | "@/*": ["./src/*"], 31 | "@fe/*": ["./frontend/*"] 32 | }, 33 | "typeRoots": ["./node_modules/@types"], 34 | "target": "ES2017" 35 | }, 36 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 37 | "exclude": ["node_modules", ".next"], 38 | "ts-node": { 39 | // these options are overrides used only by ts-node 40 | // same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable 41 | "compilerOptions": { 42 | "module": "commonjs", 43 | "jsx": "react-jsx" 44 | }, 45 | "require": ["tsconfig-paths/register"] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import path from "path"; 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | "@fe": path.resolve("./frontend"), 10 | "@": path.resolve("./src"), 11 | }, 12 | }, 13 | plugins: [react()], 14 | }); 15 | --------------------------------------------------------------------------------