├── .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 |

7 |

8 |

9 |

10 |

11 |
12 |
13 | ---
14 |
15 | 
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 |
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 |
--------------------------------------------------------------------------------