├── .cargo
└── config.toml
├── .github
└── workflows
│ └── build-app.yml
├── .gitignore
├── .vscode
└── extensions.json
├── LICENSE
├── README.md
├── README_CN.md
├── components.json
├── index.html
├── package.json
├── pnpm-lock.yaml
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── capabilities
│ └── default.json
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── 64x64.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square30x30Logo.png
│ ├── Square310x310Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── StoreLogo.png
│ ├── android
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ └── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ ├── icon.icns
│ ├── icon.ico
│ ├── icon.png
│ └── ios
│ │ ├── AppIcon-20x20@1x.png
│ │ ├── AppIcon-20x20@2x-1.png
│ │ ├── AppIcon-20x20@2x.png
│ │ ├── AppIcon-20x20@3x.png
│ │ ├── AppIcon-29x29@1x.png
│ │ ├── AppIcon-29x29@2x-1.png
│ │ ├── AppIcon-29x29@2x.png
│ │ ├── AppIcon-29x29@3x.png
│ │ ├── AppIcon-40x40@1x.png
│ │ ├── AppIcon-40x40@2x-1.png
│ │ ├── AppIcon-40x40@2x.png
│ │ ├── AppIcon-40x40@3x.png
│ │ ├── AppIcon-512@2x.png
│ │ ├── AppIcon-60x60@2x.png
│ │ ├── AppIcon-60x60@3x.png
│ │ ├── AppIcon-76x76@1x.png
│ │ ├── AppIcon-76x76@2x.png
│ │ └── AppIcon-83.5x83.5@2x.png
├── src
│ ├── lib.rs
│ ├── main.rs
│ └── utility
│ │ ├── codec.rs
│ │ ├── formatter.rs
│ │ ├── generator.rs
│ │ └── mod.rs
└── tauri.conf.json
├── src
├── app.tsx
├── assets
│ └── react.svg
├── components
│ ├── sidebar
│ │ ├── index.tsx
│ │ └── search-form.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── radio-group.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── sidebar.tsx
│ │ ├── skeleton.tsx
│ │ ├── textarea.tsx
│ │ └── tooltip.tsx
├── hooks
│ └── use-mobile.ts
├── index.css
├── lib
│ ├── copyboard.ts
│ └── utils.ts
├── main.tsx
├── tauri.ts
├── utilities
│ ├── formatter
│ │ ├── css.tsx
│ │ ├── html.tsx
│ │ ├── js.tsx
│ │ └── json.tsx
│ ├── generators
│ │ ├── hash.tsx
│ │ └── id.tsx
│ └── types.ts
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | rustdocflags = ["-C", "target-cpu=native"]
3 | rustflags = ["-g", "-C", "target-cpu=native"]
4 |
5 | [bench]
6 | rustdocflags = ["-C", "target-cpu=native"]
7 | rustflags = ["-g", "-C", "target-cpu=native"]
--------------------------------------------------------------------------------
/.github/workflows/build-app.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - release
6 | tags:
7 | - 'v*'
8 | workflow_dispatch:
9 |
10 | jobs:
11 | release:
12 | permissions:
13 | contents: write
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | platform: [macos-latest, ubuntu-20.04, windows-latest]
18 | runs-on: ${{ matrix.platform }}
19 |
20 | steps:
21 | - name: Checkout repository
22 | uses: actions/checkout@v3
23 |
24 | - name: Install dependencies (ubuntu only)
25 | if: matrix.platform == 'ubuntu-20.04'
26 | # You can remove libayatana-appindicator3-dev if you don't use the system tray feature.
27 | run: |
28 | sudo apt-get update
29 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
30 |
31 | - name: Rust setup
32 | uses: dtolnay/rust-toolchain@stable
33 |
34 | - name: Rust cache
35 | uses: swatinem/rust-cache@v2
36 | with:
37 | workspaces: './src-tauri -> target'
38 |
39 | - uses: pnpm/action-setup@v2.4.0
40 |
41 | - name: Sync node version and setup cache
42 | uses: actions/setup-node@v3
43 | with:
44 | node-version: 'lts/*'
45 | cache: 'pnpm' # Set this to npm, yarn or pnpm.
46 |
47 | - name: Install frontend dependencies
48 | # If you don't have `beforeBuildCommand` configured you may want to build your frontend here too.
49 | run: pnpm install # Change this to npm, yarn or pnpm.
50 |
51 | - name: Build the app
52 | uses: tauri-apps/tauri-action@v0
53 |
54 | env:
55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56 | with:
57 | tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags.
58 | releaseName: 'App Name v__VERSION__' # tauri-action replaces \_\_VERSION\_\_ with the app version.
59 | releaseBody: 'See the assets to download and install this version.'
60 | releaseDraft: true
61 | prerelease: false
62 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!WARNING]
2 | > 🚧Under development
3 |
4 |
English | 简体中文
5 |
6 |
7 |
8 | Developer Utility
9 | All-in-one Utility Box for Developers
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ## Tools
25 |
26 | | Tools Name | JavaScript Implementation | Rust Implementation |
27 | | --------------------------- | ---------------------------------- | ------------------------------- |
28 | | Base64 String Encode/Decode | :white_check_mark: | |
29 | | Base64 Image Encode/Decode | :white_check_mark: | |
30 | | GPT Tokenizer | :white_check_mark: `gpt-tokenizer` | :o: `tiktoken-rs` |
31 | | JSON Format/Validate | :white_check_mark: `JSON` | :white_check_mark: `serde_json` |
32 | | JSON ↔ YAML ↔ TOML | :white_check_mark: | |
33 | | JWT Debugger | :white_check_mark: | |
34 | | Unix Time Converter | :white_check_mark: | |
35 |
36 | ## Project Status
37 |
38 | 
39 |
40 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | > [!WARNING]
2 | > 🚧Under development
3 |
4 |
5 |
6 |
7 |
8 | DevUtils
9 | 开发人员多功能工具箱
10 |
11 |

12 |
13 |
14 |
15 |
16 |
17 | [开始](https://utils.sku.moe) / [技术详情](https://manual.sku.moe/project/devutils) / [GitHub Sponsor](https://github.com/sponsors/AprilNEA) / [爱发电](https://afdian.net/a/aprilnea)
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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/index.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 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Developer Utility
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "utility-dev",
3 | "private": true,
4 | "version": "0.1.2",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview",
10 | "tauri": "tauri"
11 | },
12 | "dependencies": {
13 | "@radix-ui/react-checkbox": "^1.3.2",
14 | "@radix-ui/react-dialog": "^1.1.14",
15 | "@radix-ui/react-dropdown-menu": "^2.1.15",
16 | "@radix-ui/react-label": "^2.1.7",
17 | "@radix-ui/react-radio-group": "^1.3.7",
18 | "@radix-ui/react-scroll-area": "^1.2.9",
19 | "@radix-ui/react-select": "^2.2.5",
20 | "@radix-ui/react-separator": "^1.1.7",
21 | "@radix-ui/react-slot": "^1.2.3",
22 | "@radix-ui/react-tooltip": "^1.2.7",
23 | "@tailwindcss/vite": "^4.1.8",
24 | "@tauri-apps/api": "^2",
25 | "@tauri-apps/plugin-clipboard-manager": "~2.2.2",
26 | "@tauri-apps/plugin-opener": "^2",
27 | "class-variance-authority": "^0.7.1",
28 | "clsx": "^2.1.1",
29 | "foxact": "^0.2.45",
30 | "lucide-react": "^0.513.0",
31 | "react": "^18.3.1",
32 | "react-dom": "^18.3.1",
33 | "tailwind-merge": "^3.3.0",
34 | "tailwind-scrollbar": "^4.0.2",
35 | "tailwindcss": "^4.1.8",
36 | "wouter": "^3.7.1"
37 | },
38 | "devDependencies": {
39 | "@tauri-apps/cli": "^2",
40 | "@types/node": "^22.15.30",
41 | "@types/react": "^18.3.1",
42 | "@types/react-dom": "^18.3.1",
43 | "@vitejs/plugin-react": "^4.3.4",
44 | "tw-animate-css": "^1.3.4",
45 | "typescript": "~5.6.2",
46 | "vite": "^6.0.3"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # Generated by Tauri
6 | # will have schema files for capabilities auto-completion
7 | /gen/schemas
8 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "utility-dev"
3 | version = "0.1.2"
4 | description = "A Tauri App"
5 | authors = ["you"]
6 | edition = "2021"
7 |
8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9 |
10 | [lib]
11 | # The `_lib` suffix may seem redundant but it is necessary
12 | # to make the lib name unique and wouldn't conflict with the bin name.
13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
14 | name = "utility_dev_lib"
15 | crate-type = ["staticlib", "cdylib", "rlib"]
16 |
17 | [build-dependencies]
18 | tauri-build = { version = "2", features = [] }
19 |
20 | [dependencies]
21 | tauri = { version = "2", features = ["macos-private-api"] }
22 | tauri-plugin-opener = "2"
23 | serde = { version = "1", features = ["derive"] }
24 | serde_json = "1"
25 | window-vibrancy = "0.6.0"
26 | rayon = "1.7"
27 | # ID Generators
28 | uuid-simd = "0.8.0"
29 | uuid = { version = "1.17", features = [
30 | "v1",
31 | "v3",
32 | "v4",
33 | "v5",
34 | "v6",
35 | "v7",
36 | "v8",
37 | "serde",
38 | ] }
39 | ulid = "1.2.1"
40 | nanoid = "0.4.0"
41 |
42 | # Hash Generators
43 | sha1 = "0.10"
44 | sha2 = "0.10"
45 | sha3 = "0.10"
46 | md2 = "0.10"
47 | md4 = "0.10"
48 | md-5 = "0.10"
49 | digest = "0.10"
50 | hex = "0.4"
51 | tiny-keccak = { version = "2.0", features = ["sha3"] }
52 |
53 | # Formatters
54 | sonic-rs = "0.5.1"
55 | tauri-plugin-clipboard-manager = "2"
56 |
57 | # Encoders
58 | base64 = "0.22.1"
59 |
60 | # Decoders
61 | jsonwebtoken = "9.3.1"
62 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../gen/schemas/desktop-schema.json",
3 | "identifier": "default",
4 | "description": "Capability for the main window",
5 | "windows": [
6 | "main"
7 | ],
8 | "permissions": [
9 | "core:default",
10 | "opener:default",
11 | "core:window:default",
12 | "core:window:allow-start-dragging",
13 | "core:window:allow-set-always-on-top",
14 | "clipboard-manager:default"
15 | ]
16 | }
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/64x64.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-20x20@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-20x20@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-20x20@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-29x29@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-29x29@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-29x29@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-40x40@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-40x40@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-40x40@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-512@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-60x60@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-60x60@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-76x76@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-76x76@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AprilNEA/DevUtils/0db5c090e052c775c71025657169154277011bc1/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/src-tauri/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(
2 | all(not(debug_assertions), target_os = "windows"),
3 | windows_subsystem = "windows"
4 | )]
5 |
6 | mod utility;
7 | use tauri::Manager;
8 | use window_vibrancy::*;
9 |
10 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
11 | #[tauri::command]
12 | fn greet(name: &str) -> String {
13 | format!("Hello, {}! You've been greeted from Rust!", name)
14 | }
15 |
16 | #[cfg_attr(mobile, tauri::mobile_entry_point)]
17 | pub fn run() {
18 | tauri::Builder::default()
19 | .plugin(tauri_plugin_clipboard_manager::init())
20 | .setup(|app| {
21 | let window = app.get_webview_window("main").unwrap();
22 |
23 | #[cfg(target_os = "macos")]
24 | apply_vibrancy(
25 | &window,
26 | NSVisualEffectMaterial::HudWindow,
27 | Some(NSVisualEffectState::Inactive),
28 | None,
29 | )
30 | .expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
31 |
32 | #[cfg(target_os = "windows")]
33 | apply_blur(&window, Some((18, 18, 18, 125)))
34 | .expect("Unsupported platform! 'apply_blur' is only supported on Windows");
35 |
36 | Ok(())
37 | })
38 | .plugin(tauri_plugin_opener::init())
39 | .invoke_handler(tauri::generate_handler![
40 | greet,
41 | utility::generator::generate_uuid_v4,
42 | utility::generator::generate_uuid_v7,
43 | utility::generator::generate_ulid,
44 | utility::generator::generate_nanoid,
45 | utility::formatter::format_json,
46 | utility::generator::generate_hashes,
47 | ])
48 | .run(tauri::generate_context!())
49 | .expect("error while running tauri application");
50 | }
51 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | fn main() {
5 | utility_dev_lib::run()
6 | }
7 |
--------------------------------------------------------------------------------
/src-tauri/src/utility/codec.rs:
--------------------------------------------------------------------------------
1 | use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
2 | use jsonwebtoken::{decode, DecodingKey, Validation};
3 | use serde::{Deserialize, Serialize};
4 |
5 | #[tauri::command]
6 | pub fn decode_base64(input: &str) -> Result {
7 | BASE64
8 | .decode(input)
9 | .map_err(|e| e.to_string())
10 | .and_then(|bytes| String::from_utf8(bytes).map_err(|e| e.to_string()))
11 | }
12 |
13 | #[tauri::command]
14 | pub fn encode_base64(input: &str) -> String {
15 | BASE64.encode(input.as_bytes())
16 | }
17 |
18 | #[derive(Debug, Serialize, Deserialize)]
19 | pub struct JwtHeader {
20 | pub alg: String,
21 | pub typ: Option,
22 | pub kid: Option,
23 | }
24 |
25 | #[derive(Debug, Serialize, Deserialize)]
26 | pub struct JwtPayload {
27 | #[serde(flatten)]
28 | pub claims: serde_json::Value,
29 | }
30 |
31 | #[derive(Debug, Serialize, Deserialize)]
32 | pub struct JwtDecodeResult {
33 | pub header: JwtHeader,
34 | pub payload: JwtPayload,
35 | pub signature: String,
36 | }
37 |
38 | #[tauri::command]
39 | pub fn decode_jwt(token: &str) -> Result {
40 | let parts: Vec<&str> = token.split('.').collect();
41 | if parts.len() != 3 {
42 | return Err("Invalid JWT format".to_string());
43 | }
44 |
45 | let header = base64::decode(parts[0])
46 | .map_err(|_| "Invalid header encoding".to_string())
47 | .and_then(|bytes| {
48 | serde_json::from_slice::(&bytes)
49 | .map_err(|_| "Invalid header format".to_string())
50 | })?;
51 |
52 | let payload = base64::decode(parts[1])
53 | .map_err(|_| "Invalid payload encoding".to_string())
54 | .and_then(|bytes| {
55 | serde_json::from_slice::(&bytes)
56 | .map_err(|_| "Invalid payload format".to_string())
57 | })?;
58 |
59 | let signature = parts[2].to_string();
60 |
61 | Ok(JwtDecodeResult {
62 | header,
63 | payload,
64 | signature,
65 | })
66 | }
67 |
68 | #[tauri::command]
69 | pub fn verify_jwt(token: &str, secret: &str) -> Result {
70 | let validation = Validation::default();
71 | let key = DecodingKey::from_secret(secret.as_bytes());
72 |
73 | match decode::(token, &key, &validation) {
74 | Ok(_) => decode_jwt(token),
75 | Err(e) => Err(e.to_string()),
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src-tauri/src/utility/formatter.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use sonic_rs;
3 |
4 | #[derive(Debug, Clone, Serialize, Deserialize)]
5 | #[serde(rename_all = "lowercase")]
6 | pub enum IndentStyle {
7 | Spaces(usize),
8 | Tabs,
9 | Minified,
10 | }
11 |
12 | #[tauri::command]
13 | pub fn format_json(input: &str, style: IndentStyle) -> String {
14 | let value: sonic_rs::Value = sonic_rs::from_str(input).unwrap();
15 | // let pretty_str = sonic_rs::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string());
16 | match style {
17 | IndentStyle::Minified => sonic_rs::to_string(&value).unwrap_or_else(|_| "{}".to_string()),
18 | IndentStyle::Spaces(size) => sonic_rs::to_string_pretty(&value)
19 | .unwrap_or_else(|_| "{}".to_string())
20 | .lines()
21 | .map(|line| {
22 | if size == 2 {
23 | return line.to_string();
24 | }
25 | let leading_spaces = line.len() - line.trim_start().len();
26 | let indent_level = leading_spaces / 2;
27 |
28 | format!("{}{}", " ".repeat(indent_level * size), line.trim_start())
29 | })
30 | .collect::>()
31 | .join("\n"),
32 | IndentStyle::Tabs => sonic_rs::to_string_pretty(&value)
33 | .unwrap_or_else(|_| "{}".to_string())
34 | .lines()
35 | .map(|line| {
36 | let leading_spaces = line.len() - line.trim_start().len();
37 | let indent_level = leading_spaces / 2; // Default is 2-space indentation
38 | format!("{}{}", "\t".repeat(indent_level), line.trim_start())
39 | })
40 | .collect::>()
41 | .join("\n"),
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src-tauri/src/utility/generator.rs:
--------------------------------------------------------------------------------
1 | use digest::Digest;
2 | use hex;
3 | use nanoid::nanoid;
4 | use rayon::prelude::*;
5 | use serde::{Deserialize, Serialize};
6 | use std::sync::Arc;
7 | use std::time::{SystemTime, UNIX_EPOCH};
8 | use ulid::Ulid;
9 | use uuid::{Timestamp, Uuid};
10 |
11 | #[tauri::command]
12 | pub fn generate_uuid_v4(count: u32) -> String {
13 | (0..count)
14 | .map(|_| Uuid::new_v4().to_string())
15 | .collect::>()
16 | .join("\n")
17 | }
18 |
19 | #[tauri::command]
20 | pub fn generate_uuid_v7(count: u32, timestamp: Option) -> String {
21 | let context = uuid::Context::new_random();
22 |
23 | let base_seconds = timestamp.unwrap_or_else(|| {
24 | let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
25 | now.as_secs()
26 | });
27 | (0..count)
28 | .map(|i| {
29 | let nanos = i;
30 | let time = Timestamp::from_unix(&context, base_seconds, nanos);
31 | Uuid::new_v7(time).to_string()
32 | })
33 | .collect::>()
34 | .join("7\n")
35 | }
36 |
37 | #[tauri::command]
38 | pub fn generate_ulid(count: u32) -> String {
39 | (0..count)
40 | .map(|_| Ulid::new().to_string())
41 | .collect::>()
42 | .join("\n")
43 | }
44 |
45 | #[tauri::command]
46 | pub fn generate_nanoid(count: u32) -> String {
47 | (0..count)
48 | .map(|_| nanoid!())
49 | .collect::>()
50 | .join("\n")
51 | }
52 |
53 | #[derive(Debug, Serialize, Deserialize)]
54 | pub struct HashResult {
55 | pub md2: String,
56 | pub md4: String,
57 | pub md5: String,
58 | pub sha1: String,
59 | pub sha224: String,
60 | pub sha256: String,
61 | pub sha384: String,
62 | pub sha512: String,
63 | pub sha3_256: String,
64 | pub keccak256: String,
65 | }
66 |
67 | fn hash_with_algorithm(data: &[u8]) -> String {
68 | let mut hasher = D::new();
69 | hasher.update(data);
70 | hex::encode(hasher.finalize())
71 | }
72 |
73 | #[tauri::command]
74 | pub fn generate_hashes(input: &str) -> HashResult {
75 | let data = Arc::new(input.as_bytes().to_vec());
76 |
77 | let tasks: Vec<(&str, Box String + Send + Sync>)> = vec![
78 | (
79 | "MD2",
80 | Box::new(|data| hash_with_algorithm::(data)),
81 | ),
82 | (
83 | "MD5",
84 | Box::new(|data| hash_with_algorithm::(data)),
85 | ),
86 | (
87 | "MD4",
88 | Box::new(|data| hash_with_algorithm::(data)),
89 | ),
90 | (
91 | "SHA1",
92 | Box::new(|data| hash_with_algorithm::(data)),
93 | ),
94 | (
95 | "SHA224",
96 | Box::new(|data| hash_with_algorithm::(data)),
97 | ),
98 | (
99 | "SHA256",
100 | Box::new(|data| hash_with_algorithm::(data)),
101 | ),
102 | (
103 | "SHA384",
104 | Box::new(|data| hash_with_algorithm::(data)),
105 | ),
106 | (
107 | "SHA512",
108 | Box::new(|data| hash_with_algorithm::(data)),
109 | ),
110 | (
111 | "SHA3-256",
112 | Box::new(|data| hash_with_algorithm::(data)),
113 | ),
114 | (
115 | "Keccak256",
116 | Box::new(|data| hash_with_algorithm::(data)),
117 | ),
118 | ];
119 |
120 | let results: Vec<(String, String)> = tasks
121 | .into_par_iter()
122 | .map(|(name, hasher)| {
123 | let hash = hasher(&data);
124 | (name.to_string(), hash)
125 | })
126 | .collect();
127 |
128 | let mut hash_map = std::collections::HashMap::new();
129 | for (name, hash) in results {
130 | hash_map.insert(name, hash);
131 | }
132 |
133 | HashResult {
134 | md2: hash_map.get("MD2").unwrap().clone(),
135 | md4: hash_map.get("MD4").unwrap().clone(),
136 | md5: hash_map.get("MD5").unwrap().clone(),
137 | sha1: hash_map.get("SHA1").unwrap().clone(),
138 | sha224: hash_map.get("SHA224").unwrap().clone(),
139 | sha256: hash_map.get("SHA256").unwrap().clone(),
140 | sha384: hash_map.get("SHA384").unwrap().clone(),
141 | sha512: hash_map.get("SHA512").unwrap().clone(),
142 | sha3_256: hash_map.get("SHA3-256").unwrap().clone(),
143 | keccak256: hash_map.get("Keccak256").unwrap().clone(),
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src-tauri/src/utility/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod codec;
2 | pub mod formatter;
3 | pub mod generator;
4 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.tauri.app/config/2",
3 | "productName": "utility-dev",
4 | "version": "0.1.0",
5 | "identifier": "dev.utility.app",
6 | "build": {
7 | "beforeDevCommand": "pnpm dev",
8 | "devUrl": "http://localhost:1420",
9 | "beforeBuildCommand": "pnpm build",
10 | "frontendDist": "../dist"
11 | },
12 | "app": {
13 | "withGlobalTauri": true,
14 | "windows": [
15 | {
16 | "title": "",
17 | "titleBarStyle": "Overlay",
18 | "width": 1200,
19 | "height": 800,
20 | "transparent": true
21 | }
22 | ],
23 | "macOSPrivateApi": true,
24 | "security": {
25 | "csp": null
26 | }
27 | },
28 | "bundle": {
29 | "active": true,
30 | "category": "Utility",
31 | "targets": "all",
32 | "icon": [
33 | "icons/32x32.png",
34 | "icons/128x128.png",
35 | "icons/128x128@2x.png",
36 | "icons/icon.icns",
37 | "icons/icon.ico"
38 | ]
39 | }
40 | }
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import "./index.css";
2 |
3 | import { Router, Route } from "wouter";
4 |
5 | import AppSidebar from "./components/sidebar";
6 |
7 | import JsonFormatterPage from "./utilities/formatter/json";
8 | import HtmlEncoderDecoderPage from "./utilities/formatter/html";
9 | import { CssBeautifyMinifyTool } from "./utilities/formatter/css";
10 |
11 | import IdGeneratorPage from "./utilities/generators/id";
12 | import HashGeneratorPage from "./utilities/generators/hash";
13 |
14 | function App() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | export default App;
33 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { getCurrentWindow } from "@tauri-apps/api/window";
3 | import {
4 | Sidebar,
5 | SidebarContent,
6 | SidebarGroup,
7 | SidebarGroupContent,
8 | SidebarGroupLabel,
9 | SidebarHeader,
10 | SidebarInset,
11 | SidebarMenu,
12 | SidebarMenuButton,
13 | SidebarMenuItem,
14 | SidebarProvider,
15 | SidebarRail,
16 | SidebarTrigger,
17 | } from "@/components/ui/sidebar";
18 | import { Separator } from "@radix-ui/react-separator";
19 | import { Link, useLocation } from "wouter";
20 | import {
21 | FileJsonIcon,
22 | FileCodeIcon,
23 | FileTextIcon,
24 | FileTypeIcon,
25 | FileCode2Icon,
26 | FileIcon,
27 | FileArchiveIcon,
28 | ListOrderedIcon,
29 | LinkIcon,
30 | BinaryIcon,
31 | TableIcon,
32 | Code2Icon,
33 | TypeIcon,
34 | TerminalIcon,
35 | CodeIcon,
36 | HashIcon,
37 | TextIcon,
38 | QrCodeIcon,
39 | PaletteIcon,
40 | CalendarIcon,
41 | SearchIcon,
42 | DiffIcon,
43 | ClockIcon,
44 | KeyIcon,
45 | RegexIcon,
46 | ImageIcon,
47 | PinIcon,
48 | PinOffIcon,
49 | } from "lucide-react";
50 | import { SearchForm } from "./search-form";
51 | import { useState } from "react";
52 | import { Button } from "../ui/button";
53 | import { useSidebar } from "../ui/sidebar";
54 | import { cn } from "@/lib/utils";
55 |
56 | // This is sample data.
57 | const navMain = [
58 | {
59 | title: "Format / Validate / Minify",
60 | url: "formatter",
61 | items: [
62 | {
63 | title: "JSON Format/Validate",
64 | url: "json",
65 | icon: FileJsonIcon,
66 | },
67 | {
68 | title: "HTML Beautify/Minify",
69 | url: "html",
70 | icon: FileCodeIcon,
71 | },
72 | {
73 | title: "CSS Beautify/Minify",
74 | url: "css",
75 | icon: FileIcon,
76 | },
77 | {
78 | title: "JS Beautify/Minify",
79 | url: "js",
80 | icon: FileCode2Icon,
81 | },
82 | // {
83 | // title: "ERB Beautify/Minify",
84 | // url: "erb",
85 | // icon: FileTypeIcon,
86 | // },
87 | // {
88 | // title: "LESS Beautify/Minify",
89 | // url: "less",
90 | // icon: FileIcon,
91 | // },
92 | // {
93 | // title: "SCSS Beautify/Minify",
94 | // url: "scss",
95 | // icon: FileIcon,
96 | // },
97 | // {
98 | // title: "XML Beautify/Minify",
99 | // url: "xml",
100 | // icon: FileCodeIcon,
101 | // },
102 | // {
103 | // title: "SQL Formatter",
104 | // url: "sql",
105 | // icon: FileIcon,
106 | // },
107 | // {
108 | // title: "Line Sort/Dedupe",
109 | // url: "lines",
110 | // icon: ListOrderedIcon,
111 | // },
112 | ],
113 | },
114 | {
115 | title: "Data Converter",
116 | url: "#",
117 | items: [
118 | {
119 | title: "URL Parser",
120 | url: "url-parser",
121 | icon: LinkIcon,
122 | },
123 | // {
124 | // title: "YAML to JSON",
125 | // url: "yaml-to-json",
126 | // icon: FileIcon,
127 | // },
128 | // {
129 | // title: "JSON to YAML",
130 | // url: "json-to-yaml",
131 | // icon: FileJsonIcon,
132 | // },
133 | // {
134 | // title: "Number Base Converter",
135 | // url: "number-base-converter",
136 | // icon: BinaryIcon,
137 | // },
138 | // {
139 | // title: "JSON to CSV",
140 | // url: "json-to-csv",
141 | // icon: TableIcon,
142 | // },
143 | // {
144 | // title: "CSV to JSON",
145 | // url: "csv-to-json",
146 | // icon: TableIcon,
147 | // },
148 | // {
149 | // title: "HTML to JSX",
150 | // url: "html-to-jsx",
151 | // icon: Code2Icon,
152 | // },
153 | // {
154 | // title: "String Case Converter",
155 | // url: "string-case-converter",
156 | // icon: TypeIcon,
157 | // },
158 | // {
159 | // title: "PHP to JSON",
160 | // url: "php-to-json",
161 | // icon: FileCodeIcon,
162 | // },
163 | // {
164 | // title: "JSON to PHP",
165 | // url: "json-to-php",
166 | // icon: FileJsonIcon,
167 | // },
168 | // {
169 | // title: "PHP Serializer",
170 | // url: "php-serializer",
171 | // icon: FileArchiveIcon,
172 | // },
173 | // {
174 | // title: "PHP Unserializer",
175 | // url: "php-unserializer",
176 | // icon: FileArchiveIcon,
177 | // },
178 | // {
179 | // title: "SVG to CSS",
180 | // url: "svg-to-css",
181 | // icon: FileIcon,
182 | // },
183 | // {
184 | // title: "cURL to Code",
185 | // url: "curl-to-code",
186 | // icon: TerminalIcon,
187 | // },
188 | // {
189 | // title: "JSON to Code",
190 | // url: "json-to-code",
191 | // icon: CodeIcon,
192 | // },
193 | // {
194 | // title: "Hex to ASCII",
195 | // url: "hex-to-ascii",
196 | // icon: HashIcon,
197 | // },
198 | // {
199 | // title: "ASCII to Hex",
200 | // url: "ascii-to-hex",
201 | // icon: TextIcon,
202 | // },
203 | ],
204 | },
205 | {
206 | title: "Inspect, Preview, Debug",
207 | items: [
208 | {
209 | title: "Unix Time Converter",
210 | url: "unix-time-converter",
211 | icon: ClockIcon,
212 | },
213 | {
214 | title: "JWT Debugger",
215 | url: "jwt-debugger",
216 | icon: KeyIcon,
217 | },
218 | {
219 | title: "RegExp Tester",
220 | url: "regexp-tester",
221 | icon: RegexIcon,
222 | },
223 | {
224 | title: "HTML Preview",
225 | url: "html-preview",
226 | icon: FileCodeIcon,
227 | },
228 | {
229 | title: "Text Diff Checker",
230 | url: "text-diff-checker",
231 | icon: DiffIcon,
232 | },
233 | {
234 | title: "String Inspector",
235 | url: "string-inspector",
236 | icon: SearchIcon,
237 | },
238 | {
239 | title: "Markdown Preview",
240 | url: "markdown-preview",
241 | icon: FileTextIcon,
242 | },
243 | {
244 | title: "Cron Job Parser",
245 | url: "cron-job-parser",
246 | icon: CalendarIcon,
247 | },
248 | {
249 | title: "Color Converter",
250 | url: "color-converter",
251 | icon: PaletteIcon,
252 | },
253 | ],
254 | },
255 | {
256 | title: "Generators",
257 | url: "generator",
258 | items: [
259 | {
260 | title: "UUID/ULID Generate/Decode",
261 | url: "id",
262 | icon: HashIcon,
263 | },
264 | {
265 | title: "Lorem Ipsum Generator",
266 | url: "lorem-ipsum-generator",
267 | icon: TextIcon,
268 | },
269 | {
270 | title: "QR Code Reader/Generator",
271 | url: "qr-code-generator",
272 | icon: QrCodeIcon,
273 | },
274 | {
275 | title: "Hash Generator",
276 | url: "hash",
277 | icon: HashIcon,
278 | },
279 | {
280 | title: "Random String Generator",
281 | url: "random-string-generator",
282 | icon: TextIcon,
283 | },
284 | ],
285 | },
286 | {
287 | title: "Encoder, Decoder",
288 | items: [
289 | {
290 | title: "Base64 String Encode/Decode",
291 | url: "base64-string",
292 | icon: FileTextIcon,
293 | },
294 | {
295 | title: "Base64 Image Encode/Decode",
296 | url: "base64-image",
297 | icon: ImageIcon,
298 | },
299 | {
300 | title: "URL Encode/Decode",
301 | url: "url-encoder",
302 | icon: LinkIcon,
303 | },
304 | {
305 | title: "HTML Entity Encode/Decode",
306 | url: "html-entity",
307 | icon: FileCodeIcon,
308 | },
309 | {
310 | title: "Backslash Escape/Unescape",
311 | url: "backslash-escape",
312 | icon: TextIcon,
313 | },
314 | {
315 | title: "Certificate Decoder (X.509)",
316 | url: "certificate-decoder",
317 | icon: FileTextIcon,
318 | },
319 | ],
320 | },
321 | ];
322 |
323 | const InsetHeader: React.FC<{ title: string }> = ({ title }) => {
324 | const { open } = useSidebar();
325 |
326 | const setOnTop = async () => {
327 | await getCurrentWindow().setAlwaysOnTop(!isOnTop);
328 | setIsOnTop(!isOnTop);
329 | };
330 | const [isOnTop, setIsOnTop] = useState(false);
331 |
332 | return (
333 |
337 |
338 |
342 |
346 |
347 |
348 | {title}
349 |
350 |
351 |
352 |
353 |
356 |
357 | );
358 | };
359 |
360 | export default function AppSidebar({
361 | children,
362 | ...props
363 | }: React.ComponentProps) {
364 | const [title, setTitle] = useState("Developer Utility");
365 | const [pathname] = useLocation();
366 |
367 | return (
368 |
369 |
370 |
371 |
372 |
373 |
374 | {/* We create a SidebarGroup for each parent. */}
375 | {navMain.map((category) => (
376 |
377 | {category.title}
378 |
379 |
380 | {category.items.map((util) => {
381 | const href = `/${category.url}/${util.url}`;
382 | return (
383 |
384 |
385 | setTitle(util.title)}
389 | >
390 |
391 | {util.title}
392 |
393 |
394 |
395 | );
396 | })}
397 |
398 |
399 |
400 | ))}
401 |
402 |
403 |
404 |
405 |
406 |
407 | {children}
408 |
409 |
410 |
411 | );
412 | }
413 |
--------------------------------------------------------------------------------
/src/components/sidebar/search-form.tsx:
--------------------------------------------------------------------------------
1 | import { Search } from "lucide-react"
2 |
3 | import { Label } from "@/components/ui/label"
4 | import {
5 | SidebarGroup,
6 | SidebarGroupContent,
7 | SidebarInput,
8 | } from "@/components/ui/sidebar"
9 |
10 | export function SearchForm({ ...props }: React.ComponentProps<"form">) {
11 | return (
12 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/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 | destructive:
15 | "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",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32 | return (
33 |
38 | )
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42 | return (
43 |
48 | )
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52 | return (
53 |
61 | )
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65 | return (
66 |
71 | )
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | export {
85 | Card,
86 | CardHeader,
87 | CardFooter,
88 | CardTitle,
89 | CardAction,
90 | CardDescription,
91 | CardContent,
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3 | import { CheckIcon } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function Checkbox({
8 | className,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
20 |
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3 | import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function DropdownMenu({
8 | ...props
9 | }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function DropdownMenuPortal({
14 | ...props
15 | }: React.ComponentProps) {
16 | return (
17 |
18 | )
19 | }
20 |
21 | function DropdownMenuTrigger({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
29 | )
30 | }
31 |
32 | function DropdownMenuContent({
33 | className,
34 | sideOffset = 4,
35 | ...props
36 | }: React.ComponentProps) {
37 | return (
38 |
39 |
48 |
49 | )
50 | }
51 |
52 | function DropdownMenuGroup({
53 | ...props
54 | }: React.ComponentProps) {
55 | return (
56 |
57 | )
58 | }
59 |
60 | function DropdownMenuItem({
61 | className,
62 | inset,
63 | variant = "default",
64 | ...props
65 | }: React.ComponentProps & {
66 | inset?: boolean
67 | variant?: "default" | "destructive"
68 | }) {
69 | return (
70 |
80 | )
81 | }
82 |
83 | function DropdownMenuCheckboxItem({
84 | className,
85 | children,
86 | checked,
87 | ...props
88 | }: React.ComponentProps) {
89 | return (
90 |
99 |
100 |
101 |
102 |
103 |
104 | {children}
105 |
106 | )
107 | }
108 |
109 | function DropdownMenuRadioGroup({
110 | ...props
111 | }: React.ComponentProps) {
112 | return (
113 |
117 | )
118 | }
119 |
120 | function DropdownMenuRadioItem({
121 | className,
122 | children,
123 | ...props
124 | }: React.ComponentProps) {
125 | return (
126 |
134 |
135 |
136 |
137 |
138 |
139 | {children}
140 |
141 | )
142 | }
143 |
144 | function DropdownMenuLabel({
145 | className,
146 | inset,
147 | ...props
148 | }: React.ComponentProps & {
149 | inset?: boolean
150 | }) {
151 | return (
152 |
161 | )
162 | }
163 |
164 | function DropdownMenuSeparator({
165 | className,
166 | ...props
167 | }: React.ComponentProps) {
168 | return (
169 |
174 | )
175 | }
176 |
177 | function DropdownMenuShortcut({
178 | className,
179 | ...props
180 | }: React.ComponentProps<"span">) {
181 | return (
182 |
190 | )
191 | }
192 |
193 | function DropdownMenuSub({
194 | ...props
195 | }: React.ComponentProps) {
196 | return
197 | }
198 |
199 | function DropdownMenuSubTrigger({
200 | className,
201 | inset,
202 | children,
203 | ...props
204 | }: React.ComponentProps & {
205 | inset?: boolean
206 | }) {
207 | return (
208 |
217 | {children}
218 |
219 |
220 | )
221 | }
222 |
223 | function DropdownMenuSubContent({
224 | className,
225 | ...props
226 | }: React.ComponentProps) {
227 | return (
228 |
236 | )
237 | }
238 |
239 | export {
240 | DropdownMenu,
241 | DropdownMenuPortal,
242 | DropdownMenuTrigger,
243 | DropdownMenuContent,
244 | DropdownMenuGroup,
245 | DropdownMenuLabel,
246 | DropdownMenuItem,
247 | DropdownMenuCheckboxItem,
248 | DropdownMenuRadioGroup,
249 | DropdownMenuRadioItem,
250 | DropdownMenuSeparator,
251 | DropdownMenuShortcut,
252 | DropdownMenuSub,
253 | DropdownMenuSubTrigger,
254 | DropdownMenuSubContent,
255 | }
256 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { CircleIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function RadioGroup({
10 | className,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
19 | )
20 | }
21 |
22 | function RadioGroupItem({
23 | className,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
35 |
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | export { RadioGroup, RadioGroupItem }
46 |
--------------------------------------------------------------------------------
/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 | function ScrollArea({
7 | className,
8 | children,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
17 |
21 | {children}
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | function ScrollBar({
30 | className,
31 | orientation = "vertical",
32 | ...props
33 | }: React.ComponentProps) {
34 | return (
35 |
48 |
52 |
53 | )
54 | }
55 |
56 | export { ScrollArea, ScrollBar }
57 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Select({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function SelectGroup({
16 | ...props
17 | }: React.ComponentProps) {
18 | return
19 | }
20 |
21 | function SelectValue({
22 | ...props
23 | }: React.ComponentProps) {
24 | return
25 | }
26 |
27 | function SelectTrigger({
28 | className,
29 | size = "default",
30 | children,
31 | ...props
32 | }: React.ComponentProps & {
33 | size?: "sm" | "default"
34 | }) {
35 | return (
36 |
45 | {children}
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | function SelectContent({
54 | className,
55 | children,
56 | position = "popper",
57 | ...props
58 | }: React.ComponentProps) {
59 | return (
60 |
61 |
72 |
73 |
80 | {children}
81 |
82 |
83 |
84 |
85 | )
86 | }
87 |
88 | function SelectLabel({
89 | className,
90 | ...props
91 | }: React.ComponentProps) {
92 | return (
93 |
98 | )
99 | }
100 |
101 | function SelectItem({
102 | className,
103 | children,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
115 |
116 |
117 |
118 |
119 |
120 | {children}
121 |
122 | )
123 | }
124 |
125 | function SelectSeparator({
126 | className,
127 | ...props
128 | }: React.ComponentProps) {
129 | return (
130 |
135 | )
136 | }
137 |
138 | function SelectScrollUpButton({
139 | className,
140 | ...props
141 | }: React.ComponentProps) {
142 | return (
143 |
151 |
152 |
153 | )
154 | }
155 |
156 | function SelectScrollDownButton({
157 | className,
158 | ...props
159 | }: React.ComponentProps) {
160 | return (
161 |
169 |
170 |
171 | )
172 | }
173 |
174 | export {
175 | Select,
176 | SelectContent,
177 | SelectGroup,
178 | SelectItem,
179 | SelectLabel,
180 | SelectScrollDownButton,
181 | SelectScrollUpButton,
182 | SelectSeparator,
183 | SelectTrigger,
184 | SelectValue,
185 | }
186 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Separator({
9 | className,
10 | orientation = "horizontal",
11 | decorative = true,
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
25 | )
26 | }
27 |
28 | export { Separator }
29 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SheetPrimitive from "@radix-ui/react-dialog"
3 | import { XIcon } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | function Sheet({ ...props }: React.ComponentProps) {
8 | return
9 | }
10 |
11 | function SheetTrigger({
12 | ...props
13 | }: React.ComponentProps) {
14 | return
15 | }
16 |
17 | function SheetClose({
18 | ...props
19 | }: React.ComponentProps) {
20 | return
21 | }
22 |
23 | function SheetPortal({
24 | ...props
25 | }: React.ComponentProps) {
26 | return
27 | }
28 |
29 | function SheetOverlay({
30 | className,
31 | ...props
32 | }: React.ComponentProps) {
33 | return (
34 |
42 | )
43 | }
44 |
45 | function SheetContent({
46 | className,
47 | children,
48 | side = "right",
49 | ...props
50 | }: React.ComponentProps & {
51 | side?: "top" | "right" | "bottom" | "left"
52 | }) {
53 | return (
54 |
55 |
56 |
72 | {children}
73 |
74 |
75 | Close
76 |
77 |
78 |
79 | )
80 | }
81 |
82 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
83 | return (
84 |
89 | )
90 | }
91 |
92 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
93 | return (
94 |
99 | )
100 | }
101 |
102 | function SheetTitle({
103 | className,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
112 | )
113 | }
114 |
115 | function SheetDescription({
116 | className,
117 | ...props
118 | }: React.ComponentProps) {
119 | return (
120 |
125 | )
126 | }
127 |
128 | export {
129 | Sheet,
130 | SheetTrigger,
131 | SheetClose,
132 | SheetContent,
133 | SheetHeader,
134 | SheetFooter,
135 | SheetTitle,
136 | SheetDescription,
137 | }
138 |
--------------------------------------------------------------------------------
/src/components/ui/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, VariantProps } from "class-variance-authority";
4 | import { PanelLeftIcon } from "lucide-react";
5 |
6 | import { useIsMobile } from "@/hooks/use-mobile";
7 | import { cn } from "@/lib/utils";
8 | import { Button } from "@/components/ui/button";
9 | import { Input } from "@/components/ui/input";
10 | import { Separator } from "@/components/ui/separator";
11 | import {
12 | Sheet,
13 | SheetContent,
14 | SheetDescription,
15 | SheetHeader,
16 | SheetTitle,
17 | } from "@/components/ui/sheet";
18 | import { Skeleton } from "@/components/ui/skeleton";
19 | import {
20 | Tooltip,
21 | TooltipContent,
22 | TooltipProvider,
23 | TooltipTrigger,
24 | } from "@/components/ui/tooltip";
25 |
26 | const SIDEBAR_COOKIE_NAME = "sidebar_state";
27 | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
28 | const SIDEBAR_WIDTH = "16rem";
29 | const SIDEBAR_WIDTH_MOBILE = "18rem";
30 | const SIDEBAR_WIDTH_ICON = "3rem";
31 | const SIDEBAR_KEYBOARD_SHORTCUT = "b";
32 |
33 | type SidebarContextProps = {
34 | state: "expanded" | "collapsed";
35 | open: boolean;
36 | setOpen: (open: boolean) => void;
37 | openMobile: boolean;
38 | setOpenMobile: (open: boolean) => void;
39 | isMobile: boolean;
40 | toggleSidebar: () => void;
41 | };
42 |
43 | const SidebarContext = React.createContext(null);
44 |
45 | function useSidebar() {
46 | const context = React.useContext(SidebarContext);
47 | if (!context) {
48 | throw new Error("useSidebar must be used within a SidebarProvider.");
49 | }
50 |
51 | return context;
52 | }
53 |
54 | function SidebarProvider({
55 | defaultOpen = true,
56 | open: openProp,
57 | onOpenChange: setOpenProp,
58 | className,
59 | style,
60 | children,
61 | ...props
62 | }: React.ComponentProps<"div"> & {
63 | defaultOpen?: boolean;
64 | open?: boolean;
65 | onOpenChange?: (open: boolean) => void;
66 | }) {
67 | const isMobile = useIsMobile();
68 | const [openMobile, setOpenMobile] = React.useState(false);
69 |
70 | // This is the internal state of the sidebar.
71 | // We use openProp and setOpenProp for control from outside the component.
72 | const [_open, _setOpen] = React.useState(defaultOpen);
73 | const open = openProp ?? _open;
74 | const setOpen = React.useCallback(
75 | (value: boolean | ((value: boolean) => boolean)) => {
76 | const openState = typeof value === "function" ? value(open) : value;
77 | if (setOpenProp) {
78 | setOpenProp(openState);
79 | } else {
80 | _setOpen(openState);
81 | }
82 |
83 | // This sets the cookie to keep the sidebar state.
84 | document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
85 | },
86 | [setOpenProp, open]
87 | );
88 |
89 | // Helper to toggle the sidebar.
90 | const toggleSidebar = React.useCallback(() => {
91 | return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
92 | }, [isMobile, setOpen, setOpenMobile]);
93 |
94 | // Adds a keyboard shortcut to toggle the sidebar.
95 | React.useEffect(() => {
96 | const handleKeyDown = (event: KeyboardEvent) => {
97 | if (
98 | event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
99 | (event.metaKey || event.ctrlKey)
100 | ) {
101 | event.preventDefault();
102 | toggleSidebar();
103 | }
104 | };
105 |
106 | window.addEventListener("keydown", handleKeyDown);
107 | return () => window.removeEventListener("keydown", handleKeyDown);
108 | }, [toggleSidebar]);
109 |
110 | // We add a state so that we can do data-state="expanded" or "collapsed".
111 | // This makes it easier to style the sidebar with Tailwind classes.
112 | const state = open ? "expanded" : "collapsed";
113 |
114 | const contextValue = React.useMemo(
115 | () => ({
116 | state,
117 | open,
118 | setOpen,
119 | isMobile,
120 | openMobile,
121 | setOpenMobile,
122 | toggleSidebar,
123 | }),
124 | [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
125 | );
126 |
127 | return (
128 |
129 |
130 |
145 | {children}
146 |
147 |
148 |
149 | );
150 | }
151 |
152 | function Sidebar({
153 | side = "left",
154 | variant = "sidebar",
155 | collapsible = "offcanvas",
156 | className,
157 | children,
158 | ...props
159 | }: React.ComponentProps<"div"> & {
160 | side?: "left" | "right";
161 | variant?: "sidebar" | "floating" | "inset";
162 | collapsible?: "offcanvas" | "icon" | "none";
163 | }) {
164 | const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
165 |
166 | if (collapsible === "none") {
167 | return (
168 |
176 | {children}
177 |
178 | );
179 | }
180 |
181 | if (isMobile) {
182 | return (
183 |
184 |
196 |
197 | Sidebar
198 | Displays the mobile sidebar.
199 |
200 | {children}
201 |
202 |
203 | );
204 | }
205 |
206 | return (
207 |
215 | {/* This is what handles the sidebar gap on desktop */}
216 |
227 |
242 |
248 | {children}
249 |
250 |
251 |
252 | );
253 | }
254 |
255 | function SidebarTrigger({
256 | className,
257 | onClick,
258 | ...props
259 | }: React.ComponentProps) {
260 | const { toggleSidebar } = useSidebar();
261 |
262 | return (
263 |
278 | );
279 | }
280 |
281 | function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
282 | const { toggleSidebar } = useSidebar();
283 |
284 | return (
285 |
303 | );
304 | }
305 |
306 | function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
307 | return (
308 |
317 | );
318 | }
319 |
320 | function SidebarInput({
321 | className,
322 | ...props
323 | }: React.ComponentProps) {
324 | return (
325 |
331 | );
332 | }
333 |
334 | function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
335 | return (
336 |
342 | );
343 | }
344 |
345 | function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
346 | return (
347 |
353 | );
354 | }
355 |
356 | function SidebarSeparator({
357 | className,
358 | ...props
359 | }: React.ComponentProps) {
360 | return (
361 |
367 | );
368 | }
369 |
370 | function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
371 | return (
372 |
381 | );
382 | }
383 |
384 | function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
385 | return (
386 |
392 | );
393 | }
394 |
395 | function SidebarGroupLabel({
396 | className,
397 | asChild = false,
398 | ...props
399 | }: React.ComponentProps<"div"> & { asChild?: boolean }) {
400 | const Comp = asChild ? Slot : "div";
401 |
402 | return (
403 | svg]:size-4 [&>svg]:shrink-0",
408 | "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
409 | className
410 | )}
411 | {...props}
412 | />
413 | );
414 | }
415 |
416 | function SidebarGroupAction({
417 | className,
418 | asChild = false,
419 | ...props
420 | }: React.ComponentProps<"button"> & { asChild?: boolean }) {
421 | const Comp = asChild ? Slot : "button";
422 |
423 | return (
424 | svg]:size-4 [&>svg]:shrink-0",
429 | // Increases the hit area of the button on mobile.
430 | "after:absolute after:-inset-2 md:after:hidden",
431 | "group-data-[collapsible=icon]:hidden",
432 | className
433 | )}
434 | {...props}
435 | />
436 | );
437 | }
438 |
439 | function SidebarGroupContent({
440 | className,
441 | ...props
442 | }: React.ComponentProps<"div">) {
443 | return (
444 |
450 | );
451 | }
452 |
453 | function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
454 | return (
455 |
461 | );
462 | }
463 |
464 | function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
465 | return (
466 |
472 | );
473 | }
474 |
475 | const sidebarMenuButtonVariants = cva(
476 | "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
477 | {
478 | variants: {
479 | variant: {
480 | default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
481 | outline:
482 | "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
483 | },
484 | size: {
485 | default: "h-8 text-sm",
486 | sm: "h-7 text-xs",
487 | lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
488 | },
489 | },
490 | defaultVariants: {
491 | variant: "default",
492 | size: "default",
493 | },
494 | }
495 | );
496 |
497 | function SidebarMenuButton({
498 | asChild = false,
499 | isActive = false,
500 | variant = "default",
501 | size = "default",
502 | tooltip,
503 | className,
504 | ...props
505 | }: React.ComponentProps<"button"> & {
506 | asChild?: boolean;
507 | isActive?: boolean;
508 | tooltip?: string | React.ComponentProps;
509 | } & VariantProps) {
510 | const Comp = asChild ? Slot : "button";
511 | const { isMobile, state } = useSidebar();
512 |
513 | const button = (
514 |
522 | );
523 |
524 | if (!tooltip) {
525 | return button;
526 | }
527 |
528 | if (typeof tooltip === "string") {
529 | tooltip = {
530 | children: tooltip,
531 | };
532 | }
533 |
534 | return (
535 |
536 | {button}
537 |
543 |
544 | );
545 | }
546 |
547 | function SidebarMenuAction({
548 | className,
549 | asChild = false,
550 | showOnHover = false,
551 | ...props
552 | }: React.ComponentProps<"button"> & {
553 | asChild?: boolean;
554 | showOnHover?: boolean;
555 | }) {
556 | const Comp = asChild ? Slot : "button";
557 |
558 | return (
559 | svg]:size-4 [&>svg]:shrink-0",
564 | // Increases the hit area of the button on mobile.
565 | "after:absolute after:-inset-2 md:after:hidden",
566 | "peer-data-[size=sm]/menu-button:top-1",
567 | "peer-data-[size=default]/menu-button:top-1.5",
568 | "peer-data-[size=lg]/menu-button:top-2.5",
569 | "group-data-[collapsible=icon]:hidden",
570 | showOnHover &&
571 | "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
572 | className
573 | )}
574 | {...props}
575 | />
576 | );
577 | }
578 |
579 | function SidebarMenuBadge({
580 | className,
581 | ...props
582 | }: React.ComponentProps<"div">) {
583 | return (
584 |
598 | );
599 | }
600 |
601 | function SidebarMenuSkeleton({
602 | className,
603 | showIcon = false,
604 | ...props
605 | }: React.ComponentProps<"div"> & {
606 | showIcon?: boolean;
607 | }) {
608 | // Random width between 50 to 90%.
609 | const width = React.useMemo(() => {
610 | return `${Math.floor(Math.random() * 40) + 50}%`;
611 | }, []);
612 |
613 | return (
614 |
620 | {showIcon && (
621 |
625 | )}
626 |
635 |
636 | );
637 | }
638 |
639 | function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
640 | return (
641 |
651 | );
652 | }
653 |
654 | function SidebarMenuSubItem({
655 | className,
656 | ...props
657 | }: React.ComponentProps<"li">) {
658 | return (
659 |
665 | );
666 | }
667 |
668 | function SidebarMenuSubButton({
669 | asChild = false,
670 | size = "md",
671 | isActive = false,
672 | className,
673 | ...props
674 | }: React.ComponentProps<"a"> & {
675 | asChild?: boolean;
676 | size?: "sm" | "md";
677 | isActive?: boolean;
678 | }) {
679 | const Comp = asChild ? Slot : "a";
680 |
681 | return (
682 | svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
689 | "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
690 | size === "sm" && "text-xs",
691 | size === "md" && "text-sm",
692 | "group-data-[collapsible=icon]:hidden",
693 | className
694 | )}
695 | {...props}
696 | />
697 | );
698 | }
699 |
700 | export {
701 | Sidebar,
702 | SidebarContent,
703 | SidebarFooter,
704 | SidebarGroup,
705 | SidebarGroupAction,
706 | SidebarGroupContent,
707 | SidebarGroupLabel,
708 | SidebarHeader,
709 | SidebarInput,
710 | SidebarInset,
711 | SidebarMenu,
712 | SidebarMenuAction,
713 | SidebarMenuBadge,
714 | SidebarMenuButton,
715 | SidebarMenuItem,
716 | SidebarMenuSkeleton,
717 | SidebarMenuSub,
718 | SidebarMenuSubButton,
719 | SidebarMenuSubItem,
720 | SidebarProvider,
721 | SidebarRail,
722 | SidebarSeparator,
723 | SidebarTrigger,
724 | useSidebar,
725 | };
726 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export { Skeleton }
14 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/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 | function TooltipProvider({
7 | delayDuration = 0,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function Tooltip({
20 | ...props
21 | }: React.ComponentProps) {
22 | return (
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | function TooltipTrigger({
30 | ...props
31 | }: React.ComponentProps) {
32 | return
33 | }
34 |
35 | function TooltipContent({
36 | className,
37 | sideOffset = 0,
38 | children,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
43 |
52 | {children}
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
60 |
--------------------------------------------------------------------------------
/src/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 | @plugin "tailwind-scrollbar" {
4 | nocompatible: true;
5 | }
6 |
7 | @custom-variant dark (&:is(.dark *));
8 |
9 | @theme inline {
10 | --radius-sm: calc(var(--radius) - 4px);
11 | --radius-md: calc(var(--radius) - 2px);
12 | --radius-lg: var(--radius);
13 | --radius-xl: calc(var(--radius) + 4px);
14 | --color-background: var(--background);
15 | --color-foreground: var(--foreground);
16 | --color-card: var(--card);
17 | --color-card-foreground: var(--card-foreground);
18 | --color-popover: var(--popover);
19 | --color-popover-foreground: var(--popover-foreground);
20 | --color-primary: var(--primary);
21 | --color-primary-foreground: var(--primary-foreground);
22 | --color-secondary: var(--secondary);
23 | --color-secondary-foreground: var(--secondary-foreground);
24 | --color-muted: var(--muted);
25 | --color-muted-foreground: var(--muted-foreground);
26 | --color-accent: var(--accent);
27 | --color-accent-foreground: var(--accent-foreground);
28 | --color-destructive: var(--destructive);
29 | --color-border: var(--border);
30 | --color-input: var(--input);
31 | --color-ring: var(--ring);
32 | --color-chart-1: var(--chart-1);
33 | --color-chart-2: var(--chart-2);
34 | --color-chart-3: var(--chart-3);
35 | --color-chart-4: var(--chart-4);
36 | --color-chart-5: var(--chart-5);
37 | --color-sidebar: var(--sidebar);
38 | --color-sidebar-foreground: var(--sidebar-foreground);
39 | --color-sidebar-primary: var(--sidebar-primary);
40 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
41 | --color-sidebar-accent: var(--sidebar-accent);
42 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
43 | --color-sidebar-border: var(--sidebar-border);
44 | --color-sidebar-ring: var(--sidebar-ring);
45 | }
46 |
47 | :root {
48 | --radius: 0.625rem;
49 | --background: oklch(0.98 0.002 240);
50 | --foreground: oklch(0.15 0.003 240);
51 | --card: oklch(1 0 0);
52 | --card-foreground: oklch(0.15 0.003 240);
53 | --popover: oklch(1 0 0);
54 | --popover-foreground: oklch(0.15 0.003 240);
55 | --primary: oklch(0.25 0.015 240);
56 | --primary-foreground: oklch(0.98 0.002 240);
57 | --secondary: oklch(0.95 0.005 240);
58 | --secondary-foreground: oklch(0.25 0.015 240);
59 | --muted: oklch(0.94 0.008 240);
60 | --muted-foreground: oklch(0.45 0.012 240);
61 | --accent: oklch(0.92 0.01 240);
62 | --accent-foreground: oklch(0.25 0.015 240);
63 | --destructive: oklch(0.577 0.245 27.325);
64 | --border: oklch(0.88 0.008 240);
65 | --input: oklch(0.88 0.008 240);
66 | --ring: oklch(0.55 0.05 240);
67 | --sidebar: oklch(0.985 0 0); */
68 | --sidebar-foreground: oklch(0.145 0 0);
69 | --sidebar-primary: oklch(0.205 0 0);
70 | --sidebar-primary-foreground: oklch(0.985 0 0);
71 | --sidebar-accent: oklch(0.97 0 0);
72 | --sidebar-accent-foreground: oklch(0.205 0 0);
73 | --sidebar-border: oklch(0.922 0 0);
74 | --sidebar-ring: oklch(0.708 0 0);
75 | }
76 |
77 | .dark {
78 | --background: oklch(0.145 0 0);
79 | --foreground: oklch(0.985 0 0);
80 | --card: oklch(0.205 0 0);
81 | --card-foreground: oklch(0.985 0 0);
82 | --popover: oklch(0.205 0 0);
83 | --popover-foreground: oklch(0.985 0 0);
84 | --primary: oklch(0.922 0 0);
85 | --primary-foreground: oklch(0.205 0 0);
86 | --secondary: oklch(0.269 0 0);
87 | --secondary-foreground: oklch(0.985 0 0);
88 | --muted: oklch(0.269 0 0);
89 | --muted-foreground: oklch(0.708 0 0);
90 | --accent: oklch(0.269 0 0);
91 | --accent-foreground: oklch(0.985 0 0);
92 | --destructive: oklch(0.704 0.191 22.216);
93 | --border: oklch(1 0 0 / 10%);
94 | --input: oklch(1 0 0 / 15%);
95 | --ring: oklch(0.556 0 0);
96 | --chart-1: oklch(0.488 0.243 264.376);
97 | --chart-2: oklch(0.696 0.17 162.48);
98 | --chart-3: oklch(0.769 0.188 70.08);
99 | --chart-4: oklch(0.627 0.265 303.9);
100 | --chart-5: oklch(0.645 0.246 16.439);
101 | --sidebar: oklch(0.205 0 0);
102 | --sidebar-foreground: oklch(0.985 0 0);
103 | --sidebar-primary: oklch(0.488 0.243 264.376);
104 | --sidebar-primary-foreground: oklch(0.985 0 0);
105 | --sidebar-accent: oklch(0.269 0 0);
106 | --sidebar-accent-foreground: oklch(0.985 0 0);
107 | --sidebar-border: oklch(1 0 0 / 10%);
108 | --sidebar-ring: oklch(0.556 0 0);
109 | }
110 |
111 | @layer base {
112 | * {
113 | @apply border-border outline-ring/50;
114 | }
115 | html {
116 | @apply bg-transparent;
117 | }
118 | body {
119 | @apply bg-transparent text-foreground;
120 | }
121 | }
122 |
123 | ::-webkit-scrollbar {
124 | width: 8px;
125 | height: 8px;
126 | }
127 | ::-webkit-scrollbar-track {
128 | background: transparent;
129 | }
130 | ::-webkit-scrollbar-thumb {
131 | background: rgba(0, 0, 0, 0.25);
132 | border-radius: 4px;
133 | }
134 | ::-webkit-scrollbar-thumb:hover {
135 | background: rgba(0, 0, 0, 0.5);
136 | }
137 | ::-webkit-scrollbar-corner {
138 | background: transparent;
139 | }
140 |
--------------------------------------------------------------------------------
/src/lib/copyboard.ts:
--------------------------------------------------------------------------------
1 | import { writeText, readText } from "@tauri-apps/plugin-clipboard-manager";
2 |
3 | export async function copyToClipboard(text: string): Promise {
4 | try {
5 | await writeText(text);
6 | } catch (error) {
7 | console.error("Failed to copy to clipboard:", error);
8 | throw error;
9 | }
10 | }
11 |
12 | export async function readFromClipboard(): Promise {
13 | try {
14 | const content = await readText();
15 | return content;
16 | } catch (error) {
17 | console.error("Failed to read from clipboard:", error);
18 | throw error;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/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 React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./app";
4 |
5 | import { TooltipProvider } from "@radix-ui/react-tooltip";
6 |
7 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/tauri.ts:
--------------------------------------------------------------------------------
1 | import { invoke as invokeCore, InvokeOptions } from "@tauri-apps/api/core";
2 | import { InvokeFunction, IndentStyle, HashResult } from "./utilities/types";
3 |
4 | interface InvokeArgs {
5 | [InvokeFunction.GenerateUlid]: { count: number };
6 | [InvokeFunction.GenerateNanoid]: { count: number };
7 | [InvokeFunction.GenerateUuidV4]: { count: number };
8 | [InvokeFunction.GenerateUuidV7]: { count: number; timestamp?: number };
9 | [InvokeFunction.FormatJson]: { input: string; style: IndentStyle };
10 | [InvokeFunction.GenerateHashes]: { input: string };
11 | }
12 |
13 | interface InvokeReturns {
14 | [InvokeFunction.GenerateUlid]: string;
15 | [InvokeFunction.GenerateNanoid]: string;
16 | [InvokeFunction.GenerateUuidV4]: string;
17 | [InvokeFunction.GenerateUuidV7]: string;
18 | [InvokeFunction.FormatJson]: string;
19 | [InvokeFunction.GenerateHashes]: HashResult;
20 | }
21 |
22 | export function invoke(
23 | cmd: T,
24 | args: InvokeArgs[T],
25 | options?: InvokeOptions
26 | ): Promise {
27 | return invokeCore(cmd, args, options);
28 | }
29 |
--------------------------------------------------------------------------------
/src/utilities/formatter/css.tsx:
--------------------------------------------------------------------------------
1 | import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
2 |
3 | import { useState, useEffect } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { Textarea } from "@/components/ui/textarea";
6 | import { Label } from "@/components/ui/label";
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuRadioGroup,
11 | DropdownMenuRadioItem,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu";
14 | import {
15 | Tooltip,
16 | TooltipContent,
17 | TooltipProvider,
18 | TooltipTrigger,
19 | } from "@/components/ui/tooltip";
20 | import {
21 | Zap,
22 | Clipboard,
23 | FileText,
24 | Trash2,
25 | Copy,
26 | ChevronDown,
27 | } from "lucide-react";
28 |
29 | const sampleCssMinified = `@font-face{font-family:Chunkfive;src:url('Chunkfive.otf');}body,.usertext{color:#F0F0F0;background:#600;font-family:Chunkfive,sans;--heading-1:30px / 32px Helvetica,sans-serif;}@import url('print.css');@media print{a[href^=http]::after{content:attr(href)x;}}`;
30 |
31 | const sampleCssBeautified = `
32 | @font-face {
33 | font-family: Chunkfive;
34 | src: url('Chunkfive.otf');
35 | }
36 |
37 | body,
38 | .usertext {
39 | color: #F0F0F0;
40 | background: #600;
41 | font-family: Chunkfive, sans;
42 | --heading-1: 30px / 32px Helvetica, sans-serif;
43 | }
44 |
45 | @import url('print.css');
46 |
47 | @media print {
48 | a[href^="http"]::after {
49 | content: attr(href) x;
50 | }
51 | }
52 | `;
53 |
54 | // Very basic CSS beautifier
55 | function beautifyCss(css: string, indent: string): string {
56 | let beautifiedCss = css
57 | .replace(/\s*{\s*/g, ` {\n${indent}`)
58 | .replace(/\s*;\s*/g, `;\n${indent}`)
59 | .replace(/\s*}\s*/g, `\n}\n`)
60 | .replace(/^\s*\n/gm, "") // Remove empty lines at the start of blocks
61 | .replace(/\n\s*\n/g, "\n"); // Reduce multiple newlines to one
62 |
63 | // Indent lines within blocks
64 | let depth = 0;
65 | beautifiedCss = beautifiedCss
66 | .split("\n")
67 | .map((line) => {
68 | if (line.includes("}")) depth = Math.max(0, depth - 1);
69 | const currentIndent = indent.repeat(depth);
70 | const trimmedLine = line.trim();
71 | if (trimmedLine === "") return ""; // Keep intentional empty lines if any, or remove
72 | if (trimmedLine.endsWith("{")) depth++;
73 | return currentIndent + trimmedLine;
74 | })
75 | .join("\n")
76 | .trim();
77 |
78 | return beautifiedCss;
79 | }
80 |
81 | // Very basic CSS minifier
82 | function minifyCss(css: string): string {
83 | return css
84 | .replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "") // Remove comments
85 | .replace(/\s+/g, " ") // Collapse whitespace
86 | .replace(/\s*([{};:,])\s*/g, "$1") // Remove space around delimiters
87 | .replace(/;\s*}/g, "}") // Remove trailing semicolon in a block
88 | .trim();
89 | }
90 |
91 | export function CssBeautifyMinifyTool() {
92 | const [inputCss, setInputCss] = useState(sampleCssMinified);
93 | const [outputCss, setOutputCss] = useState("");
94 | const [indentation, setIndentation] = useState("2"); // "2", "4", "tab"
95 | const [mode, setMode] = useState<"beautify" | "minify">("beautify"); // Added mode state
96 |
97 | useEffect(() => {
98 | processCss();
99 | }, [inputCss, indentation, mode]);
100 |
101 | const processCss = () => {
102 | if (mode === "beautify") {
103 | const indentChar =
104 | indentation === "tab" ? "\t" : " ".repeat(Number.parseInt(indentation));
105 | setOutputCss(beautifyCss(inputCss, indentChar));
106 | } else {
107 | setOutputCss(minifyCss(inputCss));
108 | }
109 | };
110 |
111 | const handleProcessButtonClick = () => {
112 | processCss();
113 | };
114 |
115 | const handleCopyToClipboard = async (text: string) => {
116 | try {
117 | await navigator.clipboard.writeText(text);
118 | console.log("Copied to clipboard");
119 | } catch (err) {
120 | console.error("Failed to copy: ", err);
121 | }
122 | };
123 |
124 | const handlePasteFromClipboard = async () => {
125 | try {
126 | const text = await navigator.clipboard.readText();
127 | setInputCss(text);
128 | } catch (err) {
129 | console.error("Failed to paste: ", err);
130 | }
131 | };
132 |
133 | const handleLoadSample = () => {
134 | // Load sample based on current mode for better UX
135 | setInputCss(mode === "beautify" ? sampleCssMinified : sampleCssBeautified);
136 | };
137 |
138 | const handleClearInput = () => {
139 | setInputCss("");
140 | setOutputCss("");
141 | };
142 |
143 | const getIndentationLabel = (value: string) => {
144 | if (value === "tab") return "Tabs";
145 | return `${value} spaces`;
146 | };
147 |
148 | return (
149 |
150 |
151 |
152 | {/* Input Section */}
153 |
154 |
155 |
161 |
162 |
163 |
164 |
172 |
173 |
174 | Process CSS
175 |
176 |
177 |
178 |
179 |
187 |
188 |
189 | Paste
190 |
191 |
192 |
193 |
194 |
202 |
203 |
204 | Sample
205 |
206 |
207 |
208 |
209 |
217 |
218 |
219 | Clear
220 |
221 |
222 |
223 |
224 |
233 |
234 | {/* Output Section */}
235 |
236 |
237 |
243 |
244 |
245 |
246 |
252 |
253 |
254 |
257 | setMode(val as "beautify" | "minify")
258 | }
259 | >
260 |
261 | Beautify
262 |
263 |
264 | Minify
265 |
266 |
267 | {mode === "beautify" && (
268 | <>
269 |
270 |
274 |
275 | 2 spaces
276 |
277 |
278 | 4 spaces
279 |
280 |
281 | Tabs
282 |
283 |
284 | >
285 | )}
286 |
287 |
288 |
289 |
290 |
298 |
299 |
300 | Copy Output
301 |
302 |
303 |
304 |
305 |
313 |
314 |
315 |
316 |
317 | );
318 | }
319 |
--------------------------------------------------------------------------------
/src/utilities/formatter/html.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { Button } from "@/components/ui/button";
3 | import { Textarea } from "@/components/ui/textarea";
4 | import { Label } from "@/components/ui/label";
5 | import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
6 | import {
7 | Tooltip,
8 | TooltipContent,
9 | TooltipProvider,
10 | TooltipTrigger,
11 | } from "@/components/ui/tooltip";
12 | import {
13 | DropdownMenu,
14 | DropdownMenuContent,
15 | DropdownMenuItem,
16 | DropdownMenuCheckboxItem,
17 | DropdownMenuSeparator,
18 | DropdownMenuTrigger,
19 | } from "@/components/ui/dropdown-menu";
20 | import {
21 | Zap,
22 | Clipboard,
23 | FileText,
24 | Trash2,
25 | Settings,
26 | Copy,
27 | ArrowUpFromLine,
28 | } from "lucide-react";
29 |
30 | // Basic HTML entity maps
31 | const encodeMap: { [key: string]: string } = {
32 | "&": "&",
33 | "<": "<",
34 | ">": ">",
35 | '"': """,
36 | "'": "'",
37 | // Add more entities as needed
38 | };
39 |
40 | const decodeMap: { [key: string]: string } = {
41 | "&": "&",
42 | "<": "<",
43 | ">": ">",
44 | """: '"',
45 | "'": "'",
46 | // Add more entities as needed
47 | };
48 |
49 | const sampleHtml = 'Hello & Welcome!
\nThis is a "sample" text.
';
50 |
51 | export default function HtmlEncoderDecoderPage() {
52 | const [inputHtml, setInputHtml] = useState(sampleHtml);
53 | const [outputHtml, setOutputHtml] = useState("");
54 | const [mode, setMode] = useState<"encode" | "decode">("encode");
55 | const [continuousMode, setContinuousMode] = useState(true);
56 |
57 | const processHtml = (html: string, currentMode: "encode" | "decode") => {
58 | if (currentMode === "encode") {
59 | return html.replace(/[&<>"']/g, (match) => encodeMap[match] || match);
60 | } else {
61 | // A more robust decoder would handle numeric entities etc.
62 | // This is a simplified version.
63 | let decoded = html;
64 | for (const key in decodeMap) {
65 | const regex = new RegExp(key, "g");
66 | decoded = decoded.replace(regex, decodeMap[key]);
67 | }
68 | return decoded;
69 | }
70 | };
71 |
72 | useEffect(() => {
73 | if (continuousMode) {
74 | setOutputHtml(processHtml(inputHtml, mode));
75 | }
76 | }, [inputHtml, mode, continuousMode]);
77 |
78 | const handleProcessButtonClick = () => {
79 | setOutputHtml(processHtml(inputHtml, mode));
80 | };
81 |
82 | const handleCopyToClipboard = async (text: string) => {
83 | try {
84 | await navigator.clipboard.writeText(text);
85 | // Consider adding a toast notification for success
86 | console.log("Copied to clipboard");
87 | } catch (err) {
88 | console.error("Failed to copy: ", err);
89 | }
90 | };
91 |
92 | const handlePasteFromClipboard = async () => {
93 | try {
94 | const text = await navigator.clipboard.readText();
95 | setInputHtml(text);
96 | } catch (err) {
97 | console.error("Failed to paste: ", err);
98 | }
99 | };
100 |
101 | const handleLoadSample = () => {
102 | setInputHtml(sampleHtml);
103 | };
104 |
105 | const handleClearInput = () => {
106 | setInputHtml("");
107 | setOutputHtml("");
108 | };
109 |
110 | const handleUseOutputAsInput = () => {
111 | setInputHtml(outputHtml);
112 | };
113 |
114 | return (
115 |
116 |
117 | {/* Input Section */}
118 |
119 |
120 |
126 |
127 |
128 |
129 |
138 |
139 |
140 | Process HTML
141 |
142 |
143 |
144 |
145 |
153 |
154 |
155 | Paste
156 |
157 |
158 |
159 |
160 |
168 |
169 |
170 | Sample
171 |
172 |
173 |
174 |
175 |
183 |
184 |
185 | Clear
186 |
187 |
188 |
189 |
190 |
191 |
192 |
199 |
200 |
201 |
202 | Settings
203 |
204 |
205 |
206 |
210 | Continuous Mode
211 |
212 |
213 |
214 | More settings soon...
215 |
216 |
217 |
218 |
219 |
setMode(value)}
222 | className="flex items-center space-x-2"
223 | >
224 |
225 |
226 |
229 |
230 |
231 |
232 |
235 |
236 |
237 |
238 |
247 |
248 | {/* Output Section */}
249 |
250 |
251 |
257 |
258 |
259 |
260 |
268 |
269 |
270 | Copy Output
271 |
272 |
273 |
274 |
275 |
284 |
285 |
286 | Use Output as Input
287 |
288 |
289 |
290 |
291 |
299 |
300 |
301 |
302 | );
303 | }
304 |
--------------------------------------------------------------------------------
/src/utilities/formatter/js.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 | import { Button } from "@/components/ui/button"
5 | import { Textarea } from "@/components/ui/textarea"
6 | import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
7 | import { Label } from "@/components/ui/label"
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuCheckboxItem,
13 | DropdownMenuSeparator,
14 | DropdownMenuTrigger,
15 | } from "@/components/ui/dropdown-menu"
16 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
17 | import { Zap, Clipboard, FileText, Trash2, Settings, Copy, ArrowUpFromLine } from "lucide-react"
18 |
19 | export default function HtmlToolPage() {
20 | const [inputHtml, setInputHtml] = useState("Hello
")
21 | const [outputHtml, setOutputHtml] = useState("<h1>Hello</h1>")
22 | const [mode, setMode] = useState("encode") // "encode" or "decode"
23 |
24 | // Placeholder states for settings - adapt as needed
25 | const [indentSize, setIndentSize] = useState(2)
26 | const [wrapLines, setWrapLines] = useState(true)
27 |
28 | return (
29 |
30 |
31 | {/* Input Section */}
32 |
33 |
34 |
35 |
Input
36 |
37 |
38 |
41 |
42 |
43 | Beautify/Minify
44 |
45 |
46 |
47 |
48 |
51 |
52 |
53 | Paste from Clipboard
54 |
55 |
56 |
57 |
58 |
61 |
62 |
63 | Load Sample HTML
64 |
65 |
66 |
67 |
68 |
71 |
72 |
73 | Clear Input
74 |
75 |
76 |
77 |
78 |
79 |
80 |
87 |
88 |
89 |
90 | Settings
91 |
92 |
93 |
94 |
95 | Wrap lines
96 |
97 |
98 | Indent with 2 spaces
99 | Indent with 4 spaces
100 | Indent with tabs
101 |
102 |
103 |
104 |
105 |
106 |
107 |
110 |
111 |
112 |
113 |
116 |
117 |
118 |
119 |
127 |
128 | {/* Output Section */}
129 |
130 |
131 |
Output
132 |
133 |
134 |
135 |
139 |
140 |
141 | Copy Output
142 |
143 |
144 |
145 |
146 |
150 |
151 |
152 | Use Output as New Input
153 |
154 |
155 |
156 |
157 |
164 |
165 |
166 |
167 | )
168 | }
169 |
--------------------------------------------------------------------------------
/src/utilities/formatter/json.tsx:
--------------------------------------------------------------------------------
1 | import { useDebouncedValue } from "foxact/use-debounced-value";
2 | import { useState, useEffect } from "react";
3 | import { Button } from "@/components/ui/button";
4 | import { Textarea } from "@/components/ui/textarea";
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuCheckboxItem,
10 | DropdownMenuSeparator,
11 | DropdownMenuTrigger,
12 | DropdownMenuRadioGroup,
13 | DropdownMenuRadioItem,
14 | } from "@/components/ui/dropdown-menu";
15 | import { Input } from "@/components/ui/input";
16 | import {
17 | Tooltip,
18 | TooltipContent,
19 | TooltipTrigger,
20 | } from "@/components/ui/tooltip";
21 | import {
22 | Zap,
23 | Clipboard,
24 | FileText,
25 | Trash2,
26 | Settings,
27 | Copy,
28 | HelpCircle,
29 | ChevronDown,
30 | ZapIcon,
31 | SettingsIcon,
32 | } from "lucide-react";
33 | import { Label } from "@/components/ui/label";
34 | import { Checkbox } from "@/components/ui/checkbox";
35 | // import { InvokeFunction } from "@/tauri";
36 | import { invoke } from "@/tauri";
37 | import { IndentStyle, IndentStyleEnum, InvokeFunction } from "../types";
38 | import { copyToClipboard, readFromClipboard } from "@/lib/copyboard";
39 |
40 | const jsonExampleInput = `{
41 | "store": {
42 | "book": [
43 | {
44 | "category": "reference",
45 | "author": "Nigel Rees",
46 | "title": "Sayings of the Century",
47 | "price": 8.95
48 | },
49 | {
50 | "category": "fiction",
51 | "author": "Evelyn Waugh",
52 | "title": "Sword of Honour",
53 | "price": 12.99
54 | },
55 | {
56 | "category": "fiction",
57 | "author": "J. R. R. Tolkien",
58 | "title": "The Lord of the Rings",
59 | "isbn": "0-395-19395-8",
60 | "price": 22.99
61 | }
62 | ],
63 | "bicycle": {
64 | "color": "red",
65 | "price": 19.95
66 | }
67 | }
68 | }`;
69 |
70 | export default function JsonFormatterPage() {
71 | const [inputJson, setInputJson] = useState(jsonExampleInput);
72 | const debouncedInputJson = useDebouncedValue(inputJson, 100, false);
73 |
74 | const [outputJson, setOutputJson] = useState("");
75 | const [style, setStyle] = useState({
76 | [IndentStyleEnum.Spaces]: 2,
77 | }); // "2", "4", "tab"
78 |
79 | // Settings states
80 | const [autoDetect, setAutoDetect] = useState(true);
81 | const [allowTrailingCommas, setAllowTrailingCommas] = useState(false);
82 | const [autoRepair, setAutoRepair] = useState(true);
83 | const [continuousMode, setContinuousMode] = useState(true);
84 | const [sortKeys, setSortKeys] = useState(true);
85 |
86 | const handleFormat = async () => {
87 | const result = await invoke(InvokeFunction.FormatJson, {
88 | input: debouncedInputJson,
89 | style,
90 | });
91 | console.log(result);
92 | setOutputJson(result);
93 | };
94 |
95 | useEffect(() => {
96 | handleFormat();
97 | }, [debouncedInputJson, style]);
98 |
99 | const handleLoadSample = () => {
100 | setInputJson(jsonExampleInput);
101 | };
102 |
103 | const handlePasteFromClipboard = async () => {
104 | const clipboardContent = await readFromClipboard();
105 | setInputJson(clipboardContent);
106 | };
107 |
108 | const handleClearInput = () => {
109 | setInputJson("");
110 | setOutputJson("");
111 | };
112 |
113 | const handleResetSettings = () => {
114 | setAutoDetect(true);
115 | setAllowTrailingCommas(false);
116 | setAutoRepair(true);
117 | setContinuousMode(true);
118 | setSortKeys(true);
119 | };
120 |
121 | const getStyleLabel = (value: string) => {
122 | if (value === "tab") return "Tabs";
123 | return `${value} spaces`;
124 | };
125 |
126 | return (
127 |
128 |
129 | {/* Input Section */}
130 |
131 |
132 |
133 | Input
134 |
135 |
136 |
137 |
138 |
147 |
148 |
152 | Format JSON
153 |
154 |
155 |
156 |
157 |
165 |
166 |
170 | Paste from Clipboard
171 |
172 |
173 |
174 |
175 |
183 |
184 |
188 | Load Sample JSON
189 |
190 |
191 |
192 |
193 |
201 |
202 |
206 | Clear Input
207 |
208 |
209 |
210 |
211 |
212 |
213 |
220 |
221 |
222 |
223 | {
227 | if (checked === "indeterminate") {
228 | setAutoDetect(false);
229 | } else {
230 | setAutoDetect(checked);
231 | }
232 | }}
233 | className="mt-1"
234 | />
235 |
241 |
242 |
243 | {
247 | if (checked === "indeterminate") {
248 | setAllowTrailingCommas(false);
249 | } else {
250 | setAllowTrailingCommas(checked);
251 | }
252 | }}
253 | className="mt-1"
254 | />
255 |
261 |
262 |
263 |
{
267 | if (checked === "indeterminate") {
268 | setAutoRepair(false);
269 | } else {
270 | setAutoRepair(checked);
271 | }
272 | }}
273 | className="mt-1"
274 | />
275 |
287 |
288 |
289 | {
293 | if (checked === "indeterminate") {
294 | setContinuousMode(false);
295 | } else {
296 | setContinuousMode(checked);
297 | }
298 | }}
299 | className="mt-1"
300 | />
301 |
308 |
309 |
310 | {
314 | if (checked === "indeterminate") {
315 | setSortKeys(false);
316 | } else {
317 | setSortKeys(checked);
318 | }
319 | }}
320 | className="mt-1"
321 | />
322 |
328 |
329 |
330 |
336 |
347 |
348 |
356 |
357 |
358 | {/*
359 | */}
360 |
364 |
369 | Auto detect when input is valid JSON
370 |
371 |
376 | Allow trailing commas and comments
377 |
378 |
383 |
384 | Auto repair invalid JSON if possible
385 |
386 | Fix missing quotes, strip commas, etc.
387 |
388 |
389 |
390 |
395 | Continuous Mode: format as you type
396 |
397 |
402 | Sort keys in output
403 |
404 |
405 |
409 | Reset to Defaults
410 |
411 |
412 |
413 |
414 |
415 |
416 |
423 |
424 |
428 |
429 |
433 | JSON
434 |
435 |
440 | XML (soon)
441 |
442 |
447 | YAML (soon)
448 |
449 |
450 |
451 |
452 |
453 |
454 |
462 |
463 | {/* Output Section */}
464 |
465 |
466 |
467 | Output
468 |
469 |
470 |
471 |
472 |
482 |
483 |
487 | {
494 | if (value === IndentStyleEnum.Tabs) {
495 | setStyle(IndentStyleEnum.Tabs);
496 | } else if (value === IndentStyleEnum.Minified) {
497 | setStyle(IndentStyleEnum.Minified);
498 | } else {
499 | setStyle({ [IndentStyleEnum.Spaces]: Number(value) });
500 | }
501 | }}
502 | >
503 |
507 | 2 spaces
508 |
509 |
513 | 4 spaces
514 |
515 |
519 | Tabs
520 |
521 |
525 | Minified
526 |
527 |
528 |
529 |
530 |
531 |
532 |
540 |
541 |
545 | Copy Output
546 |
547 |
548 |
549 |
550 |
557 |
558 |
563 |
564 |
565 |
572 |
573 |
577 | JSONPath Help
578 |
579 |
580 |
581 |
582 |
583 |
584 | );
585 | }
586 |
--------------------------------------------------------------------------------
/src/utilities/generators/hash.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3 | import { Input } from "@/components/ui/input";
4 | import { Textarea } from "@/components/ui/textarea";
5 | import { Button } from "@/components/ui/button";
6 | import { Checkbox } from "@/components/ui/checkbox";
7 | import { Label } from "@/components/ui/label";
8 | import { ScrollArea } from "@/components/ui/scroll-area";
9 | import { HashAlgorithm, HashResult, InvokeFunction } from "../types";
10 | import { invoke } from "@/tauri";
11 | import { useDebouncedValue } from "foxact/use-debounced-value";
12 |
13 | export default function HashGeneratorPage() {
14 | const [input, setInput] = useState("");
15 | const debouncedInput = useDebouncedValue(input, 100, false);
16 |
17 | const [hashResult, setHashResult] = useState>({});
18 | const [lowercased, setLowercased] = useState(false);
19 |
20 | const handleGenerateHashes = async () => {
21 | const result = await invoke(InvokeFunction.GenerateHashes, {
22 | input: debouncedInput,
23 | });
24 | console.log(result);
25 | setHashResult(result);
26 | };
27 |
28 | useEffect(() => {
29 | handleGenerateHashes();
30 | }, [debouncedInput]);
31 |
32 | return (
33 |
34 | {/* Left: Input Area */}
35 |
36 |
37 | Input:
38 |
39 |
40 |
41 |
44 |
47 |
50 |
53 |
54 |
55 |
62 |
63 |
64 |
65 | {/* Right: Hash Results */}
66 |
67 |
68 |
69 |
70 | Length: {input.length}
71 |
72 |
73 |
74 | setLowercased(!!v)}
78 | />
79 |
82 |
83 |
84 |
85 | {Object.values(HashAlgorithm).map((algo) => (
86 |
87 |
93 |
100 |
101 | ))}
102 |
103 |
104 |
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/src/utilities/generators/id.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 | import { Button } from "@/components/ui/button";
3 | import { Input } from "@/components/ui/input";
4 | import { Textarea } from "@/components/ui/textarea";
5 | import { Label } from "@/components/ui/label";
6 | import { Checkbox } from "@/components/ui/checkbox";
7 | import {
8 | Select,
9 | SelectContent,
10 | SelectItem,
11 | SelectTrigger,
12 | SelectValue,
13 | } from "@/components/ui/select";
14 | import {
15 | DropdownMenu,
16 | DropdownMenuContent,
17 | DropdownMenuItem,
18 | DropdownMenuTrigger,
19 | } from "@/components/ui/dropdown-menu";
20 | import {
21 | Tooltip,
22 | TooltipContent,
23 | TooltipProvider,
24 | TooltipTrigger,
25 | } from "@/components/ui/tooltip";
26 | import {
27 | ClipboardIcon,
28 | FileText,
29 | Trash2,
30 | Settings,
31 | Copy,
32 | RefreshCw,
33 | X,
34 | } from "lucide-react";
35 | import { useFastClick } from "foxact/use-fast-click";
36 | import { InvokeFunction } from "../types";
37 | import { invoke } from "@/tauri";
38 |
39 | const sampleUuid = "f469e069-221e-401e-b495-646a773b055f";
40 | const sampleRawContents = "f4:69:e0:69:22:1e:40:1e:b4:95:64:6a:77:3b:05:5f";
41 | const sampleVersion = "4 (random)";
42 | const sampleVariant = "Standard (DCE 1.1, ISO/IEC 11578:1996)";
43 |
44 | const sampleGeneratedIds = [
45 | "R6JIB99DISW4PYWAWD0UU",
46 | "CZCQICQKLQWRDNSIJUGS",
47 | "88_Z~WVFIHEOSZ6EPHWQ6",
48 | "PZ_IAM4IEKHPDXUFTNJGR",
49 | "~Z3YKKNF4WXJ8YBL93N~C",
50 | "QQTUCSFL04INTQKNZACBB",
51 | "XFK0MXJZWKQJRZX2G0QIP",
52 | "TSKHQDHTUB0OBNYJTSNSM",
53 | "SK0QDGRYDXXAKARNT_0ZD",
54 | "DYBNGWZNZB5VFARGXPZ~D",
55 | "9SNXDHBMK2VT8YEJ9Q04X",
56 | "8WF5TKWQQPRLHX_GPD~2G",
57 | "KRDPNTRCALJLQGV0ZEVWD",
58 | "DIRCIMLG7QFYPQ0QDR0PJE",
59 | "R83KBRR0GRFW0CQJGCQZQ",
60 | "CLVFYJC0M~6MBGANZCVYN",
61 | "VX9QF7ZRAE009DCBJCVZPZ",
62 | "PRBZF03ZP7M~ONZ72D9GP",
63 | "SEE40CV001A8REEI4YPW1",
64 | "SBCBFEALEI7YAF8BBE8AE",
65 | "LV7DKSVVMQVWVCRV804GZ",
66 | "KDEHC868SZDTLFU2JTQB6",
67 | "BEVDPW9XL6CXJ0JTONCRG",
68 | ].join("\n");
69 |
70 | enum IdType {
71 | NANOID = "nanoid",
72 | ULID = "ulid",
73 | UUID_V4 = "uuidv4",
74 | UUID_V1 = "uuidv1",
75 | UUID_V3 = "uuidv3",
76 | UUID_V5 = "uuidv5",
77 | UUID_V6 = "uuidv6",
78 | UUID_V7 = "uuidv7",
79 | UUID_V8 = "uuidv8",
80 | }
81 | export default function IdGeneratorPage() {
82 | const [idType, setIdType] = useState(IdType.NANOID);
83 | const [generatedIds, setGeneratedIds] = useState(sampleGeneratedIds);
84 | const [generateCount, setGenerateCount] = useState(100);
85 |
86 | const [inputValue, setInputValue] = useState(sampleUuid);
87 | const [standardFormat, setStandardFormat] = useState(sampleUuid);
88 | const [rawContents, setRawContents] = useState(sampleRawContents);
89 | const [version, setVersion] = useState(sampleVersion);
90 | const [variant, setVariant] = useState(sampleVariant);
91 |
92 | const [isLowercase, setIsLowercase] = useState(false);
93 |
94 | const handleCopy = async (textToCopy: string) => {
95 | try {
96 | await navigator.clipboard.writeText(textToCopy);
97 | // Add toast notification for success if desired
98 | console.log("Copied:", textToCopy);
99 | } catch (err) {
100 | console.error("Failed to copy:", err);
101 | }
102 | };
103 |
104 | const generateIds = useCallback(async (idType: IdType, count: number) => {
105 | console.log("generateIds", idType, count);
106 | switch (idType) {
107 | case IdType.NANOID: {
108 | const result = await invoke(InvokeFunction.GenerateNanoid, {
109 | count,
110 | });
111 | setGeneratedIds(result);
112 | break;
113 | }
114 | case IdType.ULID: {
115 | const result = await invoke(InvokeFunction.GenerateUlid, {
116 | count,
117 | });
118 | setGeneratedIds(result);
119 | break;
120 | }
121 | case IdType.UUID_V7: {
122 | const result = await invoke(InvokeFunction.GenerateUuidV7, {
123 | count,
124 | // timestamp: 0,
125 | });
126 | setGeneratedIds(result);
127 | break;
128 | }
129 | case IdType.UUID_V4:
130 | default: {
131 | const result = await invoke(InvokeFunction.GenerateUuidV4, {
132 | count,
133 | });
134 | setGeneratedIds(result);
135 | break;
136 | }
137 | }
138 | }, []);
139 | return (
140 |
141 |
142 | {/* Left Panel: Input & Details */}
143 |
144 |
145 |
146 | Input:
147 |
148 |
149 |
150 |
153 |
154 |
155 | Paste from Clipboard
156 |
157 |
158 |
159 |
160 |
163 |
164 |
165 | Load Sample ID
166 |
167 |
168 |
169 |
170 |
173 |
174 |
175 | Clear Input
176 |
177 |
178 |
179 |
180 |
181 |
182 |
189 |
190 |
191 |
192 | Settings
193 |
194 |
195 |
196 | UUID Settings...
197 | NanoID Settings...
198 |
199 |
200 |
201 |
202 |
setInputValue(e.target.value)}
206 | placeholder="Enter UUID or trigger generation"
207 | className="bg-input border-border text-foreground"
208 | />
209 |
210 |
211 |
216 |
221 |
226 |
231 |
232 |
233 |
234 | {/* Right Panel: Generate New IDs */}
235 |
236 |
237 | Generate new IDs
238 |
239 |
240 |
247 |
251 |
263 |
264 |
265 |
284 |
x
285 |
{
289 | setGenerateCount(Number.parseInt(e.target.value, 10) || 1);
290 | }}
291 | className="w-20 h-9 bg-input border-border"
292 | min="1"
293 | />
294 |
295 |
299 | setIsLowercase(checked as boolean)
300 | }
301 | />
302 |
305 |
306 |
307 |
314 |
315 |
316 |
317 | );
318 | }
319 |
320 | interface FieldWithCopyProps {
321 | label: string;
322 | value: string;
323 | onCopy: (value: string) => void;
324 | }
325 |
326 | function FieldWithCopy({ label, value, onCopy }: FieldWithCopyProps) {
327 | return (
328 |
329 |
330 |
331 |
337 |
338 |
339 |
347 |
348 |
349 | Copy
350 |
351 |
352 |
353 |
354 | );
355 | }
356 |
--------------------------------------------------------------------------------
/src/utilities/types.ts:
--------------------------------------------------------------------------------
1 | export enum InvokeFunction {
2 | GenerateUlid = "generate_ulid",
3 | GenerateNanoid = "generate_nanoid",
4 | GenerateUuidV4 = "generate_uuid_v4",
5 | GenerateUuidV7 = "generate_uuid_v7",
6 | GenerateHashes = "generate_hashes",
7 | FormatJson = "format_json",
8 | }
9 | export enum HashAlgorithm {
10 | MD2 = "md2",
11 | MD4 = "md4",
12 | MD5 = "md5",
13 | SHA1 = "sha1",
14 | SHA224 = "sha224",
15 | SHA256 = "sha256",
16 | SHA384 = "sha384",
17 | SHA512 = "sha512",
18 | SHA3_256 = "sha3_256",
19 | Keccak256 = "keccak256",
20 | }
21 | export type HashResult = {
22 | [HashAlgorithm.MD2]: string;
23 | [HashAlgorithm.MD4]: string;
24 | [HashAlgorithm.MD5]: string;
25 | [HashAlgorithm.SHA1]: string;
26 | [HashAlgorithm.SHA224]: string;
27 | [HashAlgorithm.SHA256]: string;
28 | [HashAlgorithm.SHA384]: string;
29 | [HashAlgorithm.SHA512]: string;
30 | [HashAlgorithm.SHA3_256]: string;
31 | };
32 | export enum IndentStyleEnum {
33 | Spaces = "spaces",
34 | Tabs = "tabs",
35 | Minified = "minified",
36 | }
37 | export type IndentStyle =
38 | | { [IndentStyleEnum.Spaces]: number }
39 | | IndentStyleEnum.Tabs
40 | | IndentStyleEnum.Minified;
41 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 |
23 | "baseUrl": ".",
24 | "paths": {
25 | "@/*": ["./src/*"]
26 | }
27 | },
28 | "include": ["src"],
29 | "references": [{ "path": "./tsconfig.node.json" }]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import tailwindcss from "@tailwindcss/vite";
3 | import { defineConfig } from "vite";
4 | import react from "@vitejs/plugin-react";
5 |
6 | // @ts-expect-error process is a nodejs global
7 | const host = process.env.TAURI_DEV_HOST;
8 |
9 | // https://vitejs.dev/config/
10 | export default defineConfig(async () => ({
11 | plugins: [react(), tailwindcss()],
12 | resolve: {
13 | alias: {
14 | "@": path.resolve(__dirname, "./src"),
15 | },
16 | },
17 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
18 | //
19 | // 1. prevent vite from obscuring rust errors
20 | clearScreen: false,
21 | // 2. tauri expects a fixed port, fail if that port is not available
22 | server: {
23 | port: 1420,
24 | strictPort: true,
25 | host: host || false,
26 | hmr: host
27 | ? {
28 | protocol: "ws",
29 | host,
30 | port: 1421,
31 | }
32 | : undefined,
33 | watch: {
34 | // 3. tell vite to ignore watching `src-tauri`
35 | ignored: ["**/src-tauri/**"],
36 | },
37 | },
38 | }));
39 |
--------------------------------------------------------------------------------