├── .gitignore ├── LICENSE ├── README.md ├── components.json ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── apple-touch-icon.png ├── favicon-96x96.png ├── favicon.ico ├── favicon.svg ├── large-og-image.png ├── logo.png ├── site.webmanifest ├── vite.svg ├── web-app-manifest-192x192.png └── web-app-manifest-512x512.png ├── src ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── BaseFontSize.tsx │ ├── ConversionHistory.tsx │ ├── ConversionInfo.tsx │ ├── CopyButton.tsx │ ├── FontSizeTip.tsx │ ├── Navbar.tsx │ ├── SaveToHistoryButton.tsx │ ├── ThemeToggle.tsx │ ├── UnitInput.tsx │ ├── UnitSelect.tsx │ ├── theme │ │ └── ThemeProvider.tsx │ └── ui │ │ ├── button.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── tooltip.tsx ├── constants │ ├── defaultTheme.ts │ ├── initialState.ts │ └── storageKey.ts ├── context │ └── ThemeProviderContext.tsx ├── hooks │ ├── useConversionHistory.ts │ ├── useMediaQuery.ts │ ├── useThemeContext.ts │ └── useToast.ts ├── layout │ ├── ConversionHistoryLayout.tsx │ ├── MainLayout.tsx │ └── NavbarLayout.tsx ├── lib │ └── utils.ts ├── main.tsx ├── styles │ └── global.css ├── types │ └── index.ts ├── utils │ ├── convertionInfo.ts │ └── copyInputText.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 zshaian 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 | # CSS Unit Playground 2 | 3 | ## Technologies Used 4 | 5 |
6 | vite logo 7 | react logo 8 | typescript logo 9 | tailwind logo 10 | shadcnUI logo 11 |
12 | 13 | --- 14 | 15 | ![cover image](./public/large-og-image.png) 16 | 17 | A simple tool for converting CSS units to between various types like `rem`, `em`, `px`, and other css units. 18 | 19 | ## License 20 | 21 | This project is licensed under the [MIT](./LICENSE) License. 22 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | CSS Units Playground 33 | 34 | 35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-unit-playground", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-dialog": "^1.1.2", 14 | "@radix-ui/react-dropdown-menu": "^2.1.2", 15 | "@radix-ui/react-scroll-area": "^1.2.1", 16 | "@radix-ui/react-select": "^2.1.2", 17 | "@radix-ui/react-slot": "^1.1.0", 18 | "@radix-ui/react-toast": "^1.2.2", 19 | "@radix-ui/react-tooltip": "^1.1.4", 20 | "class-variance-authority": "^0.7.1", 21 | "clsx": "^2.1.1", 22 | "lucide-react": "^0.468.0", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "tailwind-merge": "^2.5.5", 26 | "tailwindcss-animate": "^1.0.7", 27 | "unitflip": "^1.3.0", 28 | "vaul": "^1.1.1", 29 | "zustand": "^5.0.2" 30 | }, 31 | "devDependencies": { 32 | "@eslint/js": "^9.15.0", 33 | "@types/node": "^22.10.1", 34 | "@types/react": "^18.3.12", 35 | "@types/react-dom": "^18.3.1", 36 | "@vitejs/plugin-react": "^4.3.4", 37 | "autoprefixer": "^10.4.20", 38 | "eslint": "^9.15.0", 39 | "eslint-plugin-react-hooks": "^5.0.0", 40 | "eslint-plugin-react-refresh": "^0.4.14", 41 | "globals": "^15.12.0", 42 | "postcss": "^8.4.49", 43 | "tailwindcss": "^3.4.16", 44 | "typescript": "~5.6.2", 45 | "typescript-eslint": "^8.15.0", 46 | "vite": "^6.0.11" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zshaian/css-units-playground/d30df7d4a7abbfb346d42543f2a345673788c0c6/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zshaian/css-units-playground/d30df7d4a7abbfb346d42543f2a345673788c0c6/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zshaian/css-units-playground/d30df7d4a7abbfb346d42543f2a345673788c0c6/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/large-og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zshaian/css-units-playground/d30df7d4a7abbfb346d42543f2a345673788c0c6/public/large-og-image.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zshaian/css-units-playground/d30df7d4a7abbfb346d42543f2a345673788c0c6/public/logo.png -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MyWebSite", 3 | "short_name": "MySite", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zshaian/css-units-playground/d30df7d4a7abbfb346d42543f2a345673788c0c6/public/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /public/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zshaian/css-units-playground/d30df7d4a7abbfb346d42543f2a345673788c0c6/public/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import ConversionInfo from "@/components/ConversionInfo"; 2 | import SaveToHistoryButton from "@/components/SaveToHistoryButton"; 3 | import UnitInput from "@/components/UnitInput"; 4 | import UnitSelect from "@/components/UnitSelect"; 5 | import BaseFontSize from "@/components/BaseFontSize"; 6 | import MainLayout from "./layout/MainLayout"; 7 | import { ArrowUpDown } from "lucide-react"; 8 | import { useState } from "react"; 9 | import type { UnitState, ValidUnits } from "./types"; 10 | import { INITIAL_UNIT_STATE_VALUE } from "./constants/initialState"; 11 | import { getConvertionInfo } from "./utils/convertionInfo"; 12 | import ConversionHistoryLayout from "./layout/ConversionHistoryLayout"; 13 | import ConversionHistory from "./components/ConversionHistory"; 14 | import { useConversionHistory } from "./hooks/useConversionHistory"; 15 | import ThemeProvider from "./components/theme/ThemeProvider"; 16 | import Navbar from "./components/Navbar"; 17 | import NavbarLayout from "./layout/NavbarLayout"; 18 | import { Toaster } from "./components/ui/toaster"; 19 | import { useToast } from "./hooks/useToast"; 20 | import unitFlip from "unitflip"; 21 | 22 | function App() { 23 | const [unitState, setUnitState] = useState( 24 | INITIAL_UNIT_STATE_VALUE 25 | ); 26 | const [conversionHistory, setConversionHistory] = useConversionHistory(); 27 | const { toast } = useToast(); 28 | const validUnitList: ValidUnits[] = [ 29 | "px", 30 | "rem", 31 | "em", 32 | "%", 33 | "vw", 34 | "vh", 35 | "vmin", 36 | "vmax", 37 | ]; 38 | 39 | const handleBaseFontSizeChange = ( 40 | value: string, 41 | name: "baseFontSize" | "rootFontSize" 42 | ) => { 43 | const parsedNewBaseFontSize = parseFloat(value); 44 | 45 | if (isNaN(parsedNewBaseFontSize)) { 46 | setUnitState((previousState) => ({ ...previousState, [name]: "" })); 47 | return; 48 | } 49 | 50 | setUnitState((previousState) => ({ 51 | ...previousState, 52 | fromUnitValue: "", 53 | toUnitValue: "", 54 | [name]: parsedNewBaseFontSize, 55 | })); 56 | }; 57 | 58 | const handleUnitValueChange = ( 59 | newUnitValue: string, 60 | name: "fromUnitValue" | "toUnitValue" 61 | ) => { 62 | const parsedNewUnitValue = parseFloat(newUnitValue); 63 | 64 | if (isNaN(parsedNewUnitValue)) { 65 | setUnitState((previousState) => ({ 66 | ...previousState, 67 | fromUnitValue: "", 68 | toUnitValue: "", 69 | })); 70 | return; 71 | } 72 | 73 | const isFromUnitValue = name === "fromUnitValue"; 74 | 75 | setUnitState((previousState) => { 76 | const sourceUnit = isFromUnitValue 77 | ? previousState.fromUnit 78 | : previousState.toUnit; 79 | const targetUnit = isFromUnitValue 80 | ? previousState.toUnit 81 | : previousState.fromUnit; 82 | const convertedUnitValue = unitFlip( 83 | parsedNewUnitValue, 84 | sourceUnit, 85 | targetUnit, 86 | 2, 87 | { 88 | rootFontSize: (previousState.rootFontSize || 0) as number, 89 | baseFontSize: (previousState.baseFontSize || 0) as number, 90 | } 91 | ); 92 | return { 93 | ...previousState, 94 | [name]: parsedNewUnitValue, 95 | [isFromUnitValue ? "toUnitValue" : "fromUnitValue"]: convertedUnitValue, 96 | }; 97 | }); 98 | }; 99 | 100 | const handleUnitChange = ( 101 | newUnitValue: ValidUnits, 102 | name: "fromUnit" | "toUnit" 103 | ) => { 104 | setUnitState((previousState) => ({ 105 | ...previousState, 106 | toUnitValue: "", 107 | fromUnitValue: "", 108 | [name]: newUnitValue, 109 | })); 110 | }; 111 | 112 | const handleSaveToHistoryConversion = () => { 113 | try { 114 | setConversionHistory((previousHistory) => [ 115 | ...previousHistory, 116 | unitState, 117 | ]); 118 | toast({ description: "Conversion Value is Added to History" }); 119 | } catch (error) { 120 | console.error(error); 121 | toast({ 122 | description: "Failed to Save Conversion Value to History.", 123 | variant: "destructive", 124 | }); 125 | } 126 | }; 127 | 128 | const handleClearConversionHistory = () => { 129 | try { 130 | setConversionHistory(() => []); 131 | toast({ description: "Conversion History Has Been Cleared." }); 132 | } catch (error) { 133 | console.error(error); 134 | toast({ 135 | description: "Failed to Clear the Conversion History.", 136 | variant: "destructive", 137 | }); 138 | } 139 | }; 140 | 141 | return ( 142 | <> 143 | 144 | 145 | 149 | 150 |
151 | 152 | 153 | 154 | 155 | 156 |
157 | 163 | handleBaseFontSizeChange(event.target.value, "rootFontSize") 164 | } 165 | /> 166 | 172 | handleBaseFontSizeChange(event.target.value, "baseFontSize") 173 | } 174 | /> 175 |
176 |
177 | 181 | handleUnitValueChange(event.target.value, "fromUnitValue") 182 | } 183 | /> 184 | handleUnitChange(value, "fromUnit")} 189 | /> 190 |
191 | 192 |
193 | 197 | handleUnitValueChange(event.target.value, "toUnitValue") 198 | } 199 | /> 200 | handleUnitChange(value, "toUnit")} 205 | /> 206 |
207 | 210 | 216 |
217 |
218 | 219 | 220 | ); 221 | } 222 | 223 | export default App; 224 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/BaseFontSize.tsx: -------------------------------------------------------------------------------- 1 | import FontSizeTip from "./FontSizeTip"; 2 | 3 | import { Input } from "./ui/input"; 4 | 5 | interface BaseFontSizeProps { 6 | name:string; 7 | tipDescription: string; 8 | componentLabel:string; 9 | baseFontSize: number | string; 10 | handleChangeBaseFontSize: ( 11 | event: React.ChangeEvent 12 | ) => void; 13 | } 14 | 15 | const BaseFontSize: React.FC = ({ 16 | name, 17 | tipDescription, 18 | componentLabel, 19 | baseFontSize, 20 | handleChangeBaseFontSize, 21 | }) => { 22 | return ( 23 |

24 | 25 | {componentLabel} Font Size 26 | 33 | Pixel 34 |

35 | ); 36 | }; 37 | 38 | export default BaseFontSize; 39 | -------------------------------------------------------------------------------- /src/components/ConversionHistory.tsx: -------------------------------------------------------------------------------- 1 | import { MoveRight, Paintbrush } from "lucide-react"; 2 | import { ScrollArea, ScrollBar } from "./ui/scroll-area"; 3 | import type { UnitState } from "@/types"; 4 | import CopyButton from "./CopyButton"; 5 | import { Button } from "./ui/button"; 6 | 7 | interface ConversionItemProps { 8 | value: number | string; 9 | unit: string; 10 | } 11 | 12 | interface BaseFontSizeProps { 13 | baseFontSize: number | string; 14 | baseType: "Root" | "Base"; 15 | } 16 | 17 | interface ConversionHistoryProps { 18 | conversionHistoryList: UnitState[]; 19 | handleClearConversionHistory:() => void; 20 | } 21 | 22 | const ConversionHistory: React.FC = (props) => { 23 | return ( 24 |
25 |
26 | 27 | History 28 | 29 | 38 |
39 |
40 | {props.conversionHistoryList.map((item, index) => ( 41 |
45 |
46 | 47 | 48 | 49 |
50 |
51 | 52 | 53 |
54 |
55 | ))} 56 |
57 |
58 | ); 59 | }; 60 | 61 | const ConversionItem: React.FC = (props) => ( 62 |
63 | 64 | 65 | {props.value === "" ? 0 : props.value} 66 | 67 | 68 | 69 | {props.unit} 70 | 71 |
72 | ); 73 | 74 | const BaseFontSize: React.FC = (props) => ( 75 |

76 | {props.baseType} Font Size:{" "} 77 | 78 | {props.baseFontSize === "" ? 0 : props.baseFontSize}PX 79 | 80 |

81 | ); 82 | 83 | export default ConversionHistory; 84 | -------------------------------------------------------------------------------- /src/components/ConversionInfo.tsx: -------------------------------------------------------------------------------- 1 | import type { ConvertionInfoValue } from "@/types"; 2 | 3 | const ConversionInfo: React.FC = (props) => { 4 | return ( 5 |
6 |

7 | Formula in converting 8 | 9 | {props.fromUnit} 10 | 11 | to 12 | 13 | {props.toUnit} 14 | 15 |

16 |

{props.formula}

17 |

18 | {props.calculation} 19 |

20 |
21 | ); 22 | }; 23 | 24 | export default ConversionInfo; 25 | -------------------------------------------------------------------------------- /src/components/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | import { Copy } from "lucide-react"; 2 | import { Button } from "./ui/button"; 3 | import { useToast } from "@/hooks/useToast"; 4 | import { CopyText } from "@/utils/copyInputText"; 5 | 6 | interface CopyButtonProps { 7 | value: string; 8 | } 9 | 10 | const CopyButton: React.FC = (props) => { 11 | const { toast } = useToast(); 12 | const onSuccessCopy = () => { 13 | toast({ 14 | description: "Text copied successfully!", 15 | }); 16 | }; 17 | const onErrorCopy = () => { 18 | toast({ 19 | description: "Error Copying The Value", 20 | variant: "destructive", 21 | }); 22 | }; 23 | 24 | return ( 25 | 31 | ); 32 | }; 33 | 34 | export default CopyButton; 35 | -------------------------------------------------------------------------------- /src/components/FontSizeTip.tsx: -------------------------------------------------------------------------------- 1 | import { Info } from "lucide-react"; 2 | import { 3 | Tooltip, 4 | TooltipProvider, 5 | TooltipTrigger, 6 | TooltipContent, 7 | } from "./ui/tooltip"; 8 | 9 | interface FontSizeTipProps { 10 | tipDescripion: string; 11 | } 12 | 13 | const FontSizeTip: React.FC = (props) => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 |

{props.tipDescripion}

21 |
22 |
23 |
24 | ); 25 | 26 | export default FontSizeTip; 27 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { Github } from "lucide-react"; 2 | import ThemeToggle from "./ThemeToggle"; 3 | 4 | const Navbar: React.FC = () => ( 5 | <> 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | 13 | export default Navbar; 14 | -------------------------------------------------------------------------------- /src/components/SaveToHistoryButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "./ui/button"; 2 | 3 | interface SaveToHistoryButtonProps { 4 | handleSaveToHistoryEvent:() => void; 5 | } 6 | 7 | const SaveToHistoryButton: React.FC = (props) => { 8 | return ( 9 | 12 | ); 13 | }; 14 | 15 | export default SaveToHistoryButton; 16 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { Sun,Moon } from "lucide-react"; 2 | import { ThemeProviderContext } from "@/context/ThemeProviderContext"; 3 | import { useContext } from "react"; 4 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; 5 | import { Button } from "./ui/button"; 6 | 7 | const ThemeToggle:React.FC = () => { 8 | const [theme,setTheme] = useContext(ThemeProviderContext); 9 | 10 | return ( 11 | 12 | 13 | 17 | 18 | 19 | setTheme("light")}> 20 | Light 21 | 22 | setTheme("dark")}> 23 | Dark 24 | 25 | setTheme("system")}> 26 | System 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default ThemeToggle; -------------------------------------------------------------------------------- /src/components/UnitInput.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "./ui/input"; 2 | import CopyButton from "./CopyButton"; 3 | 4 | interface UnitInputProps { 5 | name:string; 6 | unitValue:string | number; 7 | handleUnitValueChange:(event:React.ChangeEvent) => void; 8 | } 9 | 10 | const UnitInput:React.FC = ({name,unitValue,handleUnitValueChange}) => ( 11 |
12 | 19 | 20 |
21 | ); 22 | 23 | export default UnitInput; 24 | -------------------------------------------------------------------------------- /src/components/UnitSelect.tsx: -------------------------------------------------------------------------------- 1 | import type { ValidUnits } from "@/types"; 2 | import { 3 | Select, 4 | SelectTrigger, 5 | SelectValue, 6 | SelectContent, 7 | SelectItem, 8 | } from "./ui/select"; 9 | 10 | interface UnitSelectProps { 11 | name:string; 12 | selectedItem: ValidUnits; 13 | selectItems: ValidUnits[]; 14 | handleUnitChange: (value: ValidUnits) => void; 15 | } 16 | 17 | const UnitSelect: React.FC = ({ 18 | name, 19 | selectItems, 20 | selectedItem, 21 | handleUnitChange, 22 | }) => { 23 | return ( 24 | 36 | ); 37 | }; 38 | 39 | export default UnitSelect; 40 | -------------------------------------------------------------------------------- /src/components/theme/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProviderContext } from "@/context/ThemeProviderContext"; 2 | import type { ThemeProviderProps } from "@/types"; 3 | import { useThemeContext } from "@/hooks/useThemeContext"; 4 | 5 | function ThemeProvider({ children }: ThemeProviderProps) { 6 | const [theme, setTheme] = useThemeContext(); 7 | 8 | return ( 9 | 10 | {children} 11 | 12 | ); 13 | } 14 | 15 | export default ThemeProvider; 16 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /src/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Drawer as DrawerPrimitive } from "vaul" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Drawer = ({ 7 | shouldScaleBackground = true, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 14 | ) 15 | Drawer.displayName = "Drawer" 16 | 17 | const DrawerTrigger = DrawerPrimitive.Trigger 18 | 19 | const DrawerPortal = DrawerPrimitive.Portal 20 | 21 | const DrawerClose = DrawerPrimitive.Close 22 | 23 | const DrawerOverlay = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName 34 | 35 | const DrawerContent = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, children, ...props }, ref) => ( 39 | 40 | 41 | 49 |
50 | {children} 51 | 52 | 53 | )) 54 | DrawerContent.displayName = "DrawerContent" 55 | 56 | const DrawerHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
64 | ) 65 | DrawerHeader.displayName = "DrawerHeader" 66 | 67 | const DrawerFooter = ({ 68 | className, 69 | ...props 70 | }: React.HTMLAttributes) => ( 71 |
75 | ) 76 | DrawerFooter.displayName = "DrawerFooter" 77 | 78 | const DrawerTitle = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef 81 | >(({ className, ...props }, ref) => ( 82 | 90 | )) 91 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName 92 | 93 | const DrawerDescription = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, ...props }, ref) => ( 97 | 102 | )) 103 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName 104 | 105 | export { 106 | Drawer, 107 | DrawerPortal, 108 | DrawerOverlay, 109 | DrawerTrigger, 110 | DrawerClose, 111 | DrawerContent, 112 | DrawerHeader, 113 | DrawerFooter, 114 | DrawerTitle, 115 | DrawerDescription, 116 | } 117 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 3 | import { Check, ChevronRight, Circle } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const DropdownMenu = DropdownMenuPrimitive.Root 8 | 9 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 10 | 11 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 12 | 13 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 14 | 15 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 16 | 17 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 18 | 19 | const DropdownMenuSubTrigger = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef & { 22 | inset?: boolean 23 | } 24 | >(({ className, inset, children, ...props }, ref) => ( 25 | 34 | {children} 35 | 36 | 37 | )) 38 | DropdownMenuSubTrigger.displayName = 39 | DropdownMenuPrimitive.SubTrigger.displayName 40 | 41 | const DropdownMenuSubContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, ...props }, ref) => ( 45 | 53 | )) 54 | DropdownMenuSubContent.displayName = 55 | DropdownMenuPrimitive.SubContent.displayName 56 | 57 | const DropdownMenuContent = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, sideOffset = 4, ...props }, ref) => ( 61 | 62 | 72 | 73 | )) 74 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 75 | 76 | const DropdownMenuItem = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef & { 79 | inset?: boolean 80 | } 81 | >(({ className, inset, ...props }, ref) => ( 82 | svg]:size-4 [&>svg]:shrink-0", 86 | inset && "pl-8", 87 | className 88 | )} 89 | {...props} 90 | /> 91 | )) 92 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 93 | 94 | const DropdownMenuCheckboxItem = React.forwardRef< 95 | React.ElementRef, 96 | React.ComponentPropsWithoutRef 97 | >(({ className, children, checked, ...props }, ref) => ( 98 | 107 | 108 | 109 | 110 | 111 | 112 | {children} 113 | 114 | )) 115 | DropdownMenuCheckboxItem.displayName = 116 | DropdownMenuPrimitive.CheckboxItem.displayName 117 | 118 | const DropdownMenuRadioItem = React.forwardRef< 119 | React.ElementRef, 120 | React.ComponentPropsWithoutRef 121 | >(({ className, children, ...props }, ref) => ( 122 | 130 | 131 | 132 | 133 | 134 | 135 | {children} 136 | 137 | )) 138 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 139 | 140 | const DropdownMenuLabel = React.forwardRef< 141 | React.ElementRef, 142 | React.ComponentPropsWithoutRef & { 143 | inset?: boolean 144 | } 145 | >(({ className, inset, ...props }, ref) => ( 146 | 155 | )) 156 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 157 | 158 | const DropdownMenuSeparator = React.forwardRef< 159 | React.ElementRef, 160 | React.ComponentPropsWithoutRef 161 | >(({ className, ...props }, ref) => ( 162 | 167 | )) 168 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 169 | 170 | const DropdownMenuShortcut = ({ 171 | className, 172 | ...props 173 | }: React.HTMLAttributes) => { 174 | return ( 175 | 179 | ) 180 | } 181 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 182 | 183 | export { 184 | DropdownMenu, 185 | DropdownMenuTrigger, 186 | DropdownMenuContent, 187 | DropdownMenuItem, 188 | DropdownMenuCheckboxItem, 189 | DropdownMenuRadioItem, 190 | DropdownMenuLabel, 191 | DropdownMenuSeparator, 192 | DropdownMenuShortcut, 193 | DropdownMenuGroup, 194 | DropdownMenuPortal, 195 | DropdownMenuSub, 196 | DropdownMenuSubContent, 197 | DropdownMenuSubTrigger, 198 | DropdownMenuRadioGroup, 199 | } 200 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | )) 22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 23 | 24 | const ScrollBar = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, orientation = "vertical", ...props }, ref) => ( 28 | 41 | 42 | 43 | )) 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 45 | 46 | export { ScrollArea, ScrollBar } 47 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SelectPrimitive from "@radix-ui/react-select" 3 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Select = SelectPrimitive.Root 8 | 9 | const SelectGroup = SelectPrimitive.Group 10 | 11 | const SelectValue = SelectPrimitive.Value 12 | 13 | const SelectTrigger = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, children, ...props }, ref) => ( 17 | span]:line-clamp-1", 21 | className 22 | )} 23 | {...props} 24 | > 25 | {children} 26 | 27 | 28 | 29 | 30 | )) 31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 32 | 33 | const SelectScrollUpButton = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | 46 | 47 | )) 48 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 49 | 50 | const SelectScrollDownButton = React.forwardRef< 51 | React.ElementRef, 52 | React.ComponentPropsWithoutRef 53 | >(({ className, ...props }, ref) => ( 54 | 62 | 63 | 64 | )) 65 | SelectScrollDownButton.displayName = 66 | SelectPrimitive.ScrollDownButton.displayName 67 | 68 | const SelectContent = React.forwardRef< 69 | React.ElementRef, 70 | React.ComponentPropsWithoutRef 71 | >(({ className, children, position = "popper", ...props }, ref) => ( 72 | 73 | 84 | 85 | 92 | {children} 93 | 94 | 95 | 96 | 97 | )) 98 | SelectContent.displayName = SelectPrimitive.Content.displayName 99 | 100 | const SelectLabel = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, ...props }, ref) => ( 104 | 109 | )) 110 | SelectLabel.displayName = SelectPrimitive.Label.displayName 111 | 112 | const SelectItem = React.forwardRef< 113 | React.ElementRef, 114 | React.ComponentPropsWithoutRef 115 | >(({ className, children, ...props }, ref) => ( 116 | 124 | 125 | 126 | 127 | 128 | 129 | {children} 130 | 131 | )) 132 | SelectItem.displayName = SelectPrimitive.Item.displayName 133 | 134 | const SelectSeparator = React.forwardRef< 135 | React.ElementRef, 136 | React.ComponentPropsWithoutRef 137 | >(({ className, ...props }, ref) => ( 138 | 143 | )) 144 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 145 | 146 | export { 147 | Select, 148 | SelectGroup, 149 | SelectValue, 150 | SelectTrigger, 151 | SelectContent, 152 | SelectLabel, 153 | SelectItem, 154 | SelectSeparator, 155 | SelectScrollUpButton, 156 | SelectScrollDownButton, 157 | } 158 | -------------------------------------------------------------------------------- /src/components/ui/toast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ToastPrimitives from "@radix-ui/react-toast" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | import { X } from "lucide-react" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ToastProvider = ToastPrimitives.Provider 9 | 10 | const ToastViewport = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName 24 | 25 | const toastVariants = cva( 26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 27 | { 28 | variants: { 29 | variant: { 30 | default: "border bg-background text-foreground", 31 | destructive: 32 | "destructive group border-destructive bg-destructive text-destructive-foreground", 33 | }, 34 | }, 35 | defaultVariants: { 36 | variant: "default", 37 | }, 38 | } 39 | ) 40 | 41 | const Toast = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef & 44 | VariantProps 45 | >(({ className, variant, ...props }, ref) => { 46 | return ( 47 | 52 | ) 53 | }) 54 | Toast.displayName = ToastPrimitives.Root.displayName 55 | 56 | const ToastAction = React.forwardRef< 57 | React.ElementRef, 58 | React.ComponentPropsWithoutRef 59 | >(({ className, ...props }, ref) => ( 60 | 68 | )) 69 | ToastAction.displayName = ToastPrimitives.Action.displayName 70 | 71 | const ToastClose = React.forwardRef< 72 | React.ElementRef, 73 | React.ComponentPropsWithoutRef 74 | >(({ className, ...props }, ref) => ( 75 | 84 | 85 | 86 | )) 87 | ToastClose.displayName = ToastPrimitives.Close.displayName 88 | 89 | const ToastTitle = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => ( 93 | 98 | )) 99 | ToastTitle.displayName = ToastPrimitives.Title.displayName 100 | 101 | const ToastDescription = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | ToastDescription.displayName = ToastPrimitives.Description.displayName 112 | 113 | type ToastProps = React.ComponentPropsWithoutRef 114 | 115 | type ToastActionElement = React.ReactElement 116 | 117 | export { 118 | type ToastProps, 119 | type ToastActionElement, 120 | ToastProvider, 121 | ToastViewport, 122 | Toast, 123 | ToastTitle, 124 | ToastDescription, 125 | ToastClose, 126 | ToastAction, 127 | } 128 | -------------------------------------------------------------------------------- /src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | import { useToast } from "@/hooks/useToast"; 2 | import { 3 | Toast, 4 | ToastClose, 5 | ToastDescription, 6 | ToastProvider, 7 | ToastTitle, 8 | ToastViewport, 9 | } from "@/components/ui/toast"; 10 | 11 | export function Toaster() { 12 | const { toasts } = useToast(); 13 | 14 | return ( 15 | 16 | {toasts.map(function ({ id, title, description, action, ...props }) { 17 | return ( 18 | 19 |
20 | {title && {title}} 21 | {description && ( 22 | {description} 23 | )} 24 |
25 | {action} 26 | 27 |
28 | ); 29 | })} 30 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider 7 | 8 | const Tooltip = TooltipPrimitive.Root 9 | 10 | const TooltipTrigger = TooltipPrimitive.Trigger 11 | 12 | const TooltipContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, sideOffset = 4, ...props }, ref) => ( 16 | 17 | 26 | 27 | )) 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 31 | -------------------------------------------------------------------------------- /src/constants/defaultTheme.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "@/types"; 2 | 3 | export const DEFAULT_THEME:Theme = "system"; -------------------------------------------------------------------------------- /src/constants/initialState.ts: -------------------------------------------------------------------------------- 1 | import type { UnitState } from "@/types"; 2 | 3 | export const INITIAL_UNIT_STATE_VALUE: UnitState = { 4 | fromUnitValue: 16, 5 | fromUnit: "px", 6 | toUnitValue: 1, 7 | toUnit: "rem", 8 | rootFontSize: 16, 9 | baseFontSize: 16, 10 | }; 11 | -------------------------------------------------------------------------------- /src/constants/storageKey.ts: -------------------------------------------------------------------------------- 1 | export const STORAGE_KEY = "conversionUnitHistory"; 2 | export const THEME_KEY = "vite-ui-theme"; -------------------------------------------------------------------------------- /src/context/ThemeProviderContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | import type { ThemeProviderState } from "@/types"; 3 | 4 | const initialThemeState: ThemeProviderState = ["system",() => null]; 5 | 6 | export const ThemeProviderContext = 7 | createContext(initialThemeState); 8 | 9 | export const useTheme = () => { 10 | const context = useContext(ThemeProviderContext); 11 | 12 | if (context === undefined) 13 | throw new Error("useTheme must be used within a ThemeProvider"); 14 | 15 | return context; 16 | }; 17 | -------------------------------------------------------------------------------- /src/hooks/useConversionHistory.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { STORAGE_KEY } from "@/constants/storageKey"; 3 | import type { UnitState } from "@/types"; 4 | 5 | const useConversionHistory = (): [ 6 | UnitState[], 7 | React.Dispatch> 8 | ] => { 9 | const localConversionHistory = localStorage.getItem(STORAGE_KEY); 10 | const initialConversionHistory = localConversionHistory 11 | ? JSON.parse(localConversionHistory) 12 | : []; 13 | const [conversionHistory, setConversionHistory] = useState( 14 | initialConversionHistory 15 | ); 16 | 17 | useEffect(() => { 18 | localStorage.setItem(STORAGE_KEY, JSON.stringify(conversionHistory)); 19 | }, [conversionHistory]); 20 | 21 | return [conversionHistory, setConversionHistory]; 22 | }; 23 | 24 | export { useConversionHistory }; 25 | -------------------------------------------------------------------------------- /src/hooks/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useMediaQuery = (breakpoint: string): boolean => { 4 | const [isDesktop, setIsDesktop] = useState( 5 | window.matchMedia(breakpoint).matches 6 | ); 7 | 8 | useEffect(() => { 9 | const mediaQueryList = window.matchMedia(breakpoint); 10 | 11 | const handleChange = (event: MediaQueryListEvent) => { 12 | setIsDesktop(event.matches); 13 | }; 14 | 15 | mediaQueryList.addEventListener("change", handleChange); 16 | 17 | return () => mediaQueryList.removeEventListener("change", handleChange); 18 | }, [breakpoint]); 19 | 20 | return isDesktop; 21 | }; 22 | -------------------------------------------------------------------------------- /src/hooks/useThemeContext.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_THEME } from "@/constants/defaultTheme"; 2 | import { THEME_KEY } from "@/constants/storageKey"; 3 | import type { Theme } from "@/types"; 4 | import { useEffect, useState } from "react"; 5 | 6 | const useThemeContext = (): [Theme,React.Dispatch>] => { 7 | const [theme, setTheme] = useState( 8 | (localStorage.getItem(THEME_KEY) as Theme) || DEFAULT_THEME 9 | ); 10 | 11 | useEffect(() => { 12 | const rootElement = window.document.documentElement; 13 | 14 | rootElement.classList.remove("light", "dark"); 15 | 16 | if (theme === "system") { 17 | const systemTheme = window.matchMedia("(prefers-color-scheme:dark)") 18 | .matches 19 | ? "dark" 20 | : "light"; 21 | rootElement.classList.add(systemTheme); 22 | localStorage.setItem(THEME_KEY,systemTheme); 23 | return; 24 | } 25 | 26 | rootElement.classList.add(theme); 27 | localStorage.setItem(THEME_KEY,theme); 28 | }, [theme]); 29 | 30 | return [theme,setTheme]; 31 | }; 32 | 33 | export { useThemeContext }; 34 | -------------------------------------------------------------------------------- /src/hooks/useToast.ts: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | // Inspired by react-hot-toast library 4 | import * as React from "react" 5 | 6 | import type { 7 | ToastActionElement, 8 | ToastProps, 9 | } from "@/components/ui/toast" 10 | 11 | const TOAST_LIMIT = 1 12 | const TOAST_REMOVE_DELAY = 1000000 13 | 14 | type ToasterToast = ToastProps & { 15 | id: string 16 | title?: React.ReactNode 17 | description?: React.ReactNode 18 | action?: ToastActionElement 19 | } 20 | 21 | const actionTypes = { 22 | ADD_TOAST: "ADD_TOAST", 23 | UPDATE_TOAST: "UPDATE_TOAST", 24 | DISMISS_TOAST: "DISMISS_TOAST", 25 | REMOVE_TOAST: "REMOVE_TOAST", 26 | } as const 27 | 28 | let count = 0 29 | 30 | function genId() { 31 | count = (count + 1) % Number.MAX_SAFE_INTEGER 32 | return count.toString() 33 | } 34 | 35 | type ActionType = typeof actionTypes 36 | 37 | type Action = 38 | | { 39 | type: ActionType["ADD_TOAST"] 40 | toast: ToasterToast 41 | } 42 | | { 43 | type: ActionType["UPDATE_TOAST"] 44 | toast: Partial 45 | } 46 | | { 47 | type: ActionType["DISMISS_TOAST"] 48 | toastId?: ToasterToast["id"] 49 | } 50 | | { 51 | type: ActionType["REMOVE_TOAST"] 52 | toastId?: ToasterToast["id"] 53 | } 54 | 55 | interface State { 56 | toasts: ToasterToast[] 57 | } 58 | 59 | const toastTimeouts = new Map>() 60 | 61 | const addToRemoveQueue = (toastId: string) => { 62 | if (toastTimeouts.has(toastId)) { 63 | return 64 | } 65 | 66 | const timeout = setTimeout(() => { 67 | toastTimeouts.delete(toastId) 68 | dispatch({ 69 | type: "REMOVE_TOAST", 70 | toastId: toastId, 71 | }) 72 | }, TOAST_REMOVE_DELAY) 73 | 74 | toastTimeouts.set(toastId, timeout) 75 | } 76 | 77 | export const reducer = (state: State, action: Action): State => { 78 | switch (action.type) { 79 | case "ADD_TOAST": 80 | return { 81 | ...state, 82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 83 | } 84 | 85 | case "UPDATE_TOAST": 86 | return { 87 | ...state, 88 | toasts: state.toasts.map((t) => 89 | t.id === action.toast.id ? { ...t, ...action.toast } : t 90 | ), 91 | } 92 | 93 | case "DISMISS_TOAST": { 94 | const { toastId } = action 95 | 96 | // ! Side effects ! - This could be extracted into a dismissToast() action, 97 | // but I'll keep it here for simplicity 98 | if (toastId) { 99 | addToRemoveQueue(toastId) 100 | } else { 101 | state.toasts.forEach((toast) => { 102 | addToRemoveQueue(toast.id) 103 | }) 104 | } 105 | 106 | return { 107 | ...state, 108 | toasts: state.toasts.map((t) => 109 | t.id === toastId || toastId === undefined 110 | ? { 111 | ...t, 112 | open: false, 113 | } 114 | : t 115 | ), 116 | } 117 | } 118 | case "REMOVE_TOAST": 119 | if (action.toastId === undefined) { 120 | return { 121 | ...state, 122 | toasts: [], 123 | } 124 | } 125 | return { 126 | ...state, 127 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 128 | } 129 | } 130 | } 131 | 132 | const listeners: Array<(state: State) => void> = [] 133 | 134 | let memoryState: State = { toasts: [] } 135 | 136 | function dispatch(action: Action) { 137 | memoryState = reducer(memoryState, action) 138 | listeners.forEach((listener) => { 139 | listener(memoryState) 140 | }) 141 | } 142 | 143 | type Toast = Omit 144 | 145 | function toast({ ...props }: Toast) { 146 | const id = genId() 147 | 148 | const update = (props: ToasterToast) => 149 | dispatch({ 150 | type: "UPDATE_TOAST", 151 | toast: { ...props, id }, 152 | }) 153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) 154 | 155 | dispatch({ 156 | type: "ADD_TOAST", 157 | toast: { 158 | ...props, 159 | id, 160 | open: true, 161 | onOpenChange: (open) => { 162 | if (!open) dismiss() 163 | }, 164 | }, 165 | }) 166 | 167 | return { 168 | id: id, 169 | dismiss, 170 | update, 171 | } 172 | } 173 | 174 | function useToast() { 175 | const [state, setState] = React.useState(memoryState) 176 | 177 | React.useEffect(() => { 178 | listeners.push(setState) 179 | return () => { 180 | const index = listeners.indexOf(setState) 181 | if (index > -1) { 182 | listeners.splice(index, 1) 183 | } 184 | } 185 | }, [state]) 186 | 187 | return { 188 | ...state, 189 | toast, 190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 191 | } 192 | } 193 | 194 | export { useToast, toast } 195 | -------------------------------------------------------------------------------- /src/layout/ConversionHistoryLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer, DrawerContent, DrawerDescription, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; 2 | import { Button } from "@/components/ui/button"; 3 | import { ScrollArea } from "@/components/ui/scroll-area"; 4 | 5 | import { useMediaQuery } from "@/hooks/useMediaQuery"; 6 | 7 | interface ConversionHistoryLayoutProps { 8 | children: React.ReactNode; 9 | } 10 | 11 | const ConversionHistoryLayout: React.FC = ( 12 | props 13 | ) => { 14 | const isDesktop = useMediaQuery("(min-width:1024px)"); 15 | 16 | if (isDesktop) { 17 | return ( 18 | 19 |
{props.children}
20 |
21 | ); 22 | } 23 | 24 | return ( 25 | 26 | 27 | 30 | 31 | 32 | History 33 | this is a collection of conversion history 34 | 35 | {props.children} 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default ConversionHistoryLayout; 43 | -------------------------------------------------------------------------------- /src/layout/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | interface MainLayoutProps { 2 | children: React.ReactNode; 3 | } 4 | 5 | const MainLayout: React.FC = (props) => { 6 | return ( 7 | <> 8 |
9 | {props.children} 10 |
11 | 12 | ); 13 | }; 14 | 15 | export default MainLayout; 16 | -------------------------------------------------------------------------------- /src/layout/NavbarLayout.tsx: -------------------------------------------------------------------------------- 1 | interface NavbarLayoutProps { 2 | children:React.ReactNode; 3 | } 4 | 5 | const NavbarLayout:React.FC = (props) => ( 6 | 9 | ) 10 | 11 | export default NavbarLayout; -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import "./styles/global.css" 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 10% 3.9%; 26 | --chart-1: 12 76% 61%; 27 | --chart-2: 173 58% 39%; 28 | --chart-3: 197 37% 24%; 29 | --chart-4: 43 74% 66%; 30 | --chart-5: 27 87% 67%; 31 | --radius: 0.5rem; 32 | } 33 | .dark { 34 | --background: 240 10% 3.9%; 35 | --foreground: 0 0% 98%; 36 | --card: 240 10% 3.9%; 37 | --card-foreground: 0 0% 98%; 38 | --popover: 240 10% 3.9%; 39 | --popover-foreground: 0 0% 98%; 40 | --primary: 0 0% 98%; 41 | --primary-foreground: 240 5.9% 10%; 42 | --secondary: 240 3.7% 15.9%; 43 | --secondary-foreground: 0 0% 98%; 44 | --muted: 240 3.7% 15.9%; 45 | --muted-foreground: 240 5% 64.9%; 46 | --accent: 240 3.7% 15.9%; 47 | --accent-foreground: 0 0% 98%; 48 | --destructive: 0 62.8% 30.6%; 49 | --destructive-foreground: 0 0% 98%; 50 | --border: 240 3.7% 15.9%; 51 | --input: 240 3.7% 15.9%; 52 | --ring: 240 4.9% 83.9%; 53 | --chart-1: 220 70% 50%; 54 | --chart-2: 160 60% 45%; 55 | --chart-3: 30 80% 55%; 56 | --chart-4: 280 65% 60%; 57 | --chart-5: 340 75% 55%; 58 | } 59 | } 60 | @layer base { 61 | * { 62 | @apply border-border; 63 | } 64 | body { 65 | @apply bg-background text-foreground; 66 | } 67 | } 68 | 69 | @layer components { 70 | .no-spinner { 71 | @apply [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | type Theme = "dark" | "light" | "system"; 2 | type ThemeProviderState = [Theme,React.Dispatch>] 3 | interface ThemeProviderProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | type ValidUnits = "rem" | "em" | "%" | "px" | "vw" | "vh" | "vmin" | "vmax"; 8 | 9 | interface UnitState { 10 | fromUnitValue: number | string; 11 | fromUnit: ValidUnits; 12 | toUnitValue: number | string; 13 | toUnit: ValidUnits; 14 | rootFontSize: number | string; 15 | baseFontSize: number | string; 16 | } 17 | 18 | interface ConvertionFormulaValue { 19 | formula:string; 20 | calculation:string; 21 | } 22 | 23 | interface ConvertionInfoValue extends ConvertionFormulaValue { 24 | fromUnit: ValidUnits; 25 | toUnit: ValidUnits; 26 | } 27 | 28 | export type { 29 | UnitState, 30 | ConvertionFormulaValue, 31 | ConvertionInfoValue, 32 | ValidUnits, 33 | ThemeProviderProps, 34 | ThemeProviderState, 35 | Theme, 36 | }; 37 | -------------------------------------------------------------------------------- /src/utils/convertionInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConvertionFormulaValue, 3 | ConvertionInfoValue, 4 | ValidUnits, 5 | } from "@/types"; 6 | 7 | interface getConvertionInfoProps { 8 | from: ValidUnits; 9 | to: ValidUnits; 10 | } 11 | 12 | type ConvertionItemsProps = { 13 | [key in ValidUnits]: Record; 14 | }; 15 | 16 | const getConvertionInfo = ({ 17 | from, 18 | to, 19 | }: getConvertionInfoProps): ConvertionInfoValue => { 20 | const convertionItems: ConvertionItemsProps = { 21 | px: { 22 | px: { 23 | formula: "PX = PX", 24 | calculation: "16PX = 16PX", 25 | }, 26 | rem: { 27 | formula: "REM = PX / Root-Font-Size", 28 | calculation: "16px / 16px = 1rem", 29 | }, 30 | em: { 31 | formula: "EM = PX / Base-Font-Size", 32 | calculation: "16px / 16px = 1em", 33 | }, 34 | "%": { 35 | formula: "% = (PX / Base-Font-Size) * 100", 36 | calculation: "(16px / 16px * 100) = 100%", 37 | }, 38 | vw: { 39 | formula: "VW = (PX / VIEWPORTWIDTH) * 100", 40 | calculation: "(16px / 1280px) * 100 = 1.25vw", 41 | }, 42 | vh: { 43 | formula: "VH = (PX / VIEWPORTHEIGHT) * 100", 44 | calculation: "(16px / 743px) * 100 = 2.15vh", 45 | }, 46 | vmin: { 47 | formula: "VMIN = (PX / VMIN) * 100", 48 | calculation: "(16px / 743px) * 100 = 2.15vmin", 49 | }, 50 | vmax: { 51 | formula: "VMAX = (PX / VMAX) * 100", 52 | calculation: "(16px / 1280px) * 100 = 1.25vmax", 53 | }, 54 | }, 55 | rem: { 56 | px: { 57 | formula: "PX = REM * Root-Font-Size", 58 | calculation: "1rem * 16px = 16px", 59 | }, 60 | rem: { 61 | formula: "REM = REM", 62 | calculation: "1rem = 1rem", 63 | }, 64 | em: { 65 | formula: "EM = REM * (Root-Font-Size / Base-Font-Size)", 66 | calculation: "1rem * (16px / 16px) = 1em", 67 | }, 68 | "%": { 69 | formula: "% = ((REM * Root-Font-Size) / Base-Font-Size) * 100", 70 | calculation: "( (1rem * 16px) / 16px ) * 100 = 100%", 71 | }, 72 | vw: { 73 | formula: "VW = (REM * Root-Font-Size / VIEWPORTWIDTH) * 100", 74 | calculation: "(1rem * 16px / 1280px) * 100 = 1.25vw", 75 | }, 76 | vh: { 77 | formula: "VH = (REM * Root-Font-Size / VIEWPORTHEIGHT) * 100", 78 | calculation: "(1rem * 16px / 743px) * 100 = 2.15vh", 79 | }, 80 | vmin: { 81 | formula: "VMIN = (REM * Root-Font-Size / VMIN) * 100", 82 | calculation: "(1rem * 16px / 743px) * 100 = 2.15vmin", 83 | }, 84 | vmax: { 85 | formula: "VMAX = (REM * Root-Font-Size / VMAX) * 100", 86 | calculation: "(1rem * 16px / 1280px) * 100 = 1.25vmax", 87 | }, 88 | }, 89 | em: { 90 | px: { 91 | formula: "PX = EM * Base-Font-Size", 92 | calculation: "1em * 16px = 16px", 93 | }, 94 | rem: { 95 | formula: "REM = EM * (Base-Font-Size / Root-Font-Size)", 96 | calculation: "1em * (16px / 16px) = 1rem", 97 | }, 98 | em: { 99 | formula: "EM = EM", 100 | calculation: "1em = 1em", 101 | }, 102 | "%": { 103 | formula: "% = ((REM * Base-Font-Size) / Base-Font-Size) * 100", 104 | calculation: "((1em * 16px) / 16px) * 100 = 100%", 105 | }, 106 | vw: { 107 | formula: "VW = (EM * Base-Font-Size / VIEWPORTWIDTH) * 100", 108 | calculation: "(1em * 16px / 1280px) * 100 = 1.25vw", 109 | }, 110 | vh: { 111 | formula: "VH = (EM * Base-Font-Size / VIEWPORTHEIGHT) * 100", 112 | calculation: "(1em * 16px / 743px) * 100 = 2.15vh", 113 | }, 114 | vmin: { 115 | formula: "VMIN = (EM * Base-Font-Size / VMIN) * 100", 116 | calculation: "(1em * 16px / 743px) * 100 = 2.15vmin", 117 | }, 118 | vmax: { 119 | formula: "VMAX = (EM * Base-Font-Size / VMAX) * 100", 120 | calculation: "(1em * 16px / 1280px) * 100 = 1.25vmax", 121 | }, 122 | }, 123 | "%": { 124 | px: { 125 | formula: "PX = % * Base-Font-Size / 100", 126 | calculation: "100% * 16px / 100 = 16px", 127 | }, 128 | rem: { 129 | formula: "REM = % * (Base-Font-Size / Root-Font-Size) / 100", 130 | calculation: "100% * (16px / 16px) / 100 = 1rem", 131 | }, 132 | em: { 133 | formula: "EM = % / 100", 134 | calculation: "100% / 100 = 1em", 135 | }, 136 | "%": { 137 | formula: "% = %", 138 | calculation: "100% = 100%", 139 | }, 140 | vw: { 141 | formula: "VW = (% * Base-Font-Size / VIEWPORTWIDTH)", 142 | calculation: "(100% * 16px / 1280px) = 1.25vw", 143 | }, 144 | vh: { 145 | formula: "VH = (% * Base-Font-Size / VIEWPORTHEIGHT)", 146 | calculation: "(100% * 16px / 743px) = 2.15vh", 147 | }, 148 | vmin: { 149 | formula: "VMIN = (% * Base-Font-Size / VMIN)", 150 | calculation: "(100% * 16px / 743px) = 2.15vmin", 151 | }, 152 | vmax: { 153 | formula: "VMAX = (% * Base-Font-Size / VMAX)", 154 | calculation: "(100% * 16px / 1280px) = 1.25vmax", 155 | }, 156 | }, 157 | vw: { 158 | px: { 159 | formula: "PX = VW * VIEWPORTWIDTH / 100", 160 | calculation: "1.25vw * 1280px / 100 = 16px", 161 | }, 162 | rem: { 163 | formula: "REM = (VW * VIEWPORTWIDTH / Root-Font-Size) / 100", 164 | calculation: "(1.25vw * 1280px / 16px) / 100 = 1rem", 165 | }, 166 | em: { 167 | formula: "EM = (VW * VIEWPORTWIDTH / Base-Font-Size) / 100", 168 | calculation: "(1.25vw * 1280px / 16px) / 100 = 1em", 169 | }, 170 | "%": { 171 | formula: "% = (VW * VIEWPORTWIDTH / Base-Font-Size)", 172 | calculation: "(1.25vw * 1280px / 16px) = 100%", 173 | }, 174 | vw: { 175 | formula: "VW = VW", 176 | calculation: "1.25vw = 1.25vw", 177 | }, 178 | vh: { 179 | formula: "VH = (VW * VIEWPORTWIDTH / VIEWPORTHEIGHT)", 180 | calculation: "(1.25vw * 1280px / 743px) = 2.15vh", 181 | }, 182 | vmin: { 183 | formula: "VMIN = (VW * VIEWPORTWIDTH / VMIN)", 184 | calculation: "(1.25vw * 1280px / 743px) = 2.15vmin", 185 | }, 186 | vmax: { 187 | formula: "VMAX = (VW * VIEWPORTWIDTH / VMAX)", 188 | calculation: "(1.25vw * 1280px / 1280px) = 1.25vmax", 189 | }, 190 | }, 191 | vh: { 192 | px: { 193 | formula: "PX = VH * VIEWPORTHEIGHT / 100", 194 | calculation: "2.15vh * 743px / 100 = 16px", 195 | }, 196 | rem: { 197 | formula: "REM = (VH * VIEWPORTHEIGHT / Root-Font-Size) / 100", 198 | calculation: "(2.15vh * 743px / 16px) / 100 = 1rem", 199 | }, 200 | em: { 201 | formula: "EM = (VH * VIEWPORTHEIGHT / Base-Font-Size) / 100", 202 | calculation: "(2.15vh * 743px / 16px) / 100 = 1em", 203 | }, 204 | "%": { 205 | formula: "% = (VH * VIEWPORTHEIGHT / Base-Font-Size)", 206 | calculation: "(2.15vh * 743px / 16px) = 100%", 207 | }, 208 | vw: { 209 | formula: "VW = (VH * VIEWPORTHEIGHT / VIEWPORTWIDTH)", 210 | calculation: "(2.15vh * 743px / 1280px) = 1.25vw", 211 | }, 212 | vh: { 213 | formula: "VH = VH", 214 | calculation: "2.15vh = 2.15vh", 215 | }, 216 | vmin: { 217 | formula: "VMIN = (VH * VIEWPORTHEIGHT / VMIN)", 218 | calculation: "(2.15vh * 743px / 743px) = 2.15vmin", 219 | }, 220 | vmax: { 221 | formula: "VMAX = (VH * VIEWPORTHEIGHT / VMAX)", 222 | calculation: "(2.15vh * 743px / 1280px) = 1.25vmax", 223 | }, 224 | }, 225 | vmin: { 226 | px: { 227 | formula: "PX = VMIN * VMIN / 100", 228 | calculation: "2.15vmin * 743px / 100 = 16px", 229 | }, 230 | rem: { 231 | formula: "REM = (VMIN * VMIN / Root-Font-Size) / 100", 232 | calculation: "(2.15vmin * 743px / 16px) / 100 = 1rem", 233 | }, 234 | em: { 235 | formula: "EM = (VMIN * VMIN / Base-Font-Size) / 100", 236 | calculation: "(2.15vmin * 743px / 16px) / 100 = 1em", 237 | }, 238 | "%": { 239 | formula: "% = (VMIN * VMIN / Base-Font-Size)", 240 | calculation: "(2.15vmin * 743px / 16px) = 100%", 241 | }, 242 | vw: { 243 | formula: "VW = (VMIN * VMIN / VIEWPORTWIDTH)", 244 | calculation: "(2.15vmin * 743px / 1280px) = 1.25vw", 245 | }, 246 | vh: { 247 | formula: "VH = (VMIN * VMIN / VIEWPORTHEIGHT)", 248 | calculation: "(2.15vmin * 743px / 743px) = 2.15vh", 249 | }, 250 | vmin: { 251 | formula: "VMIN = VMIN", 252 | calculation: "2.15vmin = 2.15vmin", 253 | }, 254 | vmax: { 255 | formula: "VMAX = (VMIN * VMIN / VMAX)", 256 | calculation: "(2.15vmin * 743px / 1280px) = 1.25vmax", 257 | }, 258 | }, 259 | vmax: { 260 | px: { 261 | formula: "PX = VMAX * VMAX / 100", 262 | calculation: "1.25vmax * 1280px / 100 = 16px", 263 | }, 264 | rem: { 265 | formula: "REM = (VMAX * VMAX / Root-Font-Size) / 100", 266 | calculation: "(1.25vmax * 1280px / 16px) / 100 = 1rem", 267 | }, 268 | em: { 269 | formula: "EM = (VMAX * VMAX / Base-Font-Size) / 100", 270 | calculation: "(1.25vmax * 1280px / 16px) / 100 = 1em", 271 | }, 272 | "%": { 273 | formula: "% = (VMAX * VMAX / Base-Font-Size)", 274 | calculation: "(1.25vmax * 1280px / 16px) = 100%", 275 | }, 276 | vw: { 277 | formula: "VW = (VMAX * VMAX / VIEWPORTWIDTH)", 278 | calculation: "(1.25vmax * 1280px / 1280px) = 1.25vw", 279 | }, 280 | vh: { 281 | formula: "VH = (VMAX * VMAX / VIEWPORTHEIGHT)", 282 | calculation: "(1.25vmax * 1280px / 743px) = 2.15vh", 283 | }, 284 | vmin: { 285 | formula: "VMIN = (VMAX * VMAX / VMIN)", 286 | calculation: "(1.25vmax * 1280px / 743px) = 2.15vmin", 287 | }, 288 | vmax: { 289 | formula: "VMAX = VMAX", 290 | calculation: "1.25vmax = 1.25vmax", 291 | }, 292 | }, 293 | }; 294 | 295 | return { fromUnit: from, toUnit: to, ...convertionItems[from][to] }; 296 | }; 297 | 298 | export { getConvertionInfo }; 299 | -------------------------------------------------------------------------------- /src/utils/copyInputText.ts: -------------------------------------------------------------------------------- 1 | const CopyText = async ( 2 | inputValue: string, 3 | onSuccess: () => void, 4 | onError: () => void 5 | ): Promise => { 6 | try { 7 | await navigator.clipboard.writeText(inputValue); 8 | onSuccess(); 9 | } catch (error) { 10 | console.error(error); 11 | onError(); 12 | } 13 | }; 14 | 15 | export { CopyText }; 16 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | darkMode: ["class"], 4 | content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], 5 | theme: { 6 | extend: { 7 | borderRadius: { 8 | lg: "var(--radius)", 9 | md: "calc(var(--radius) - 2px)", 10 | sm: "calc(var(--radius) - 4px)", 11 | }, 12 | colors: { 13 | background: "hsl(var(--background))", 14 | foreground: "hsl(var(--foreground))", 15 | card: { 16 | DEFAULT: "hsl(var(--card))", 17 | foreground: "hsl(var(--card-foreground))", 18 | }, 19 | popover: { 20 | DEFAULT: "hsl(var(--popover))", 21 | foreground: "hsl(var(--popover-foreground))", 22 | }, 23 | primary: { 24 | DEFAULT: "hsl(var(--primary))", 25 | foreground: "hsl(var(--primary-foreground))", 26 | }, 27 | secondary: { 28 | DEFAULT: "hsl(var(--secondary))", 29 | foreground: "hsl(var(--secondary-foreground))", 30 | }, 31 | muted: { 32 | DEFAULT: "hsl(var(--muted))", 33 | foreground: "hsl(var(--muted-foreground))", 34 | }, 35 | accent: { 36 | DEFAULT: "hsl(var(--accent))", 37 | foreground: "hsl(var(--accent-foreground))", 38 | }, 39 | destructive: { 40 | DEFAULT: "hsl(var(--destructive))", 41 | foreground: "hsl(var(--destructive-foreground))", 42 | }, 43 | border: "hsl(var(--border))", 44 | input: "hsl(var(--input))", 45 | ring: "hsl(var(--ring))", 46 | chart: { 47 | 1: "hsl(var(--chart-1))", 48 | 2: "hsl(var(--chart-2))", 49 | 3: "hsl(var(--chart-3))", 50 | 4: "hsl(var(--chart-4))", 51 | 5: "hsl(var(--chart-5))", 52 | }, 53 | }, 54 | }, 55 | }, 56 | plugins: [require("tailwindcss-animate")], 57 | }; 58 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | }, 13 | 14 | /* Bundler mode */ 15 | "moduleResolution": "bundler", 16 | "allowImportingTsExtensions": true, 17 | "isolatedModules": true, 18 | "moduleDetection": "force", 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | 22 | /* Linting */ 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "noUncheckedSideEffectImports": true 28 | }, 29 | "include": ["src"] 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /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 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | "@": path.resolve(__dirname, "./src"), 11 | }, 12 | }, 13 | }); 14 | --------------------------------------------------------------------------------