├── src ├── vite-env.d.ts ├── lib │ └── utils.ts ├── main.tsx ├── components │ └── ui │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── slider.tsx │ │ ├── button.tsx │ │ └── select.tsx ├── sortingAlgorithms │ ├── insertion.ts │ ├── bubble.ts │ ├── selection.ts │ ├── quick.ts │ └── merge.ts ├── styles.css └── App.tsx ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png └── site.webmanifest ├── tsconfig.json ├── .gitignore ├── vite.config.ts ├── components.json ├── tsconfig.node.json ├── eslint.config.js ├── index.html ├── tsconfig.app.json ├── LICENSE ├── package.json └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/sorting-algorithms-visualized/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/sorting-algorithms-visualized/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/sorting-algorithms-visualized/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/sorting-algorithms-visualized/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/sorting-algorithms-visualized/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/sorting-algorithms-visualized/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react" 2 | import { createRoot } from "react-dom/client" 3 | import "./styles.css" 4 | import App from "./App.tsx" 5 | 6 | createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { defineConfig } from "vite" 3 | import react from "@vitejs/plugin-react-swc" 4 | import tailwindcss from "@tailwindcss/vite" 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(), tailwindcss()], 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "./src"), 12 | }, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /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": "", 8 | "css": "src/styles.css", 9 | "baseColor": "neutral", 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 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 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 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | export { Label } 23 | -------------------------------------------------------------------------------- /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 | Sorting Algorithms Visualized 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2023", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": ["./src/*"] 27 | } 28 | }, 29 | "include": ["src"] 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 WebDevSimplified 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 | -------------------------------------------------------------------------------- /src/sortingAlgorithms/insertion.ts: -------------------------------------------------------------------------------- 1 | export function insertionSort(array: number[]) { 2 | for (let i = 1; i < array.length; i++) { 3 | const value = array[i] 4 | let j = i - 1 5 | 6 | for (j; j >= 0; j--) { 7 | if (array[j] > value) { 8 | array[j + 1] = array[j] 9 | } else { 10 | break 11 | } 12 | } 13 | 14 | array[j + 1] = value 15 | } 16 | 17 | return array 18 | } 19 | 20 | export function* insertionSortGenerator(array: number[]) { 21 | const n = array.length 22 | 23 | for (let i = 1; i < n; i++) { 24 | const value = array[i] 25 | let j = i - 1 26 | 27 | // Move elements of array[0..i-1], that are greater than value, 28 | // to one position ahead of their current position 29 | while (j >= 0 && array[j] > value) { 30 | yield [[j + 1], []] as [number[], number[]] 31 | ;[array[j], array[j + 1]] = [array[j + 1], array[j]] 32 | // array[j + 1] = array[j] 33 | j-- 34 | } 35 | yield [[j + 1], []] as [number[], number[]] 36 | array[j + 1] = value 37 | } 38 | 39 | yield [[], Array.from({ length: array.length }, (_, i) => i)] as [ 40 | number[], 41 | number[] 42 | ] 43 | 44 | return array 45 | } 46 | -------------------------------------------------------------------------------- /src/sortingAlgorithms/bubble.ts: -------------------------------------------------------------------------------- 1 | export function bubbleSort(array: number[]) { 2 | for (let n = array.length; n >= 0; n--) { 3 | for (let i = 0; i < n - 1; i++) { 4 | if (array[i] > array[i + 1]) { 5 | // Swap elements 6 | ;[array[i], array[i + 1]] = [array[i + 1], array[i]] 7 | } 8 | } 9 | } 10 | 11 | return array 12 | } 13 | 14 | export function* bubbleSortGenerator(array: number[]) { 15 | let n = array.length 16 | let swapped = true 17 | 18 | while (swapped) { 19 | swapped = false 20 | for (let i = 0; i < n - 1; i++) { 21 | yield [ 22 | [i], 23 | Array.from( 24 | { length: array.length - n }, 25 | (_, i) => array.length - i - 1 26 | ), 27 | ] as [number[], number[]] 28 | if (array[i] > array[i + 1]) { 29 | // Swap the elements 30 | ;[array[i], array[i + 1]] = [array[i + 1], array[i]] 31 | swapped = true 32 | } 33 | } 34 | // Reduce the range of elements to check since the largest element is bubbled to the end 35 | n-- 36 | } 37 | 38 | yield [[], Array.from({ length: array.length }, (_, i) => i)] as [ 39 | number[], 40 | number[] 41 | ] 42 | 43 | return array 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sorting-algorithms-visualized", 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-label": "^2.1.2", 14 | "@radix-ui/react-select": "^2.1.6", 15 | "@radix-ui/react-slider": "^1.2.3", 16 | "@radix-ui/react-slot": "^1.1.2", 17 | "@tailwindcss/vite": "^4.0.14", 18 | "class-variance-authority": "^0.7.1", 19 | "clsx": "^2.1.1", 20 | "lucide-react": "^0.483.0", 21 | "react": "^19.0.0", 22 | "react-dom": "^19.0.0", 23 | "tailwind-merge": "^3.0.2", 24 | "tailwindcss": "^4.0.14", 25 | "tw-animate-css": "^1.2.4" 26 | }, 27 | "devDependencies": { 28 | "@eslint/js": "^9.21.0", 29 | "@types/node": "^22.13.10", 30 | "@types/react": "^19.0.10", 31 | "@types/react-dom": "^19.0.4", 32 | "@vitejs/plugin-react-swc": "^3.8.0", 33 | "eslint": "^9.21.0", 34 | "eslint-plugin-react-hooks": "^5.1.0", 35 | "eslint-plugin-react-refresh": "^0.4.19", 36 | "globals": "^15.15.0", 37 | "typescript": "~5.7.2", 38 | "typescript-eslint": "^8.24.1", 39 | "vite": "^6.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/sortingAlgorithms/selection.ts: -------------------------------------------------------------------------------- 1 | export function selectionSort(array: number[]) { 2 | for (let i = 0; i < array.length - 1; i++) { 3 | let minIndex = i 4 | 5 | for (let j = i + 1; j < array.length; j++) { 6 | if (array[j] < array[minIndex]) { 7 | minIndex = j 8 | } 9 | } 10 | 11 | if (minIndex !== i) { 12 | // Swap elements 13 | ;[array[i], array[minIndex]] = [array[minIndex], array[i]] 14 | } 15 | } 16 | 17 | return array 18 | } 19 | 20 | export function* selectionSortGenerator(array: number[]) { 21 | const n = array.length 22 | 23 | for (let i = 0; i < n - 1; i++) { 24 | let minIndex = i 25 | 26 | for (let j = i + 1; j < n; j++) { 27 | yield [[i, j, minIndex], Array.from({ length: i }, (_, i) => i)] as [ 28 | number[], 29 | number[] 30 | ] 31 | if (array[j] < array[minIndex]) { 32 | minIndex = j 33 | } 34 | } 35 | 36 | // Swap the found minimum element with the first element 37 | if (minIndex !== i) { 38 | yield [[i, minIndex], Array.from({ length: i }, (_, i) => i)] as [ 39 | number[], 40 | number[] 41 | ] 42 | ;[array[i], array[minIndex]] = [array[minIndex], array[i]] 43 | } 44 | } 45 | 46 | yield [[], Array.from({ length: array.length }, (_, i) => i)] as [ 47 | number[], 48 | number[] 49 | ] 50 | 51 | return array 52 | } 53 | -------------------------------------------------------------------------------- /src/sortingAlgorithms/quick.ts: -------------------------------------------------------------------------------- 1 | export function quickSort(array: number[]): number[] { 2 | if (array.length <= 1) { 3 | return array 4 | } 5 | 6 | const pivot = array[array.length - 1] 7 | const left: number[] = [] 8 | const right: number[] = [] 9 | 10 | for (let i = 0; i < array.length - 1; i++) { 11 | if (array[i] < pivot) { 12 | left.push(array[i]) 13 | } else { 14 | right.push(array[i]) 15 | } 16 | } 17 | 18 | return [...quickSort(left), pivot, ...quickSort(right)] 19 | } 20 | 21 | export function* quickSortGenerator(array: number[]) { 22 | if (array.length <= 1) { 23 | return array 24 | } 25 | 26 | const stack: { start: number; end: number }[] = [ 27 | { start: 0, end: array.length - 1 }, 28 | ] 29 | 30 | const sortedIndices: number[] = [] 31 | 32 | while (stack.length) { 33 | const { start, end } = stack.pop()! 34 | if (start >= end) { 35 | sortedIndices.push(start, end) 36 | continue 37 | } 38 | 39 | const pivot = array[end] 40 | let pivotIndex = end 41 | 42 | for (let i = start; i < pivotIndex; i++) { 43 | yield [[i, pivotIndex], sortedIndices] as [number[], number[]] 44 | if (array[i] > pivot) { 45 | const value = array[i] 46 | array.splice(i, 1) 47 | array.splice(end, 0, value) 48 | i-- 49 | pivotIndex-- 50 | } 51 | } 52 | 53 | stack.push({ start: pivotIndex + 1, end }) 54 | stack.push({ start, end: pivotIndex - 1 }) 55 | sortedIndices.push(pivotIndex) 56 | } 57 | 58 | yield [[], sortedIndices] as [number[], number[]] 59 | 60 | return array 61 | } 62 | -------------------------------------------------------------------------------- /src/sortingAlgorithms/merge.ts: -------------------------------------------------------------------------------- 1 | export function mergeSort(arr: number[]): number[] { 2 | if (arr.length <= 1) { 3 | return arr 4 | } 5 | 6 | const middle = Math.floor(arr.length / 2) 7 | const left = mergeSort(arr.slice(0, middle)) 8 | const right = mergeSort(arr.slice(middle)) 9 | 10 | const result: number[] = [] 11 | let i = 0, 12 | j = 0 13 | 14 | while (i < left.length && j < right.length) { 15 | if (left[i] < right[j]) { 16 | result.push(left[i]) 17 | i++ 18 | } else { 19 | result.push(right[j]) 20 | j++ 21 | } 22 | } 23 | 24 | return result.concat(left.slice(i)).concat(right.slice(j)) 25 | } 26 | 27 | export function* mergeSortGenerator(array: number[]) { 28 | if (array.length <= 1) return array 29 | 30 | const stack: { start: number; end: number }[] = [] 31 | for (let i = 0; i < array.length; i++) { 32 | stack.push({ start: i, end: i }) 33 | } 34 | 35 | while (stack.length > 1) { 36 | const left = stack.shift()! 37 | const right = stack.shift()! 38 | 39 | if (left.start >= right.start) { 40 | stack.push(left) 41 | stack.unshift(right) 42 | continue 43 | } 44 | 45 | let startOffset = 0 46 | 47 | let i = left.start, 48 | j = right.start 49 | 50 | while (i <= left.end + startOffset && j <= right.end) { 51 | yield [[i, j], []] as [number[], number[]] 52 | if (array[i] < array[j]) { 53 | i++ 54 | } else { 55 | const value = array[j] 56 | array.splice(j, 1) 57 | array.splice(i, 0, value) 58 | j++ 59 | startOffset++ 60 | } 61 | } 62 | 63 | stack.push({ start: left.start, end: right.end }) 64 | } 65 | 66 | yield [[], Array.from({ length: array.length }, (_, i) => i)] as [ 67 | number[], 68 | number[] 69 | ] 70 | 71 | return array 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 | 14 | ```js 15 | export default tseslint.config({ 16 | extends: [ 17 | // Remove ...tseslint.configs.recommended and replace with this 18 | ...tseslint.configs.recommendedTypeChecked, 19 | // Alternatively, use this for stricter rules 20 | ...tseslint.configs.strictTypeChecked, 21 | // Optionally, add this for stylistic rules 22 | ...tseslint.configs.stylisticTypeChecked, 23 | ], 24 | languageOptions: { 25 | // other options... 26 | parserOptions: { 27 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 28 | tsconfigRootDir: import.meta.dirname, 29 | }, 30 | }, 31 | }) 32 | ``` 33 | 34 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 35 | 36 | ```js 37 | // eslint.config.js 38 | import reactX from 'eslint-plugin-react-x' 39 | import reactDom from 'eslint-plugin-react-dom' 40 | 41 | export default tseslint.config({ 42 | plugins: { 43 | // Add the react-x and react-dom plugins 44 | 'react-x': reactX, 45 | 'react-dom': reactDom, 46 | }, 47 | rules: { 48 | // other rules... 49 | // Enable its recommended typescript rules 50 | ...reactX.configs['recommended-typescript'].rules, 51 | ...reactDom.configs.recommended.rules, 52 | }, 53 | }) 54 | ``` 55 | -------------------------------------------------------------------------------- /src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SliderPrimitive from "@radix-ui/react-slider" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Slider({ 9 | className, 10 | defaultValue, 11 | value, 12 | min = 0, 13 | max = 100, 14 | ...props 15 | }: React.ComponentProps) { 16 | const _values = React.useMemo( 17 | () => 18 | Array.isArray(value) 19 | ? value 20 | : Array.isArray(defaultValue) 21 | ? defaultValue 22 | : [min, max], 23 | [value, defaultValue, min, max] 24 | ) 25 | 26 | return ( 27 | 39 | 45 | 51 | 52 | {Array.from({ length: _values.length }, (_, index) => ( 53 | 58 | ))} 59 | 60 | ) 61 | } 62 | 63 | export { Slider } 64 | -------------------------------------------------------------------------------- /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-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | accent: "bg-accent text-accent-foreground shadow-xs hover:bg-accent/90", 15 | destructive: 16 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 17 | outline: 18 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 19 | secondary: 20 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 21 | ghost: 22 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 23 | link: "text-primary underline-offset-4 hover:underline", 24 | }, 25 | size: { 26 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 27 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 28 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 29 | icon: "size-9", 30 | }, 31 | }, 32 | defaultVariants: { 33 | variant: "default", 34 | size: "default", 35 | }, 36 | } 37 | ) 38 | 39 | function Button({ 40 | className, 41 | variant, 42 | size, 43 | asChild = false, 44 | ...props 45 | }: React.ComponentProps<"button"> & 46 | VariantProps & { 47 | asChild?: boolean 48 | }) { 49 | const Comp = asChild ? Slot : "button" 50 | 51 | return ( 52 | 57 | ) 58 | } 59 | 60 | export { Button } 61 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | :root { 7 | --radius: 0.625rem; 8 | --background: oklch(1 0 0); 9 | --foreground: oklch(0.145 0 0); 10 | --card: oklch(1 0 0); 11 | --card-foreground: oklch(0.145 0 0); 12 | --popover: oklch(1 0 0); 13 | --popover-foreground: oklch(0.145 0 0); 14 | --primary: oklch(0.205 0 0); 15 | --primary-foreground: oklch(0.985 0 0); 16 | /* --secondary: oklch(0.97 0 0); */ 17 | --secondary-foreground: oklch(0.205 0 0); 18 | /* --muted: oklch(0.97 0 0); */ 19 | --muted-foreground: oklch(0.556 0 0); 20 | /* --accent: oklch(0.97 0 0); */ 21 | /* --accent-foreground: oklch(0.205 0 0); */ 22 | --destructive: oklch(0.577 0.245 27.325); 23 | --border: oklch(0.922 0 0); 24 | --input: oklch(0.922 0 0); 25 | --ring: oklch(0.708 0 0); 26 | --chart-1: oklch(0.646 0.222 41.116); 27 | --chart-2: oklch(0.6 0.118 184.704); 28 | --chart-3: oklch(0.398 0.07 227.392); 29 | --chart-4: oklch(0.828 0.189 84.429); 30 | --chart-5: oklch(0.769 0.188 70.08); 31 | --sidebar: oklch(0.985 0 0); 32 | --sidebar-foreground: oklch(0.145 0 0); 33 | --sidebar-primary: oklch(0.205 0 0); 34 | --sidebar-primary-foreground: oklch(0.985 0 0); 35 | --sidebar-accent: oklch(0.97 0 0); 36 | --sidebar-accent-foreground: oklch(0.205 0 0); 37 | --sidebar-border: oklch(0.922 0 0); 38 | --sidebar-ring: oklch(0.708 0 0); 39 | 40 | /* Custom */ 41 | --secondary: oklch(71.62% 0.0036 67.77); 42 | --muted: oklch(92.62% 0.0025 165.07); 43 | --accent: oklch(49.52% 0.1979 28.98); 44 | --accent-foreground: oklch(0.985 0 0); 45 | } 46 | 47 | .dark { 48 | --background: oklch(0.145 0 0); 49 | --foreground: oklch(0.985 0 0); 50 | --card: oklch(0.205 0 0); 51 | --card-foreground: oklch(0.985 0 0); 52 | --popover: oklch(0.205 0 0); 53 | --popover-foreground: oklch(0.985 0 0); 54 | --primary: oklch(1 0 0); 55 | --primary-foreground: oklch(0.205 0 0); 56 | /* --secondary: oklch(0.269 0 0); */ 57 | --secondary-foreground: oklch(0.985 0 0); 58 | /* --muted: oklch(0.269 0 0); */ 59 | /* --muted-foreground: oklch(0.708 0 0); */ 60 | /* --accent: oklch(0.269 0 0); */ 61 | /* --accent-foreground: oklch(0.985 0 0); */ 62 | --destructive: oklch(0.704 0.191 22.216); 63 | --border: oklch(1 0 0 / 10%); 64 | --input: oklch(1 0 0 / 15%); 65 | --ring: oklch(0.556 0 0); 66 | --chart-1: oklch(0.488 0.243 264.376); 67 | --chart-2: oklch(0.696 0.17 162.48); 68 | --chart-3: oklch(0.769 0.188 70.08); 69 | --chart-4: oklch(0.627 0.265 303.9); 70 | --chart-5: oklch(0.645 0.246 16.439); 71 | --sidebar: oklch(0.205 0 0); 72 | --sidebar-foreground: oklch(0.985 0 0); 73 | --sidebar-primary: oklch(0.488 0.243 264.376); 74 | --sidebar-primary-foreground: oklch(0.985 0 0); 75 | --sidebar-accent: oklch(0.269 0 0); 76 | --sidebar-accent-foreground: oklch(0.985 0 0); 77 | --sidebar-border: oklch(1 0 0 / 10%); 78 | --sidebar-ring: oklch(0.556 0 0); 79 | 80 | /* Custom */ 81 | /* TODO: Finish */ 82 | --secondary: oklch(45.78% 0 0); 83 | --muted: oklch(0.269 0 0); 84 | --accent: oklch(40.52% 0.1979 28.98); 85 | --accent-foreground: oklch(0.985 0 0); 86 | } 87 | 88 | @theme inline { 89 | --radius-sm: calc(var(--radius) - 4px); 90 | --radius-md: calc(var(--radius) - 2px); 91 | --radius-lg: var(--radius); 92 | --radius-xl: calc(var(--radius) + 4px); 93 | --color-background: var(--background); 94 | --color-foreground: var(--foreground); 95 | --color-card: var(--card); 96 | --color-card-foreground: var(--card-foreground); 97 | --color-popover: var(--popover); 98 | --color-popover-foreground: var(--popover-foreground); 99 | --color-primary: var(--primary); 100 | --color-primary-foreground: var(--primary-foreground); 101 | --color-secondary: var(--secondary); 102 | --color-secondary-foreground: var(--secondary-foreground); 103 | --color-muted: var(--muted); 104 | --color-muted-foreground: var(--muted-foreground); 105 | --color-accent: var(--accent); 106 | --color-accent-foreground: var(--accent-foreground); 107 | --color-destructive: var(--destructive); 108 | --color-border: var(--border); 109 | --color-input: var(--input); 110 | --color-ring: var(--ring); 111 | --color-chart-1: var(--chart-1); 112 | --color-chart-2: var(--chart-2); 113 | --color-chart-3: var(--chart-3); 114 | --color-chart-4: var(--chart-4); 115 | --color-chart-5: var(--chart-5); 116 | --color-sidebar: var(--sidebar); 117 | --color-sidebar-foreground: var(--sidebar-foreground); 118 | --color-sidebar-primary: var(--sidebar-primary); 119 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 120 | --color-sidebar-accent: var(--sidebar-accent); 121 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 122 | --color-sidebar-border: var(--sidebar-border); 123 | --color-sidebar-ring: var(--sidebar-ring); 124 | } 125 | 126 | @layer base { 127 | * { 128 | @apply border-border outline-ring/50; 129 | } 130 | body { 131 | @apply bg-background text-foreground; 132 | } 133 | } 134 | 135 | @utility container { 136 | margin-inline: auto; 137 | padding-inline: 2rem; 138 | max-width: 80rem; 139 | width: 100%; 140 | } 141 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SelectPrimitive from "@radix-ui/react-select" 3 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | function Select({ 8 | ...props 9 | }: React.ComponentProps) { 10 | return 11 | } 12 | 13 | function SelectGroup({ 14 | ...props 15 | }: React.ComponentProps) { 16 | return 17 | } 18 | 19 | function SelectValue({ 20 | ...props 21 | }: React.ComponentProps) { 22 | return 23 | } 24 | 25 | function SelectTrigger({ 26 | className, 27 | size = "default", 28 | children, 29 | ...props 30 | }: React.ComponentProps & { 31 | size?: "sm" | "default" 32 | }) { 33 | return ( 34 | 43 | {children} 44 | 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | function SelectContent({ 52 | className, 53 | children, 54 | position = "popper", 55 | ...props 56 | }: React.ComponentProps) { 57 | return ( 58 | 59 | 70 | 71 | 78 | {children} 79 | 80 | 81 | 82 | 83 | ) 84 | } 85 | 86 | function SelectLabel({ 87 | className, 88 | ...props 89 | }: React.ComponentProps) { 90 | return ( 91 | 96 | ) 97 | } 98 | 99 | function SelectItem({ 100 | className, 101 | children, 102 | ...props 103 | }: React.ComponentProps) { 104 | return ( 105 | 113 | 114 | 115 | 116 | 117 | 118 | {children} 119 | 120 | ) 121 | } 122 | 123 | function SelectSeparator({ 124 | className, 125 | ...props 126 | }: React.ComponentProps) { 127 | return ( 128 | 133 | ) 134 | } 135 | 136 | function SelectScrollUpButton({ 137 | className, 138 | ...props 139 | }: React.ComponentProps) { 140 | return ( 141 | 149 | 150 | 151 | ) 152 | } 153 | 154 | function SelectScrollDownButton({ 155 | className, 156 | ...props 157 | }: React.ComponentProps) { 158 | return ( 159 | 167 | 168 | 169 | ) 170 | } 171 | 172 | export { 173 | Select, 174 | SelectContent, 175 | SelectGroup, 176 | SelectItem, 177 | SelectLabel, 178 | SelectScrollDownButton, 179 | SelectScrollUpButton, 180 | SelectSeparator, 181 | SelectTrigger, 182 | SelectValue, 183 | } 184 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent, useEffect, useReducer } from "react" 2 | import { Button } from "./components/ui/button" 3 | import { cn } from "./lib/utils" 4 | import { 5 | Select, 6 | SelectContent, 7 | SelectGroup, 8 | SelectItem, 9 | SelectTrigger, 10 | SelectValue, 11 | } from "./components/ui/select" 12 | import { Label } from "./components/ui/label" 13 | import { Slider } from "./components/ui/slider" 14 | import { Input } from "./components/ui/input" 15 | import { bubbleSortGenerator } from "./sortingAlgorithms/bubble" 16 | import { insertionSortGenerator } from "./sortingAlgorithms/insertion" 17 | import { selectionSortGenerator } from "./sortingAlgorithms/selection" 18 | import { quickSortGenerator } from "./sortingAlgorithms/quick" 19 | import { mergeSortGenerator } from "./sortingAlgorithms/merge" 20 | 21 | const SORTING_ALGORITHMS = [ 22 | "bubble", 23 | "insertion", 24 | "selection", 25 | "quick", 26 | "merge", 27 | ] as const 28 | const OPERATIONS_PER_SECOND = 2 29 | 30 | type Action = 31 | | { type: "RANDOMIZE" } 32 | | { type: "SORT" } 33 | | { type: "STOP" } 34 | | { type: "FINISH_SORTING" } 35 | | { type: "CHANGE_ALGORITHM"; payload: SortingAlgorithm } 36 | | { type: "CHANGE_SPEED"; payload: number } 37 | | { type: "CHANGE_ARRAY_LENGTH"; payload: number } 38 | | { type: "SET_INDICES"; payload: { active: number[]; sorted: number[] } } 39 | type SortingAlgorithm = (typeof SORTING_ALGORITHMS)[number] 40 | type State = { 41 | sortingAlgorithm: SortingAlgorithm 42 | sortingSpeed: number 43 | randomArray: number[] 44 | activeIndices: number[] 45 | sortedIndices: number[] 46 | activeSortingFunction?: Generator<[number[], number[]]> 47 | isSorting: boolean 48 | } 49 | 50 | const MAX_ARRAY_LENGTH = 300 51 | const MIN_ARRAY_LENGTH = 10 52 | const MAX_SPEED = 50 53 | const MIN_SPEED = 1 54 | 55 | function reducer(state: State, action: Action): State { 56 | switch (action.type) { 57 | case "RANDOMIZE": 58 | if (state.isSorting) return state 59 | return { 60 | ...state, 61 | activeIndices: [], 62 | sortedIndices: [], 63 | isSorting: false, 64 | activeSortingFunction: undefined, 65 | randomArray: getRandomElements(state.randomArray.length), 66 | } 67 | case "SORT": 68 | return { 69 | ...state, 70 | activeSortingFunction: 71 | state.activeSortingFunction ?? 72 | getSortingFunction(state.sortingAlgorithm)(state.randomArray), 73 | isSorting: true, 74 | } 75 | case "STOP": 76 | return { 77 | ...state, 78 | isSorting: false, 79 | } 80 | case "FINISH_SORTING": 81 | return { 82 | ...state, 83 | isSorting: false, 84 | activeSortingFunction: undefined, 85 | } 86 | case "CHANGE_ALGORITHM": 87 | if (state.isSorting) return state 88 | return { 89 | ...state, 90 | sortingAlgorithm: action.payload, 91 | activeIndices: [], 92 | sortedIndices: [], 93 | activeSortingFunction: undefined, 94 | } 95 | case "CHANGE_SPEED": 96 | if (action.payload > MAX_SPEED || action.payload < MIN_SPEED) return state 97 | return { 98 | ...state, 99 | sortingSpeed: action.payload, 100 | } 101 | case "CHANGE_ARRAY_LENGTH": 102 | if ( 103 | action.payload < MIN_ARRAY_LENGTH || 104 | action.payload > MAX_ARRAY_LENGTH || 105 | isNaN(action.payload) 106 | ) { 107 | return state 108 | } 109 | if (state.isSorting) return state 110 | return { 111 | ...state, 112 | activeIndices: [], 113 | sortedIndices: [], 114 | isSorting: false, 115 | activeSortingFunction: undefined, 116 | randomArray: getRandomElements(action.payload), 117 | } 118 | case "SET_INDICES": 119 | return { 120 | ...state, 121 | activeIndices: action.payload.active, 122 | sortedIndices: action.payload.sorted, 123 | } 124 | default: 125 | throw new Error(`Invalid action: ${action satisfies never}`) 126 | } 127 | } 128 | 129 | export default function App() { 130 | const [ 131 | { 132 | sortingAlgorithm, 133 | sortingSpeed, 134 | randomArray, 135 | activeIndices, 136 | sortedIndices, 137 | activeSortingFunction, 138 | isSorting, 139 | }, 140 | dispatch, 141 | ] = useReducer(reducer, { 142 | sortingAlgorithm: "bubble", 143 | sortingSpeed: 1, 144 | randomArray: getRandomElements(100), 145 | activeIndices: [], 146 | sortedIndices: [], 147 | isSorting: false, 148 | }) 149 | 150 | useEffect(() => { 151 | let cancel = false 152 | let timeout: NodeJS.Timeout 153 | async function inner() { 154 | while (activeSortingFunction != null && isSorting && !cancel) { 155 | const { 156 | done, 157 | value: [active, sorted], 158 | } = activeSortingFunction.next() 159 | 160 | if (done) { 161 | dispatch({ type: "FINISH_SORTING" }) 162 | return 163 | } 164 | 165 | dispatch({ type: "SET_INDICES", payload: { active, sorted } }) 166 | await new Promise(resolve => { 167 | timeout = setTimeout( 168 | resolve, 169 | 1000 / OPERATIONS_PER_SECOND / sortingSpeed 170 | ) 171 | }) 172 | } 173 | } 174 | 175 | inner() 176 | 177 | return () => { 178 | clearTimeout(timeout) 179 | cancel = true 180 | } 181 | }, [activeSortingFunction, sortingSpeed, isSorting]) 182 | 183 | async function handleSubmit(e: FormEvent) { 184 | e.preventDefault() 185 | 186 | if (isSorting) { 187 | dispatch({ type: "STOP" }) 188 | } else { 189 | dispatch({ type: "SORT" }) 190 | } 191 | } 192 | 193 | return ( 194 | 195 | 196 | Sort Visualizer 197 | 201 | 202 | Algorithm 203 | 207 | dispatch({ 208 | type: "CHANGE_ALGORITHM", 209 | payload: e as SortingAlgorithm, 210 | }) 211 | } 212 | > 213 | 214 | 215 | 216 | 217 | 218 | {SORTING_ALGORITHMS.map(algorithm => ( 219 | 224 | {algorithm} 225 | 226 | ))} 227 | 228 | 229 | 230 | 231 | 232 | Amount 233 | 239 | dispatch({ 240 | type: "CHANGE_ARRAY_LENGTH", 241 | payload: e.target.valueAsNumber, 242 | }) 243 | } 244 | max={MAX_ARRAY_LENGTH} 245 | min={MIN_ARRAY_LENGTH} 246 | step={1} 247 | /> 248 | 249 | 250 | 251 | Speed ({sortingSpeed}x) 252 | 253 | 254 | 258 | dispatch({ type: "CHANGE_SPEED", payload: e[0] }) 259 | } 260 | max={MAX_SPEED} 261 | min={MIN_SPEED} 262 | step={1} 263 | /> 264 | 265 | 266 | 267 | 268 | {isSorting ? "Stop" : "Sort"} 269 | 270 | dispatch({ type: "RANDOMIZE" })} 272 | disabled={isSorting} 273 | type="button" 274 | variant="outline" 275 | > 276 | Randomize 277 | 278 | 279 | 280 | 281 | 282 | 283 | {randomArray.map((value, index) => ( 284 | 293 | ))} 294 | 295 | 296 | ) 297 | } 298 | 299 | function getRandomElements(arraySize: number) { 300 | return Array.from( 301 | { length: arraySize }, 302 | () => Math.floor(Math.random() * 100) + 1 303 | ) 304 | } 305 | 306 | function getSortingFunction(algorithm: SortingAlgorithm) { 307 | switch (algorithm) { 308 | case "bubble": 309 | return bubbleSortGenerator 310 | case "insertion": 311 | return insertionSortGenerator 312 | case "selection": 313 | return selectionSortGenerator 314 | case "quick": 315 | return quickSortGenerator 316 | case "merge": 317 | return mergeSortGenerator 318 | default: 319 | throw new Error(`Invalid algorithm: ${algorithm satisfies never}`) 320 | } 321 | } 322 | --------------------------------------------------------------------------------