wip
This commit is contained in:
		
							parent
							
								
									00fd51a8e3
								
							
						
					
					
						commit
						78e185a281
					
				
							
								
								
									
										24
									
								
								apps/it-tools/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								apps/it-tools/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| # build output | ||||
| dist/ | ||||
| # generated types | ||||
| .astro/ | ||||
| 
 | ||||
| # dependencies | ||||
| node_modules/ | ||||
| 
 | ||||
| # logs | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| 
 | ||||
| 
 | ||||
| # environment variables | ||||
| .env | ||||
| .env.production | ||||
| 
 | ||||
| # macOS-specific files | ||||
| .DS_Store | ||||
| 
 | ||||
| # jetbrains setting folder | ||||
| .idea/ | ||||
							
								
								
									
										47
									
								
								apps/it-tools/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								apps/it-tools/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| # Astro Starter Kit: Minimal | ||||
| 
 | ||||
| ```sh | ||||
| pnpm create astro@latest -- --template minimal | ||||
| ``` | ||||
| 
 | ||||
| [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal) | ||||
| [](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal) | ||||
| [](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json) | ||||
| 
 | ||||
| > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun! | ||||
| 
 | ||||
| ## 🚀 Project Structure | ||||
| 
 | ||||
| Inside of your Astro project, you'll see the following folders and files: | ||||
| 
 | ||||
| ```text | ||||
| / | ||||
| ├── public/ | ||||
| ├── src/ | ||||
| │   └── pages/ | ||||
| │       └── index.astro | ||||
| └── package.json | ||||
| ``` | ||||
| 
 | ||||
| Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. | ||||
| 
 | ||||
| There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. | ||||
| 
 | ||||
| Any static assets, like images, can be placed in the `public/` directory. | ||||
| 
 | ||||
| ## 🧞 Commands | ||||
| 
 | ||||
| All commands are run from the root of the project, from a terminal: | ||||
| 
 | ||||
| | Command                   | Action                                           | | ||||
| | :------------------------ | :----------------------------------------------- | | ||||
| | `pnpm install`             | Installs dependencies                            | | ||||
| | `pnpm dev`             | Starts local dev server at `localhost:4321`      | | ||||
| | `pnpm build`           | Build your production site to `./dist/`          | | ||||
| | `pnpm preview`         | Preview your build locally, before deploying     | | ||||
| | `pnpm astro ...`       | Run CLI commands like `astro add`, `astro check` | | ||||
| | `pnpm astro -- --help` | Get help using the Astro CLI                     | | ||||
| 
 | ||||
| ## 👀 Want to learn more? | ||||
| 
 | ||||
| Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). | ||||
							
								
								
									
										32
									
								
								apps/it-tools/astro.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								apps/it-tools/astro.config.mjs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| // @ts-check
 | ||||
| import { defineConfig } from 'astro/config'; | ||||
| 
 | ||||
| import solidJs from '@astrojs/solid-js'; | ||||
| import UnoCSS from 'unocss/astro' | ||||
| import { locales, defaultLocale } from './src/i18n/languages'; | ||||
| import pagefind from "astro-pagefind"; | ||||
| 
 | ||||
| 
 | ||||
| // https://astro.build/config
 | ||||
| export default defineConfig({ | ||||
|   integrations: [ | ||||
|     solidJs(),  | ||||
|     UnoCSS({ injectReset: true }), | ||||
|     pagefind(), | ||||
|   ], | ||||
|   i18n: { | ||||
|     locales, | ||||
|     defaultLocale, | ||||
|   }, | ||||
|   markdown: { | ||||
|     shikiConfig: { | ||||
|       themes: { | ||||
|         light: 'vitesse-light', | ||||
|         dark: 'vitesse-dark', | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   build: { | ||||
|      | ||||
|   }, | ||||
| }); | ||||
| @ -3,15 +3,14 @@ | ||||
|   "uno": { | ||||
|     "config": "uno.config.ts", | ||||
|     "css": { | ||||
|       "path": "src/client/app.css", | ||||
|       "path": "src/assets/app.css", | ||||
|       "variable": true | ||||
|     }, | ||||
|     "color": "neutral", | ||||
|     "prefix": "" | ||||
|   }, | ||||
|   "alias": { | ||||
|     "component": "@/modules/ui/components", | ||||
|     "ui": "@/modules/ui/components", | ||||
|     "cn": "@/modules/ui/utils/cn" | ||||
|     "component": "@/components", | ||||
|     "cn": "@/libs/cn" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										814
									
								
								apps/it-tools/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										814
									
								
								apps/it-tools/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,814 @@ | ||||
| { | ||||
| 	"name": "@it-tools/app", | ||||
| 	"version": "0.0.1", | ||||
| 	"lockfileVersion": 3, | ||||
| 	"requires": true, | ||||
| 	"packages": { | ||||
| 		"": { | ||||
| 			"name": "@it-tools/app", | ||||
| 			"version": "0.0.1", | ||||
| 			"dependencies": { | ||||
| 				"@astrojs/solid-js": "^5.1.0", | ||||
| 				"@kobalte/core": "^0.13.10", | ||||
| 				"astro": "^5.12.0", | ||||
| 				"astro-pagefind": "^1.8.3", | ||||
| 				"class-variance-authority": "^0.7.1", | ||||
| 				"clsx": "^2.1.1", | ||||
| 				"solid-js": "^1.9.6", | ||||
| 				"tailwind-merge": "^3.2.0", | ||||
| 				"unocss-preset-animations": "^1.2.1" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| 				"@iconify-json/solar": "^1.2.2", | ||||
| 				"@iconify-json/tabler": "^1.2.19", | ||||
| 				"@unocss/reset": "^66.3.3", | ||||
| 				"unocss": "66.1.0-beta.13", | ||||
| 				"vitest": "^3.2.4" | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/@astrojs+solid-js@5.1.0_@types+node@24.0.14_jiti@2.4.2_solid-js@1.9.6/node_modules/@astrojs/solid-js": { | ||||
| 			"version": "5.1.0", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"vite": "^6.3.5", | ||||
| 				"vite-plugin-solid": "^2.11.6" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| 				"astro": "5.8.0", | ||||
| 				"astro-scripts": "0.0.14", | ||||
| 				"solid-js": "^1.9.7" | ||||
| 			}, | ||||
| 			"engines": { | ||||
| 				"node": "18.20.8 || ^20.3.0 || >=22.0.0" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-devtools": "^0.30.1", | ||||
| 				"solid-js": "^1.8.5" | ||||
| 			}, | ||||
| 			"peerDependenciesMeta": { | ||||
| 				"solid-devtools": { | ||||
| 					"optional": true | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/@iconify-json+solar@1.2.2/node_modules/@iconify-json/solar": { | ||||
| 			"version": "1.2.2", | ||||
| 			"dev": true, | ||||
| 			"license": "CC-BY-4.0", | ||||
| 			"dependencies": { | ||||
| 				"@iconify/types": "*" | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/@iconify-json+tabler@1.2.19/node_modules/@iconify-json/tabler": { | ||||
| 			"version": "1.2.19", | ||||
| 			"dev": true, | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@iconify/types": "*" | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/@unocss+reset@66.3.3/node_modules/@unocss/reset": { | ||||
| 			"version": "66.3.3", | ||||
| 			"dev": true, | ||||
| 			"license": "MIT", | ||||
| 			"devDependencies": { | ||||
| 				"@csstools/normalize.css": "^12.1.1", | ||||
| 				"sanitize.css": "^13.0.0" | ||||
| 			}, | ||||
| 			"funding": { | ||||
| 				"url": "https://github.com/sponsors/antfu" | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/astro-pagefind@1.8.3_astro@5.12.0_@types+node@24.0.14_jiti@2.4.2_rollup@4.40.1_typescript@5.8.3_/node_modules/astro-pagefind": { | ||||
| 			"version": "1.8.3", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@pagefind/default-ui": "^1.2.0", | ||||
| 				"pagefind": "^1.2.0", | ||||
| 				"sirv": "^3.0.0" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| 				"@astrojs/check": "0.9.4", | ||||
| 				"@astrojs/markdown-remark": "6.3.0", | ||||
| 				"@semantic-release/changelog": "6.0.3", | ||||
| 				"@semantic-release/git": "10.0.1", | ||||
| 				"@types/semantic-release": "20.0.6", | ||||
| 				"astro": "5.5.2", | ||||
| 				"semantic-release": "24.2.3", | ||||
| 				"typescript": "5.8.2" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"astro": "^2.0.4 || ^3 || ^4 || ^5" | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/astro@5.12.0_@types+node@24.0.14_jiti@2.4.2_rollup@4.40.1_typescript@5.8.3/node_modules/astro": { | ||||
| 			"version": "5.12.0", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@astrojs/compiler": "^2.12.2", | ||||
| 				"@astrojs/internal-helpers": "0.6.1", | ||||
| 				"@astrojs/markdown-remark": "6.3.3", | ||||
| 				"@astrojs/telemetry": "3.3.0", | ||||
| 				"@capsizecss/unpack": "^2.4.0", | ||||
| 				"@oslojs/encoding": "^1.1.0", | ||||
| 				"@rollup/pluginutils": "^5.1.4", | ||||
| 				"acorn": "^8.14.1", | ||||
| 				"aria-query": "^5.3.2", | ||||
| 				"axobject-query": "^4.1.0", | ||||
| 				"boxen": "8.0.1", | ||||
| 				"ci-info": "^4.2.0", | ||||
| 				"clsx": "^2.1.1", | ||||
| 				"common-ancestor-path": "^1.0.1", | ||||
| 				"cookie": "^1.0.2", | ||||
| 				"cssesc": "^3.0.0", | ||||
| 				"debug": "^4.4.0", | ||||
| 				"deterministic-object-hash": "^2.0.2", | ||||
| 				"devalue": "^5.1.1", | ||||
| 				"diff": "^5.2.0", | ||||
| 				"dlv": "^1.1.3", | ||||
| 				"dset": "^3.1.4", | ||||
| 				"es-module-lexer": "^1.6.0", | ||||
| 				"esbuild": "^0.25.0", | ||||
| 				"estree-walker": "^3.0.3", | ||||
| 				"flattie": "^1.1.1", | ||||
| 				"fontace": "~0.3.0", | ||||
| 				"github-slugger": "^2.0.0", | ||||
| 				"html-escaper": "3.0.3", | ||||
| 				"http-cache-semantics": "^4.1.1", | ||||
| 				"import-meta-resolve": "^4.1.0", | ||||
| 				"js-yaml": "^4.1.0", | ||||
| 				"kleur": "^4.1.5", | ||||
| 				"magic-string": "^0.30.17", | ||||
| 				"magicast": "^0.3.5", | ||||
| 				"mrmime": "^2.0.1", | ||||
| 				"neotraverse": "^0.6.18", | ||||
| 				"p-limit": "^6.2.0", | ||||
| 				"p-queue": "^8.1.0", | ||||
| 				"package-manager-detector": "^1.1.0", | ||||
| 				"picomatch": "^4.0.2", | ||||
| 				"prompts": "^2.4.2", | ||||
| 				"rehype": "^13.0.2", | ||||
| 				"semver": "^7.7.1", | ||||
| 				"shiki": "^3.2.1", | ||||
| 				"smol-toml": "^1.3.4", | ||||
| 				"tinyexec": "^0.3.2", | ||||
| 				"tinyglobby": "^0.2.12", | ||||
| 				"tsconfck": "^3.1.5", | ||||
| 				"ultrahtml": "^1.6.0", | ||||
| 				"unifont": "~0.5.0", | ||||
| 				"unist-util-visit": "^5.0.0", | ||||
| 				"unstorage": "^1.15.0", | ||||
| 				"vfile": "^6.0.3", | ||||
| 				"vite": "^6.3.4", | ||||
| 				"vitefu": "^1.0.6", | ||||
| 				"xxhash-wasm": "^1.1.0", | ||||
| 				"yargs-parser": "^21.1.1", | ||||
| 				"yocto-spinner": "^0.2.1", | ||||
| 				"zod": "^3.24.2", | ||||
| 				"zod-to-json-schema": "^3.24.5", | ||||
| 				"zod-to-ts": "^1.2.0" | ||||
| 			}, | ||||
| 			"bin": { | ||||
| 				"astro": "astro.js" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| 				"@astrojs/check": "^0.9.4", | ||||
| 				"@playwright/test": "^1.51.1", | ||||
| 				"@types/aria-query": "^5.0.4", | ||||
| 				"@types/common-ancestor-path": "^1.0.2", | ||||
| 				"@types/cssesc": "^3.0.2", | ||||
| 				"@types/debug": "^4.1.12", | ||||
| 				"@types/diff": "^5.2.3", | ||||
| 				"@types/dlv": "^1.1.5", | ||||
| 				"@types/hast": "^3.0.4", | ||||
| 				"@types/html-escaper": "3.0.4", | ||||
| 				"@types/http-cache-semantics": "^4.0.4", | ||||
| 				"@types/js-yaml": "^4.0.9", | ||||
| 				"@types/picomatch": "^3.0.2", | ||||
| 				"@types/prompts": "^2.4.9", | ||||
| 				"@types/semver": "^7.7.0", | ||||
| 				"@types/yargs-parser": "^21.0.3", | ||||
| 				"astro-scripts": "0.0.14", | ||||
| 				"cheerio": "1.0.0", | ||||
| 				"eol": "^0.10.0", | ||||
| 				"execa": "^8.0.1", | ||||
| 				"expect-type": "^1.2.0", | ||||
| 				"fs-fixture": "^2.7.1", | ||||
| 				"mdast-util-mdx": "^3.0.0", | ||||
| 				"mdast-util-mdx-jsx": "^3.2.0", | ||||
| 				"node-mocks-http": "^1.16.2", | ||||
| 				"parse-srcset": "^1.0.2", | ||||
| 				"rehype-autolink-headings": "^7.1.0", | ||||
| 				"rehype-slug": "^6.0.0", | ||||
| 				"rehype-toc": "^3.0.2", | ||||
| 				"remark-code-titles": "^0.1.2", | ||||
| 				"rollup": "^4.37.0", | ||||
| 				"sass": "^1.86.0", | ||||
| 				"typescript": "^5.8.3", | ||||
| 				"undici": "^7.5.0", | ||||
| 				"unified": "^11.0.5", | ||||
| 				"vitest": "^3.0.9" | ||||
| 			}, | ||||
| 			"engines": { | ||||
| 				"node": "18.20.8 || ^20.3.0 || >=22.0.0", | ||||
| 				"npm": ">=9.6.5", | ||||
| 				"pnpm": ">=7.1.0" | ||||
| 			}, | ||||
| 			"funding": { | ||||
| 				"type": "opencollective", | ||||
| 				"url": "https://opencollective.com/astrodotbuild" | ||||
| 			}, | ||||
| 			"optionalDependencies": { | ||||
| 				"sharp": "^0.33.3" | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority": { | ||||
| 			"version": "0.7.1", | ||||
| 			"license": "Apache-2.0", | ||||
| 			"dependencies": { | ||||
| 				"clsx": "^2.1.1" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| 				"@swc/cli": "0.3.12", | ||||
| 				"@swc/core": "1.4.16", | ||||
| 				"@types/node": "20.12.7", | ||||
| 				"@types/react": "18.2.79", | ||||
| 				"@types/react-dom": "18.2.25", | ||||
| 				"bundlesize": "0.18.2", | ||||
| 				"npm-run-all": "4.1.5", | ||||
| 				"react": "18.2.0", | ||||
| 				"react-dom": "18.2.0", | ||||
| 				"ts-node": "10.9.2", | ||||
| 				"typescript": "5.4.5" | ||||
| 			}, | ||||
| 			"funding": { | ||||
| 				"url": "https://polar.sh/cva" | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx": { | ||||
| 			"version": "2.1.1", | ||||
| 			"license": "MIT", | ||||
| 			"devDependencies": { | ||||
| 				"esm": "3.2.25", | ||||
| 				"terser": "4.8.0", | ||||
| 				"uvu": "0.5.4" | ||||
| 			}, | ||||
| 			"engines": { | ||||
| 				"node": ">=6" | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/solid-js@1.9.6/node_modules/solid-js": { | ||||
| 			"version": "1.9.6", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"csstype": "^3.1.0", | ||||
| 				"seroval": "^1.1.0", | ||||
| 				"seroval-plugins": "^1.1.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/tailwind-merge@3.2.0/node_modules/tailwind-merge": { | ||||
| 			"version": "3.2.0", | ||||
| 			"license": "MIT", | ||||
| 			"devDependencies": { | ||||
| 				"@babel/core": "^7.26.10", | ||||
| 				"@babel/preset-env": "^7.26.9", | ||||
| 				"@codspeed/vitest-plugin": "^4.0.1", | ||||
| 				"@rollup/plugin-babel": "^6.0.4", | ||||
| 				"@rollup/plugin-node-resolve": "^16.0.1", | ||||
| 				"@rollup/plugin-typescript": "^12.1.2", | ||||
| 				"@vitest/coverage-v8": "^3.1.1", | ||||
| 				"@vitest/eslint-plugin": "^1.1.38", | ||||
| 				"babel-plugin-annotate-pure-calls": "^0.5.0", | ||||
| 				"babel-plugin-polyfill-regenerator": "^0.6.4", | ||||
| 				"eslint": "^9.23.0", | ||||
| 				"eslint-plugin-import": "^2.31.0", | ||||
| 				"globby": "^11.1.0", | ||||
| 				"prettier": "^3.5.3", | ||||
| 				"rollup": "^4.38.0", | ||||
| 				"rollup-plugin-delete": "^3.0.1", | ||||
| 				"rollup-plugin-dts": "^6.2.1", | ||||
| 				"tslib": "^2.8.1", | ||||
| 				"typescript": "^5.8.2", | ||||
| 				"typescript-eslint": "^8.29.0", | ||||
| 				"vitest": "^3.1.1", | ||||
| 				"zx": "^8.4.1" | ||||
| 			}, | ||||
| 			"funding": { | ||||
| 				"type": "github", | ||||
| 				"url": "https://github.com/sponsors/dcastil" | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/unocss-preset-animations@1.2.1_unocss@66.1.0-beta.13_postcss@8.5.3_vite@6.3.4_@types+node@24._pi5ihopp7lbxzhd777cycsuybi/node_modules/unocss-preset-animations": { | ||||
| 			"version": "1.2.1", | ||||
| 			"license": "MIT", | ||||
| 			"devDependencies": { | ||||
| 				"@aelita-dev/eslint-config": "3.19.0", | ||||
| 				"@iconify/json": "^2.2.330", | ||||
| 				"@types/dom-view-transitions": "^1.0.6", | ||||
| 				"@types/markdown-it": "^14.1.2", | ||||
| 				"@types/node": "^20.17.30", | ||||
| 				"@unocss/core": "^66.0.0", | ||||
| 				"@unocss/eslint-plugin": "^66.0.0", | ||||
| 				"@unocss/preset-mini": "^66.0.0", | ||||
| 				"@vitest/coverage-v8": "^3.1.2", | ||||
| 				"@vitest/eslint-plugin": "^1.1.43", | ||||
| 				"@vue/language-server": "^2.2.10", | ||||
| 				"bumpp": "^10.1.0", | ||||
| 				"bundle-require": "^5.1.0", | ||||
| 				"changelogithub": "^13.13.0", | ||||
| 				"eslint": "^9.25.1", | ||||
| 				"eslint-import-resolver-typescript": "^4.3.4", | ||||
| 				"eslint-plugin-import-x": "^4.10.6", | ||||
| 				"eslint-plugin-vue": "^10.0.0", | ||||
| 				"eslint-plugin-vuejs-accessibility": "^2.4.1", | ||||
| 				"eslint-processor-vue-blocks": "^2.0.0", | ||||
| 				"lint-staged": "^15.5.1", | ||||
| 				"markdown-it": "^14.1.0", | ||||
| 				"sass-embedded": "^1.87.0", | ||||
| 				"simple-git-hooks": "^2.12.1", | ||||
| 				"typescript": "~5.8.3", | ||||
| 				"unbuild": "3.5.0", | ||||
| 				"unocss": "^66.0.0", | ||||
| 				"vite-tsconfig-paths": "^5.1.4", | ||||
| 				"vitepress": "1.6.3", | ||||
| 				"vitest": "^3.1.2", | ||||
| 				"vue": "^3.5.13", | ||||
| 				"vue-eslint-parser": "^10.1.3", | ||||
| 				"vue-tsc": "^2.2.10" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"@unocss/preset-wind3": ">=0.56.0 < 101", | ||||
| 				"unocss": ">=0.56.0 < 101" | ||||
| 			}, | ||||
| 			"peerDependenciesMeta": { | ||||
| 				"@unocss/preset-wind3": { | ||||
| 					"optional": true | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/unocss@66.1.0-beta.13_postcss@8.5.3_vite@6.3.4_@types+node@24.0.14_jiti@2.4.2__vue@3.5.13_typescript@5.8.3_/node_modules/unocss": { | ||||
| 			"version": "66.1.0-beta.13", | ||||
| 			"dev": true, | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@unocss/astro": "66.1.0-beta.13", | ||||
| 				"@unocss/cli": "66.1.0-beta.13", | ||||
| 				"@unocss/core": "66.1.0-beta.13", | ||||
| 				"@unocss/postcss": "66.1.0-beta.13", | ||||
| 				"@unocss/preset-attributify": "66.1.0-beta.13", | ||||
| 				"@unocss/preset-icons": "66.1.0-beta.13", | ||||
| 				"@unocss/preset-mini": "66.1.0-beta.13", | ||||
| 				"@unocss/preset-tagify": "66.1.0-beta.13", | ||||
| 				"@unocss/preset-typography": "66.1.0-beta.13", | ||||
| 				"@unocss/preset-uno": "66.1.0-beta.13", | ||||
| 				"@unocss/preset-web-fonts": "66.1.0-beta.13", | ||||
| 				"@unocss/preset-wind": "66.1.0-beta.13", | ||||
| 				"@unocss/preset-wind3": "66.1.0-beta.13", | ||||
| 				"@unocss/preset-wind4": "66.1.0-beta.13", | ||||
| 				"@unocss/transformer-attributify-jsx": "66.1.0-beta.13", | ||||
| 				"@unocss/transformer-compile-class": "66.1.0-beta.13", | ||||
| 				"@unocss/transformer-directives": "66.1.0-beta.13", | ||||
| 				"@unocss/transformer-variant-group": "66.1.0-beta.13", | ||||
| 				"@unocss/vite": "66.1.0-beta.13" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| 				"@unocss/webpack": "66.1.0-beta.13", | ||||
| 				"vite": "^6.2.6" | ||||
| 			}, | ||||
| 			"engines": { | ||||
| 				"node": ">=14" | ||||
| 			}, | ||||
| 			"funding": { | ||||
| 				"url": "https://github.com/sponsors/antfu" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"@unocss/webpack": "66.1.0-beta.13", | ||||
| 				"vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" | ||||
| 			}, | ||||
| 			"peerDependenciesMeta": { | ||||
| 				"@unocss/webpack": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"vite": { | ||||
| 					"optional": true | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"../../node_modules/.pnpm/vitest@3.2.4_@types+debug@4.1.12_@types+node@24.0.14_jiti@2.4.2/node_modules/vitest": { | ||||
| 			"version": "3.2.4", | ||||
| 			"dev": true, | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@types/chai": "^5.2.2", | ||||
| 				"@vitest/expect": "3.2.4", | ||||
| 				"@vitest/mocker": "3.2.4", | ||||
| 				"@vitest/pretty-format": "^3.2.4", | ||||
| 				"@vitest/runner": "3.2.4", | ||||
| 				"@vitest/snapshot": "3.2.4", | ||||
| 				"@vitest/spy": "3.2.4", | ||||
| 				"@vitest/utils": "3.2.4", | ||||
| 				"chai": "^5.2.0", | ||||
| 				"debug": "^4.4.1", | ||||
| 				"expect-type": "^1.2.1", | ||||
| 				"magic-string": "^0.30.17", | ||||
| 				"pathe": "^2.0.3", | ||||
| 				"picomatch": "^4.0.2", | ||||
| 				"std-env": "^3.9.0", | ||||
| 				"tinybench": "^2.9.0", | ||||
| 				"tinyexec": "^0.3.2", | ||||
| 				"tinyglobby": "^0.2.14", | ||||
| 				"tinypool": "^1.1.1", | ||||
| 				"tinyrainbow": "^2.0.0", | ||||
| 				"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", | ||||
| 				"vite-node": "3.2.4", | ||||
| 				"why-is-node-running": "^2.3.0" | ||||
| 			}, | ||||
| 			"bin": { | ||||
| 				"vitest": "vitest.mjs" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| 				"@ampproject/remapping": "^2.3.0", | ||||
| 				"@antfu/install-pkg": "^1.1.0", | ||||
| 				"@edge-runtime/vm": "^5.0.0", | ||||
| 				"@sinonjs/fake-timers": "14.0.0", | ||||
| 				"@types/debug": "^4.1.12", | ||||
| 				"@types/estree": "^1.0.8", | ||||
| 				"@types/istanbul-lib-coverage": "^2.0.6", | ||||
| 				"@types/istanbul-reports": "^3.0.4", | ||||
| 				"@types/jsdom": "^21.1.7", | ||||
| 				"@types/mime": "^4.0.0", | ||||
| 				"@types/node": "^22.15.32", | ||||
| 				"@types/picomatch": "^4.0.0", | ||||
| 				"@types/prompts": "^2.4.9", | ||||
| 				"@types/sinonjs__fake-timers": "^8.1.5", | ||||
| 				"acorn-walk": "^8.3.4", | ||||
| 				"birpc": "2.4.0", | ||||
| 				"cac": "^6.7.14", | ||||
| 				"chai-subset": "^1.6.0", | ||||
| 				"find-up": "^6.3.0", | ||||
| 				"flatted": "^3.3.3", | ||||
| 				"happy-dom": "^17.6.3", | ||||
| 				"jsdom": "^26.1.0", | ||||
| 				"local-pkg": "^1.1.1", | ||||
| 				"mime": "^4.0.7", | ||||
| 				"pretty-format": "^29.7.0", | ||||
| 				"prompts": "^2.4.2", | ||||
| 				"strip-literal": "^3.0.0", | ||||
| 				"ws": "^8.18.2" | ||||
| 			}, | ||||
| 			"engines": { | ||||
| 				"node": "^18.0.0 || ^20.0.0 || >=22.0.0" | ||||
| 			}, | ||||
| 			"funding": { | ||||
| 				"url": "https://opencollective.com/vitest" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"@edge-runtime/vm": "*", | ||||
| 				"@types/debug": "^4.1.12", | ||||
| 				"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", | ||||
| 				"@vitest/browser": "3.2.4", | ||||
| 				"@vitest/ui": "3.2.4", | ||||
| 				"happy-dom": "*", | ||||
| 				"jsdom": "*" | ||||
| 			}, | ||||
| 			"peerDependenciesMeta": { | ||||
| 				"@edge-runtime/vm": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"@types/debug": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"@types/node": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"@vitest/browser": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"@vitest/ui": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"happy-dom": { | ||||
| 					"optional": true | ||||
| 				}, | ||||
| 				"jsdom": { | ||||
| 					"optional": true | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@astrojs/solid-js": { | ||||
| 			"resolved": "../../node_modules/.pnpm/@astrojs+solid-js@5.1.0_@types+node@24.0.14_jiti@2.4.2_solid-js@1.9.6/node_modules/@astrojs/solid-js", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/@corvu/utils": { | ||||
| 			"version": "0.4.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@corvu/utils/-/utils-0.4.2.tgz", | ||||
| 			"integrity": "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@floating-ui/dom": "^1.6.11" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.8" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@floating-ui/core": { | ||||
| 			"version": "1.7.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", | ||||
| 			"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@floating-ui/utils": "^0.2.10" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@floating-ui/dom": { | ||||
| 			"version": "1.7.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", | ||||
| 			"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@floating-ui/core": "^1.7.2", | ||||
| 				"@floating-ui/utils": "^0.2.10" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@floating-ui/utils": { | ||||
| 			"version": "0.2.10", | ||||
| 			"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", | ||||
| 			"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", | ||||
| 			"license": "MIT" | ||||
| 		}, | ||||
| 		"node_modules/@iconify-json/solar": { | ||||
| 			"resolved": "../../node_modules/.pnpm/@iconify-json+solar@1.2.2/node_modules/@iconify-json/solar", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/@iconify-json/tabler": { | ||||
| 			"resolved": "../../node_modules/.pnpm/@iconify-json+tabler@1.2.19/node_modules/@iconify-json/tabler", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/@internationalized/date": { | ||||
| 			"version": "3.8.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.2.tgz", | ||||
| 			"integrity": "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==", | ||||
| 			"license": "Apache-2.0", | ||||
| 			"dependencies": { | ||||
| 				"@swc/helpers": "^0.5.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@internationalized/number": { | ||||
| 			"version": "3.6.3", | ||||
| 			"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.3.tgz", | ||||
| 			"integrity": "sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==", | ||||
| 			"license": "Apache-2.0", | ||||
| 			"dependencies": { | ||||
| 				"@swc/helpers": "^0.5.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@kobalte/core": { | ||||
| 			"version": "0.13.10", | ||||
| 			"resolved": "https://registry.npmjs.org/@kobalte/core/-/core-0.13.10.tgz", | ||||
| 			"integrity": "sha512-lzP64ThxZqZB6O6MnMq6w7DxK38o2ClbW3Ob6afUI6p86cUMz5Hb4rdysvYI6m1TKYlOAlFODKkoRznqybQohw==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@floating-ui/dom": "^1.5.1", | ||||
| 				"@internationalized/date": "^3.4.0", | ||||
| 				"@internationalized/number": "^3.2.1", | ||||
| 				"@kobalte/utils": "^0.9.1", | ||||
| 				"@solid-primitives/props": "^3.1.8", | ||||
| 				"@solid-primitives/resize-observer": "^2.0.26", | ||||
| 				"solid-presence": "^0.1.8", | ||||
| 				"solid-prevent-scroll": "^0.1.4" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.8.15" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@kobalte/utils": { | ||||
| 			"version": "0.9.1", | ||||
| 			"resolved": "https://registry.npmjs.org/@kobalte/utils/-/utils-0.9.1.tgz", | ||||
| 			"integrity": "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@solid-primitives/event-listener": "^2.2.14", | ||||
| 				"@solid-primitives/keyed": "^1.2.0", | ||||
| 				"@solid-primitives/map": "^0.4.7", | ||||
| 				"@solid-primitives/media": "^2.2.4", | ||||
| 				"@solid-primitives/props": "^3.1.8", | ||||
| 				"@solid-primitives/refs": "^1.0.5", | ||||
| 				"@solid-primitives/utils": "^6.2.1" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.8.8" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/event-listener": { | ||||
| 			"version": "2.4.3", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/event-listener/-/event-listener-2.4.3.tgz", | ||||
| 			"integrity": "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@solid-primitives/utils": "^6.3.2" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/keyed": { | ||||
| 			"version": "1.5.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/keyed/-/keyed-1.5.2.tgz", | ||||
| 			"integrity": "sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg==", | ||||
| 			"license": "MIT", | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/map": { | ||||
| 			"version": "0.4.13", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/map/-/map-0.4.13.tgz", | ||||
| 			"integrity": "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@solid-primitives/trigger": "^1.1.0" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/media": { | ||||
| 			"version": "2.3.3", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/media/-/media-2.3.3.tgz", | ||||
| 			"integrity": "sha512-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@solid-primitives/event-listener": "^2.4.3", | ||||
| 				"@solid-primitives/rootless": "^1.5.2", | ||||
| 				"@solid-primitives/static-store": "^0.1.2", | ||||
| 				"@solid-primitives/utils": "^6.3.2" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/props": { | ||||
| 			"version": "3.2.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/props/-/props-3.2.2.tgz", | ||||
| 			"integrity": "sha512-lZOTwFJajBrshSyg14nBMEP0h8MXzPowGO0s3OeiR3z6nXHTfj0FhzDtJMv+VYoRJKQHG2QRnJTgCzK6erARAw==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@solid-primitives/utils": "^6.3.2" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/refs": { | ||||
| 			"version": "1.1.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/refs/-/refs-1.1.2.tgz", | ||||
| 			"integrity": "sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@solid-primitives/utils": "^6.3.2" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/resize-observer": { | ||||
| 			"version": "2.1.3", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/resize-observer/-/resize-observer-2.1.3.tgz", | ||||
| 			"integrity": "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@solid-primitives/event-listener": "^2.4.3", | ||||
| 				"@solid-primitives/rootless": "^1.5.2", | ||||
| 				"@solid-primitives/static-store": "^0.1.2", | ||||
| 				"@solid-primitives/utils": "^6.3.2" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/rootless": { | ||||
| 			"version": "1.5.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/rootless/-/rootless-1.5.2.tgz", | ||||
| 			"integrity": "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@solid-primitives/utils": "^6.3.2" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/static-store": { | ||||
| 			"version": "0.1.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/static-store/-/static-store-0.1.2.tgz", | ||||
| 			"integrity": "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@solid-primitives/utils": "^6.3.2" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/trigger": { | ||||
| 			"version": "1.2.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/trigger/-/trigger-1.2.2.tgz", | ||||
| 			"integrity": "sha512-IWoptVc0SWYgmpBPpCMehS5b07+tpFcvw15tOQ3QbXedSYn6KP8zCjPkHNzMxcOvOicTneleeZDP7lqmz+PQ6g==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@solid-primitives/utils": "^6.3.2" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@solid-primitives/utils": { | ||||
| 			"version": "6.3.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.3.2.tgz", | ||||
| 			"integrity": "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==", | ||||
| 			"license": "MIT", | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.6.12" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@swc/helpers": { | ||||
| 			"version": "0.5.17", | ||||
| 			"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", | ||||
| 			"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", | ||||
| 			"license": "Apache-2.0", | ||||
| 			"dependencies": { | ||||
| 				"tslib": "^2.8.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/@unocss/reset": { | ||||
| 			"resolved": "../../node_modules/.pnpm/@unocss+reset@66.3.3/node_modules/@unocss/reset", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/astro": { | ||||
| 			"resolved": "../../node_modules/.pnpm/astro@5.12.0_@types+node@24.0.14_jiti@2.4.2_rollup@4.40.1_typescript@5.8.3/node_modules/astro", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/astro-pagefind": { | ||||
| 			"resolved": "../../node_modules/.pnpm/astro-pagefind@1.8.3_astro@5.12.0_@types+node@24.0.14_jiti@2.4.2_rollup@4.40.1_typescript@5.8.3_/node_modules/astro-pagefind", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/class-variance-authority": { | ||||
| 			"resolved": "../../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/clsx": { | ||||
| 			"resolved": "../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/solid-js": { | ||||
| 			"resolved": "../../node_modules/.pnpm/solid-js@1.9.6/node_modules/solid-js", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/solid-presence": { | ||||
| 			"version": "0.1.8", | ||||
| 			"resolved": "https://registry.npmjs.org/solid-presence/-/solid-presence-0.1.8.tgz", | ||||
| 			"integrity": "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@corvu/utils": "~0.4.0" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.8" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/solid-prevent-scroll": { | ||||
| 			"version": "0.1.10", | ||||
| 			"resolved": "https://registry.npmjs.org/solid-prevent-scroll/-/solid-prevent-scroll-0.1.10.tgz", | ||||
| 			"integrity": "sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw==", | ||||
| 			"license": "MIT", | ||||
| 			"dependencies": { | ||||
| 				"@corvu/utils": "~0.4.1" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"solid-js": "^1.8" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/tailwind-merge": { | ||||
| 			"resolved": "../../node_modules/.pnpm/tailwind-merge@3.2.0/node_modules/tailwind-merge", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/tslib": { | ||||
| 			"version": "2.8.1", | ||||
| 			"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", | ||||
| 			"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", | ||||
| 			"license": "0BSD" | ||||
| 		}, | ||||
| 		"node_modules/unocss": { | ||||
| 			"resolved": "../../node_modules/.pnpm/unocss@66.1.0-beta.13_postcss@8.5.3_vite@6.3.4_@types+node@24.0.14_jiti@2.4.2__vue@3.5.13_typescript@5.8.3_/node_modules/unocss", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/unocss-preset-animations": { | ||||
| 			"resolved": "../../node_modules/.pnpm/unocss-preset-animations@1.2.1_unocss@66.1.0-beta.13_postcss@8.5.3_vite@6.3.4_@types+node@24._pi5ihopp7lbxzhd777cycsuybi/node_modules/unocss-preset-animations", | ||||
| 			"link": true | ||||
| 		}, | ||||
| 		"node_modules/vitest": { | ||||
| 			"resolved": "../../node_modules/.pnpm/vitest@3.2.4_@types+debug@4.1.12_@types+node@24.0.14_jiti@2.4.2/node_modules/vitest", | ||||
| 			"link": true | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										31
									
								
								apps/it-tools/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/it-tools/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| { | ||||
| 	"name": "@it-tools/app", | ||||
| 	"type": "module", | ||||
| 	"version": "0.0.1", | ||||
| 	"scripts": { | ||||
| 		"dev": "astro dev", | ||||
| 		"build": "astro build", | ||||
| 		"preview": "astro preview", | ||||
| 		"astro": "astro", | ||||
| 		"test": "vitest run", | ||||
| 		"test:watch": "vitest watch" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@astrojs/solid-js": "^5.1.0", | ||||
| 		"@kobalte/core": "^0.13.10", | ||||
| 		"astro": "^5.12.0", | ||||
| 		"astro-pagefind": "^1.8.3", | ||||
| 		"class-variance-authority": "^0.7.1", | ||||
| 		"clsx": "^2.1.1", | ||||
| 		"solid-js": "^1.9.6", | ||||
| 		"tailwind-merge": "^3.2.0", | ||||
| 		"unocss-preset-animations": "^1.2.1" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@iconify-json/solar": "^1.2.2", | ||||
| 		"@iconify-json/tabler": "^1.2.19", | ||||
| 		"@unocss/reset": "^66.3.3", | ||||
| 		"unocss": "66.1.0-beta.13", | ||||
| 		"vitest": "^3.2.4" | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								apps/it-tools/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								apps/it-tools/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										9
									
								
								apps/it-tools/public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								apps/it-tools/public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128"> | ||||
|     <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" /> | ||||
|     <style> | ||||
|         path { fill: #000; } | ||||
|         @media (prefers-color-scheme: dark) { | ||||
|             path { fill: #FFF; } | ||||
|         } | ||||
|     </style> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 749 B | 
							
								
								
									
										75
									
								
								apps/it-tools/src/assets/app.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								apps/it-tools/src/assets/app.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | ||||
| :root { | ||||
|   --background: 0 0% 100%; | ||||
|   --foreground: 0 0% 3.9%; | ||||
| 
 | ||||
|   --card: 0 0% 100%; | ||||
|   --card-foreground: 0 0% 3.9%; | ||||
| 
 | ||||
|   --popover: 0 0% 100%; | ||||
|   --popover-foreground: 0 0% 3.9%; | ||||
| 
 | ||||
|   --primary: 0 0% 9%; | ||||
|   --primary-foreground: 0 0% 98%; | ||||
| 
 | ||||
|   --secondary: 0 0% 96.1%; | ||||
|   --secondary-foreground: 0 0% 9%; | ||||
| 
 | ||||
|   --muted: 0 0% 96.1%; | ||||
|   --muted-foreground: 0 0% 45.1%; | ||||
| 
 | ||||
|   --accent: 0 0% 96.1%; | ||||
|   --accent-foreground: 0 0% 9%; | ||||
| 
 | ||||
|   --destructive: 0 84.2% 60.2%; | ||||
|   --destructive-foreground: 0 0% 98%; | ||||
| 
 | ||||
|   --border: 0 0% 89.8%; | ||||
|   --input: 0 0% 89.8%; | ||||
|   --ring: 0 0% 3.9%; | ||||
| 
 | ||||
|   --radius: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| [data-kb-theme="dark"] { | ||||
|   --background: 0 0% 6%; | ||||
|   --foreground: 0 0% 96%; | ||||
| 
 | ||||
|   --card: 0 0% 3.9%; | ||||
|   --card-foreground: 0 0% 98%; | ||||
| 
 | ||||
|   --popover: 0 0% 3.9%; | ||||
|   --popover-foreground: 0 0% 98%; | ||||
| 
 | ||||
|   --primary: 0 0% 96%; | ||||
|   --primary-foreground: 0 0% 9%; | ||||
| 
 | ||||
|   --secondary: 0 0% 14.9%; | ||||
|   --secondary-foreground: 0 0% 98%; | ||||
| 
 | ||||
|   --muted: 0 0% 14.9%; | ||||
|   --muted-foreground: 0 0% 63.9%; | ||||
| 
 | ||||
|   --accent: 0 0% 14.9%; | ||||
|   --accent-foreground: 0 0% 98%; | ||||
| 
 | ||||
|   --destructive: 0 62.8% 30.6%; | ||||
|   --destructive-foreground: 0 0% 98%; | ||||
| 
 | ||||
|   --border: 0 0% 14.9%; | ||||
|   --input: 0 0% 14.9%; | ||||
|   --ring: 0 0% 83.1%; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| * { | ||||
|   @apply border-border; | ||||
| } | ||||
| body { | ||||
|   @apply bg-background text-foreground; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| [data-kb-theme="dark"] .astro-code, | ||||
| [data-kb-theme="dark"] .astro-code span { | ||||
|   background-color: var(--shiki-dark-bg) !important; | ||||
| } | ||||
							
								
								
									
										103
									
								
								apps/it-tools/src/components/footer.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								apps/it-tools/src/components/footer.astro
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,103 @@ | ||||
| --- | ||||
| import { cn } from '@/libs/cn'; | ||||
| 
 | ||||
| const socials = [ | ||||
|   { | ||||
|     label: 'Bluesky', | ||||
|     url: 'https://bsky.app/profile/it-tools.tech', | ||||
|     icon: 'i-tabler-brand-bluesky', | ||||
|   }, | ||||
|   { | ||||
|     label: 'GitHub', | ||||
|     url: 'https://github.com/CorentinTh/it-tools', | ||||
|     icon: 'i-tabler-brand-github', | ||||
|   }, | ||||
|   { | ||||
|     label: 'X (Twitter)', | ||||
|     url: 'https://x.com/ittoolsdottech', | ||||
|     icon: 'i-tabler-brand-x', | ||||
|   }, | ||||
| 
 | ||||
|   | ||||
| ]; | ||||
| 
 | ||||
| const sections: { | ||||
|   title: string; | ||||
|   links: { label: string; url: string; target?: string; rel?: string }[]; | ||||
| }[] = [ | ||||
|   { | ||||
|     title: 'Community', | ||||
|     links: socials, | ||||
|   }, | ||||
|   { | ||||
|     title: 'Open Source', | ||||
|     links: [ | ||||
|       { | ||||
|         label: 'Repository', | ||||
|         url: 'https://github.com/CorentinTh/it-tools', | ||||
|       }, | ||||
|       { | ||||
|         label: 'Contributing', | ||||
|         url: 'https://github.com/CorentinTh/it-tools/blob/main/CONTRIBUTING.md', | ||||
|       }, | ||||
|       { | ||||
|         label: 'Code of Conduct', | ||||
|         url: 'https://github.com/CorentinTh/it-tools/blob/main/CODE_OF_CONDUCT.md', | ||||
|       }, | ||||
|       { | ||||
|         label: 'License', | ||||
|         url: 'https://github.com/CorentinTh/it-tools/blob/main/LICENSE', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
| --- | ||||
| 
 | ||||
| <footer class="bg-card border-t border-border py-8 text-muted-foreground light:bg-muted"> | ||||
|   <div class="max-w-screen-lg mx-auto p-6"> | ||||
|     <div class="flex justify-between flex-col md:flex-row gap-10"> | ||||
|       <div> | ||||
|         <a href="/" class="text-xl inline-flex items-center group mb-2"> | ||||
|           <div class="i-solar-programming-line-duotone size-7 text-primary group-hover:(rotate-12deg) transition transform"></div> | ||||
|           <span class="ml-2 text-foreground group-hover:text-foreground/80 transition">IT-Tools</span> | ||||
|         </a> | ||||
| 
 | ||||
|         <div class="flex gap-2"> | ||||
|           { | ||||
|             socials.map(social => ( | ||||
|               <a href={social.url} class="hover:text-primary transition" target="_blank" rel="noopener noreferrer" aria-label={social.label}> | ||||
|                 <div class={`${social.icon} text-2xl`} aria-hidden="true" /> | ||||
|               </a> | ||||
|             )) | ||||
|           } | ||||
|         </div> | ||||
| 
 | ||||
|         <p class="mt-4 text-sm max-w-420px"> | ||||
|           IT-Tools is made in Europe with <span class="i-tabler-heart-filled size-3.5 mb--0.3 text-primary inline-block"></span> | ||||
|           by <a href="https://corentin.tech" class="text-primary border-b hover:border-b-primary transition">Corentin Thomasset</a>. | ||||
|         </p> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class={cn('grid gap-10 grid-cols-1', `sm:grid-cols-${sections.length}`)}> | ||||
|         { | ||||
|           sections.map(section => ( | ||||
|             <div> | ||||
|               <div class="text-foreground font-semibold">{section.title}</div> | ||||
|               <div class="mt-2"> | ||||
|                 {section.links.map(link => ( | ||||
|                   <a href={link.url} class="block hover:text-primary transition py-0.75 font-medium" target={link.target} rel={link.rel}> | ||||
|                     {link.label} | ||||
|                   </a> | ||||
|                 ))} | ||||
|               </div> | ||||
|             </div> | ||||
|           )) | ||||
|         } | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="mt-8 border-t border-border pt-4"> | ||||
|       © {new Date().getFullYear()} IT-Tools. All rights reserved. | ||||
|     </div> | ||||
|   </div> | ||||
| </footer> | ||||
							
								
								
									
										60
									
								
								apps/it-tools/src/components/header.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								apps/it-tools/src/components/header.astro
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| --- | ||||
| import { LanguagePicker } from "./language-picker"; | ||||
| import { Button } from "./ui/button"; | ||||
| 
 | ||||
| const { lang } = Astro.params; | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| <header class="fixed top-0 w-full z-50 bg-background/50 backdrop-blur-sm"> | ||||
|     <div class="max-w-screen-lg mx-auto px-6 py-3 flex items-center justify-between gap-4"> | ||||
|     <div class="flex items-center gap-3"> | ||||
|         <a href={lang ? `/${lang}` : "/"} class="flex items-center gap-2 group"> | ||||
|             <span class="i-solar-programming-line-duotone flex-shrink-0 size-6 group-hover:rotate-12 transition-transform"></span> | ||||
|             <span class="font-semibold">IT-Tools</span> | ||||
|         </a> | ||||
| 
 | ||||
|         <span class="text-sm text-muted-foreground font-medium hidden sm:block"> | ||||
|             Handy online tools. | ||||
|         </span> | ||||
|     </div> | ||||
| 
 | ||||
|         <div class="flex items-center gap-1"> | ||||
|             <Button variant="ghost" size="icon" class="size-8"> | ||||
|                 <span class="i-tabler-search flex-shrink-0 size-4"></span> | ||||
|             </Button> | ||||
| 
 | ||||
|             <Button variant="ghost" size="icon" class="size-8 toggle-theme"> | ||||
|                 <span class="i-tabler-moon flex-shrink-0 hidden dark:block size-4"></span> | ||||
|                 <span class="i-tabler-sun flex-shrink-0 block dark:hidden size-4"></span> | ||||
|             </Button> | ||||
| 
 | ||||
|             <LanguagePicker client:load /> | ||||
| 
 | ||||
|             <Button variant="ghost" size="icon" class="size-8" as="a" href="https://github.com/CorentinTh/it-tools" target="_blank" rel="noopener noreferrer"> | ||||
|                 <span class="i-tabler-brand-github flex-shrink-0 size-4"></span> | ||||
|             </Button> | ||||
| 
 | ||||
|             <Button variant="outline" size="sm" as="a" href="https://www.buymeacoffee.com/cthmsst" target="_blank" rel="noopener noreferrer"> | ||||
|                 Buy me a coffee | ||||
|             </Button> | ||||
|         </div> | ||||
|     </div> | ||||
| </header> | ||||
| 
 | ||||
| 
 | ||||
| <script> | ||||
|     const themeButtons = document.querySelectorAll("button.toggle-theme"); | ||||
| 
 | ||||
|     themeButtons.forEach(button => { | ||||
|         button.addEventListener("click", () => { | ||||
|             const localStorageTheme = localStorage.getItem("it-tools-theme"); | ||||
|             const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; | ||||
|             const theme = localStorageTheme ?? systemTheme; | ||||
|             const newTheme = theme === "dark" ? "light" : "dark"; | ||||
| 
 | ||||
|             localStorage.setItem("it-tools-theme", newTheme); | ||||
|             document.documentElement.setAttribute("data-kb-theme", newTheme); | ||||
|         }); | ||||
|     }); | ||||
| </script> | ||||
							
								
								
									
										30
									
								
								apps/it-tools/src/components/language-picker.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/it-tools/src/components/language-picker.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; | ||||
| import type { DropdownMenuSubTriggerProps } from "@kobalte/core/dropdown-menu"; | ||||
| import { languages } from "@/i18n/languages"; | ||||
| import { navigate } from "astro:transitions/client"; | ||||
| 
 | ||||
| export const LanguagePicker = () => { | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|     return ( | ||||
|         <DropdownMenu placement="bottom"> | ||||
|         <DropdownMenuTrigger | ||||
|                     as={(props: DropdownMenuSubTriggerProps) => ( | ||||
|                         <Button variant="ghost" size="icon" {...props}> | ||||
|                             <span class="i-tabler-language flex-shrink-0 size-4"></span> | ||||
|                         </Button> | ||||
|                     )} | ||||
|                 /> | ||||
|         <DropdownMenuContent class="w-42"> | ||||
|             {Object.entries(languages).map(([locale, name]) => ( | ||||
|                 <DropdownMenuItem as="a" href={`/${locale}`}> | ||||
|                     {name} | ||||
|                 </DropdownMenuItem> | ||||
|             ))} | ||||
|         </DropdownMenuContent> | ||||
|     </DropdownMenu> | ||||
|             | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										24
									
								
								apps/it-tools/src/components/tool-card.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								apps/it-tools/src/components/tool-card.astro
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| --- | ||||
| import { buildLocalizedUrl } from "@/i18n/i18n.models"; | ||||
| import { cn } from "@/libs/cn"; | ||||
| 
 | ||||
| export type Props = { | ||||
|     name: string; | ||||
|     description: string; | ||||
|     icon: string; | ||||
|     slug: string; | ||||
| }; | ||||
| 
 | ||||
| const { slug, name, description, icon }: Props = Astro.props; | ||||
| const {lang} = Astro.params; | ||||
| --- | ||||
| 
 | ||||
| <a href={buildLocalizedUrl({lang, path: slug})} class="border rounded-lg p-5 hover:bg-muted transition-colors flex flex-col gap-2"> | ||||
|         <div class="flex items-center gap-4"> | ||||
|             <span class="size-10 flex-shrink-0 bg-muted rounded-lg flex items-center justify-center"> | ||||
|                 <span class={cn(icon, "size-6 text-muted-foreground")}></span> | ||||
|             </span> | ||||
|             <h3 class="text-lg font-semibold truncate">{name}</h3> | ||||
|         </div> | ||||
|         <p class="text-sm text-muted-foreground">{description}</p> | ||||
| </a> | ||||
							
								
								
									
										66
									
								
								apps/it-tools/src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								apps/it-tools/src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| import { cn } from "@/libs/cn"; | ||||
| import type { ButtonRootProps } from "@kobalte/core/button"; | ||||
| import { Button as ButtonPrimitive } from "@kobalte/core/button"; | ||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; | ||||
| import type { VariantProps } from "class-variance-authority"; | ||||
| import { cva } from "class-variance-authority"; | ||||
| import type { ValidComponent } from "solid-js"; | ||||
| import { splitProps } from "solid-js"; | ||||
| 
 | ||||
| export const buttonVariants = cva( | ||||
| 	"inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:(outline-none ring-1.5 ring-ring) disabled:(pointer-events-none opacity-50) bg-inherit", | ||||
| 	{ | ||||
| 		variants: { | ||||
| 			variant: { | ||||
| 				default: | ||||
| 					"bg-primary text-primary-foreground shadow hover:bg-primary/90", | ||||
| 				destructive: | ||||
| 					"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", | ||||
| 				outline: | ||||
| 					"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", | ||||
| 				secondary: | ||||
| 					"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", | ||||
| 				ghost: "hover:(bg-accent text-accent-foreground)", | ||||
| 				link: "text-primary underline-offset-4 hover:underline", | ||||
| 			}, | ||||
| 			size: { | ||||
| 				default: "h-9 px-4 py-2", | ||||
| 				sm: "h-8 px-3 text-xs", | ||||
| 				lg: "h-10 px-8", | ||||
| 				icon: "h-9 w-9", | ||||
| 			}, | ||||
| 		}, | ||||
| 		defaultVariants: { | ||||
| 			variant: "default", | ||||
| 			size: "default", | ||||
| 		}, | ||||
| 	}, | ||||
| ); | ||||
| 
 | ||||
| type buttonProps<T extends ValidComponent = "button"> = ButtonRootProps<T> & | ||||
| 	VariantProps<typeof buttonVariants> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const Button = <T extends ValidComponent = "button">( | ||||
| 	props: PolymorphicProps<T, buttonProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as buttonProps, [ | ||||
| 		"class", | ||||
| 		"variant", | ||||
| 		"size", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<ButtonPrimitive | ||||
| 			class={cn( | ||||
| 				buttonVariants({ | ||||
| 					size: local.size, | ||||
| 					variant: local.variant, | ||||
| 				}), | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
							
								
								
									
										320
									
								
								apps/it-tools/src/components/ui/dropdown-menu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								apps/it-tools/src/components/ui/dropdown-menu.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,320 @@ | ||||
| import { cn } from "@/libs/cn"; | ||||
| import type { | ||||
| 	DropdownMenuCheckboxItemProps, | ||||
| 	DropdownMenuContentProps, | ||||
| 	DropdownMenuGroupLabelProps, | ||||
| 	DropdownMenuItemLabelProps, | ||||
| 	DropdownMenuItemProps, | ||||
| 	DropdownMenuRadioItemProps, | ||||
| 	DropdownMenuRootProps, | ||||
| 	DropdownMenuSeparatorProps, | ||||
| 	DropdownMenuSubTriggerProps, | ||||
| } from "@kobalte/core/dropdown-menu"; | ||||
| import { DropdownMenu as DropdownMenuPrimitive } from "@kobalte/core/dropdown-menu"; | ||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; | ||||
| import type { ComponentProps, ParentProps, ValidComponent } from "solid-js"; | ||||
| import { mergeProps, splitProps } from "solid-js"; | ||||
| 
 | ||||
| export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; | ||||
| export const DropdownMenuGroup = DropdownMenuPrimitive.Group; | ||||
| export const DropdownMenuSub = DropdownMenuPrimitive.Sub; | ||||
| export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; | ||||
| 
 | ||||
| export const DropdownMenu = (props: DropdownMenuRootProps) => { | ||||
| 	const merge = mergeProps<DropdownMenuRootProps[]>( | ||||
| 		{ | ||||
| 			gutter: 4, | ||||
| 			flip: false, | ||||
| 		}, | ||||
| 		props, | ||||
| 	); | ||||
| 
 | ||||
| 	return <DropdownMenuPrimitive {...merge} />; | ||||
| }; | ||||
| 
 | ||||
| type dropdownMenuContentProps<T extends ValidComponent = "div"> = | ||||
| 	DropdownMenuContentProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const DropdownMenuContent = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuContentProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as dropdownMenuContentProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<DropdownMenuPrimitive.Portal> | ||||
| 			<DropdownMenuPrimitive.Content | ||||
| 				class={cn( | ||||
| 					"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95) focus-visible:(outline-none ring-1.5 ring-ring) transition-shadow", | ||||
| 					local.class, | ||||
| 				)} | ||||
| 				{...rest} | ||||
| 			/> | ||||
| 		</DropdownMenuPrimitive.Portal> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type dropdownMenuItemProps<T extends ValidComponent = "div"> = | ||||
| 	DropdownMenuItemProps<T> & { | ||||
| 		class?: string; | ||||
| 		inset?: boolean; | ||||
| 	}; | ||||
| 
 | ||||
| export const DropdownMenuItem = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuItemProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as dropdownMenuItemProps, [ | ||||
| 		"class", | ||||
| 		"inset", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<DropdownMenuPrimitive.Item | ||||
| 			class={cn( | ||||
| 				"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)", | ||||
| 				local.inset && "pl-8", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type dropdownMenuGroupLabelProps<T extends ValidComponent = "span"> = | ||||
| 	DropdownMenuGroupLabelProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const DropdownMenuGroupLabel = <T extends ValidComponent = "span">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<DropdownMenuPrimitive.GroupLabel | ||||
| 			as="div" | ||||
| 			class={cn("px-2 py-1.5 text-sm font-semibold", local.class)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type dropdownMenuItemLabelProps<T extends ValidComponent = "div"> = | ||||
| 	DropdownMenuItemLabelProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const DropdownMenuItemLabel = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<DropdownMenuPrimitive.ItemLabel | ||||
| 			as="div" | ||||
| 			class={cn("px-2 py-1.5 text-sm font-semibold", local.class)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type dropdownMenuSeparatorProps<T extends ValidComponent = "hr"> = | ||||
| 	DropdownMenuSeparatorProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const DropdownMenuSeparator = <T extends ValidComponent = "hr">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<DropdownMenuPrimitive.Separator | ||||
| 			class={cn("-mx-1 my-1 h-px bg-muted", local.class)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| export const DropdownMenuShortcut = (props: ComponentProps<"span">) => { | ||||
| 	const [local, rest] = splitProps(props, ["class"]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<span | ||||
| 			class={cn("ml-auto text-xs tracking-widest opacity-60", local.class)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type dropdownMenuSubTriggerProps<T extends ValidComponent = "div"> = | ||||
| 	ParentProps< | ||||
| 		DropdownMenuSubTriggerProps<T> & { | ||||
| 			class?: string; | ||||
| 		} | ||||
| 	>; | ||||
| 
 | ||||
| export const DropdownMenuSubTrigger = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [ | ||||
| 		"class", | ||||
| 		"children", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<DropdownMenuPrimitive.SubTrigger | ||||
| 			class={cn( | ||||
| 				"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[expanded]:bg-accent", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		> | ||||
| 			{local.children} | ||||
| 			<svg | ||||
| 				xmlns="http://www.w3.org/2000/svg" | ||||
| 				width="1em" | ||||
| 				height="1em" | ||||
| 				viewBox="0 0 24 24" | ||||
| 				class="ml-auto h-4 w-4" | ||||
| 			> | ||||
| 				<path | ||||
| 					fill="none" | ||||
| 					stroke="currentColor" | ||||
| 					stroke-linecap="round" | ||||
| 					stroke-linejoin="round" | ||||
| 					stroke-width="2" | ||||
| 					d="m9 6l6 6l-6 6" | ||||
| 				/> | ||||
| 				<title>Arrow</title> | ||||
| 			</svg> | ||||
| 		</DropdownMenuPrimitive.SubTrigger> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type dropdownMenuSubContentProps<T extends ValidComponent = "div"> = | ||||
| 	DropdownMenuSubTriggerProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const DropdownMenuSubContent = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<DropdownMenuPrimitive.Portal> | ||||
| 			<DropdownMenuPrimitive.SubContent | ||||
| 				class={cn( | ||||
| 					"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95)", | ||||
| 					local.class, | ||||
| 				)} | ||||
| 				{...rest} | ||||
| 			/> | ||||
| 		</DropdownMenuPrimitive.Portal> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type dropdownMenuCheckboxItemProps<T extends ValidComponent = "div"> = | ||||
| 	ParentProps< | ||||
| 		DropdownMenuCheckboxItemProps<T> & { | ||||
| 			class?: string; | ||||
| 		} | ||||
| 	>; | ||||
| 
 | ||||
| export const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [ | ||||
| 		"class", | ||||
| 		"children", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<DropdownMenuPrimitive.CheckboxItem | ||||
| 			class={cn( | ||||
| 				"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		> | ||||
| 			<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center"> | ||||
| 				<svg | ||||
| 					xmlns="http://www.w3.org/2000/svg" | ||||
| 					viewBox="0 0 24 24" | ||||
| 					class="h-4 w-4" | ||||
| 				> | ||||
| 					<path | ||||
| 						fill="none" | ||||
| 						stroke="currentColor" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 						stroke-width="2" | ||||
| 						d="m5 12l5 5L20 7" | ||||
| 					/> | ||||
| 					<title>Checkbox</title> | ||||
| 				</svg> | ||||
| 			</DropdownMenuPrimitive.ItemIndicator> | ||||
| 			{props.children} | ||||
| 		</DropdownMenuPrimitive.CheckboxItem> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type dropdownMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps< | ||||
| 	DropdownMenuRadioItemProps<T> & { | ||||
| 		class?: string; | ||||
| 	} | ||||
| >; | ||||
| 
 | ||||
| export const DropdownMenuRadioItem = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [ | ||||
| 		"class", | ||||
| 		"children", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<DropdownMenuPrimitive.RadioItem | ||||
| 			class={cn( | ||||
| 				"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		> | ||||
| 			<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center"> | ||||
| 				<svg | ||||
| 					xmlns="http://www.w3.org/2000/svg" | ||||
| 					viewBox="0 0 24 24" | ||||
| 					class="h-2 w-2" | ||||
| 				> | ||||
| 					<g | ||||
| 						fill="none" | ||||
| 						stroke-linecap="round" | ||||
| 						stroke-linejoin="round" | ||||
| 						stroke-width="2" | ||||
| 					> | ||||
| 						<path d="M0 0h24v24H0z" /> | ||||
| 						<path | ||||
| 							fill="currentColor" | ||||
| 							d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34" | ||||
| 						/> | ||||
| 					</g> | ||||
| 					<title>Radio</title> | ||||
| 				</svg> | ||||
| 			</DropdownMenuPrimitive.ItemIndicator> | ||||
| 			{props.children} | ||||
| 		</DropdownMenuPrimitive.RadioItem> | ||||
| 	); | ||||
| }; | ||||
							
								
								
									
										19
									
								
								apps/it-tools/src/components/ui/label.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								apps/it-tools/src/components/ui/label.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| import type { Component, ComponentProps } from "solid-js" | ||||
| import { splitProps } from "solid-js" | ||||
|   | ||||
| import { cn } from "@/libs/cn" | ||||
|   | ||||
| const Label: Component<ComponentProps<"label">> = (props) => { | ||||
|   const [local, others] = splitProps(props, ["class"]) | ||||
|   return ( | ||||
|     <label | ||||
|       class={cn( | ||||
|         "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", | ||||
|         local.class | ||||
|       )} | ||||
|       {...others} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| export { Label } | ||||
							
								
								
									
										214
									
								
								apps/it-tools/src/components/ui/number-field.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								apps/it-tools/src/components/ui/number-field.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,214 @@ | ||||
| import { cn } from "@/libs/cn"; | ||||
| import type { | ||||
| 	NumberFieldDecrementTriggerProps, | ||||
| 	NumberFieldDescriptionProps, | ||||
| 	NumberFieldErrorMessageProps, | ||||
| 	NumberFieldIncrementTriggerProps, | ||||
| 	NumberFieldInputProps, | ||||
| 	NumberFieldLabelProps, | ||||
| 	NumberFieldRootProps, | ||||
| } from "@kobalte/core/number-field"; | ||||
| import { NumberField as NumberFieldPrimitive } from "@kobalte/core/number-field"; | ||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; | ||||
| import type { ComponentProps, ValidComponent, VoidProps } from "solid-js"; | ||||
| import { splitProps } from "solid-js"; | ||||
| import { textfieldLabel } from "./textfield"; | ||||
| 
 | ||||
| export const NumberFieldHiddenInput = NumberFieldPrimitive.HiddenInput; | ||||
| 
 | ||||
| type numberFieldLabelProps<T extends ValidComponent = "div"> = | ||||
| 	NumberFieldLabelProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const NumberFieldLabel = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, numberFieldLabelProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as numberFieldLabelProps, ["class"]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<NumberFieldPrimitive.Label | ||||
| 			class={cn(textfieldLabel({ label: true }), local.class)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type numberFieldDescriptionProps<T extends ValidComponent = "div"> = | ||||
| 	NumberFieldDescriptionProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const NumberFieldDescription = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, numberFieldDescriptionProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as numberFieldDescriptionProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<NumberFieldPrimitive.Description | ||||
| 			class={cn( | ||||
| 				textfieldLabel({ description: true, label: false }), | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type numberFieldErrorMessageProps<T extends ValidComponent = "div"> = | ||||
| 	NumberFieldErrorMessageProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const NumberFieldErrorMessage = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, numberFieldErrorMessageProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as numberFieldErrorMessageProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<NumberFieldPrimitive.ErrorMessage | ||||
| 			class={cn(textfieldLabel({ error: true }), local.class)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type numberFieldProps<T extends ValidComponent = "div"> = | ||||
| 	NumberFieldRootProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const NumberField = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, numberFieldProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as numberFieldProps, ["class"]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<NumberFieldPrimitive class={cn("grid gap-1.5", local.class)} {...rest} /> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| export const NumberFieldGroup = (props: ComponentProps<"div">) => { | ||||
| 	const [local, rest] = splitProps(props, ["class"]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<div | ||||
| 			class={cn( | ||||
| 				"relative focus-within:(outline-none ring-1.5 ring-ring) transition-shadow rounded-md", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type numberFieldInputProps<T extends ValidComponent = "input"> = | ||||
| 	NumberFieldInputProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const NumberFieldInput = <T extends ValidComponent = "input">( | ||||
| 	props: PolymorphicProps<T, VoidProps<numberFieldInputProps<T>>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as numberFieldInputProps, ["class"]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<NumberFieldPrimitive.Input | ||||
| 			class={cn( | ||||
| 				"flex h-9 w-full rounded-md border border-input bg-transparent px-10 py-1 text-sm text-center shadow-sm placeholder:text-muted-foreground disabled:(cursor-not-allowed opacity-50) focus-visible:outline-none", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type numberFieldDecrementTriggerProps<T extends ValidComponent = "button"> = | ||||
| 	VoidProps< | ||||
| 		NumberFieldDecrementTriggerProps<T> & { | ||||
| 			class?: string; | ||||
| 		} | ||||
| 	>; | ||||
| 
 | ||||
| export const NumberFieldDecrementTrigger = < | ||||
| 	T extends ValidComponent = "button", | ||||
| >( | ||||
| 	props: PolymorphicProps<T, VoidProps<numberFieldDecrementTriggerProps<T>>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as numberFieldDecrementTriggerProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<NumberFieldPrimitive.DecrementTrigger | ||||
| 			class={cn( | ||||
| 				"absolute top-1/2 -translate-y-1/2 left-0 p-3 disabled:(cursor-not-allowed opacity-20)", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		> | ||||
| 			<svg | ||||
| 				xmlns="http://www.w3.org/2000/svg" | ||||
| 				class="size-4" | ||||
| 				viewBox="0 0 24 24" | ||||
| 			> | ||||
| 				<path | ||||
| 					fill="none" | ||||
| 					stroke="currentColor" | ||||
| 					stroke-linecap="round" | ||||
| 					stroke-linejoin="round" | ||||
| 					stroke-width="2" | ||||
| 					d="M5 12h14" | ||||
| 				/> | ||||
| 				<title>Decrease number</title> | ||||
| 			</svg> | ||||
| 		</NumberFieldPrimitive.DecrementTrigger> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type numberFieldIncrementTriggerProps<T extends ValidComponent = "button"> = | ||||
| 	VoidProps< | ||||
| 		NumberFieldIncrementTriggerProps<T> & { | ||||
| 			class?: string; | ||||
| 		} | ||||
| 	>; | ||||
| 
 | ||||
| export const NumberFieldIncrementTrigger = < | ||||
| 	T extends ValidComponent = "button", | ||||
| >( | ||||
| 	props: PolymorphicProps<T, numberFieldIncrementTriggerProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as numberFieldIncrementTriggerProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<NumberFieldPrimitive.IncrementTrigger | ||||
| 			class={cn( | ||||
| 				"absolute top-1/2 -translate-y-1/2 right-0 disabled:(cursor-not-allowed opacity-20) p-3", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		> | ||||
| 			<svg | ||||
| 				xmlns="http://www.w3.org/2000/svg" | ||||
| 				class="size-4" | ||||
| 				viewBox="0 0 24 24" | ||||
| 			> | ||||
| 				<path | ||||
| 					fill="none" | ||||
| 					stroke="currentColor" | ||||
| 					stroke-linecap="round" | ||||
| 					stroke-linejoin="round" | ||||
| 					stroke-width="2" | ||||
| 					d="M12 5v14m-7-7h14" | ||||
| 				/> | ||||
| 				<title>Increase number</title> | ||||
| 			</svg> | ||||
| 		</NumberFieldPrimitive.IncrementTrigger> | ||||
| 	); | ||||
| }; | ||||
							
								
								
									
										92
									
								
								apps/it-tools/src/components/ui/slider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								apps/it-tools/src/components/ui/slider.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,92 @@ | ||||
| import type { JSX, ValidComponent } from "solid-js" | ||||
| import { splitProps } from "solid-js" | ||||
|   | ||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic" | ||||
| import * as SliderPrimitive from "@kobalte/core/slider" | ||||
|   | ||||
| import { cn } from "@/libs/cn"; | ||||
| import { Label } from "@/components/ui/label" | ||||
|   | ||||
| type SliderRootProps<T extends ValidComponent = "div"> = SliderPrimitive.SliderRootProps<T> & { | ||||
|   class?: string | undefined | ||||
| } | ||||
|   | ||||
| const Slider = <T extends ValidComponent = "div">( | ||||
|   props: PolymorphicProps<T, SliderRootProps<T>> | ||||
| ) => { | ||||
|   const [local, others] = splitProps(props as SliderRootProps, ["class"]) | ||||
|   return ( | ||||
|     <SliderPrimitive.Root | ||||
|       class={cn("relative flex w-full touch-none select-none flex-col items-center", local.class)} | ||||
|       {...others} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| type SliderTrackProps<T extends ValidComponent = "div"> = SliderPrimitive.SliderTrackProps<T> & { | ||||
|   class?: string | undefined | ||||
| } | ||||
|   | ||||
| const SliderTrack = <T extends ValidComponent = "div">( | ||||
|   props: PolymorphicProps<T, SliderTrackProps<T>> | ||||
| ) => { | ||||
|   const [local, others] = splitProps(props as SliderTrackProps, ["class"]) | ||||
|   return ( | ||||
|     <SliderPrimitive.Track | ||||
|       class={cn("relative h-2 w-full grow rounded-full bg-secondary", local.class)} | ||||
|       {...others} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| type SliderFillProps<T extends ValidComponent = "div"> = SliderPrimitive.SliderFillProps<T> & { | ||||
|   class?: string | undefined | ||||
| } | ||||
|   | ||||
| const SliderFill = <T extends ValidComponent = "div">( | ||||
|   props: PolymorphicProps<T, SliderFillProps<T>> | ||||
| ) => { | ||||
|   const [local, others] = splitProps(props as SliderFillProps, ["class"]) | ||||
|   return ( | ||||
|     <SliderPrimitive.Fill | ||||
|       class={cn("absolute h-full rounded-full bg-primary", local.class)} | ||||
|       {...others} | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| type SliderThumbProps<T extends ValidComponent = "span"> = SliderPrimitive.SliderThumbProps<T> & { | ||||
|   class?: string | undefined | ||||
|   children?: JSX.Element | ||||
| } | ||||
|   | ||||
| const SliderThumb = <T extends ValidComponent = "span">( | ||||
|   props: PolymorphicProps<T, SliderThumbProps<T>> | ||||
| ) => { | ||||
|   const [local, others] = splitProps(props as SliderThumbProps, ["class", "children"]) | ||||
|   return ( | ||||
|     <SliderPrimitive.Thumb | ||||
|       class={cn( | ||||
|         "top-[-6px] block size-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", | ||||
|         local.class | ||||
|       )} | ||||
|       {...others} | ||||
|     > | ||||
|       <SliderPrimitive.Input /> | ||||
|     </SliderPrimitive.Thumb> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| const SliderLabel = <T extends ValidComponent = "label">( | ||||
|   props: PolymorphicProps<T, SliderPrimitive.SliderLabelProps<T>> | ||||
| ) => { | ||||
|   return <SliderPrimitive.Label as={Label} {...props} /> | ||||
| } | ||||
|   | ||||
| const SliderValueLabel = <T extends ValidComponent = "label">( | ||||
|   props: PolymorphicProps<T, SliderPrimitive.SliderValueLabelProps<T>> | ||||
| ) => { | ||||
|   return <SliderPrimitive.ValueLabel as={Label} {...props} /> | ||||
| } | ||||
|   | ||||
| export { Slider, SliderTrack, SliderFill, SliderThumb, SliderLabel, SliderValueLabel } | ||||
							
								
								
									
										84
									
								
								apps/it-tools/src/components/ui/switch.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								apps/it-tools/src/components/ui/switch.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | ||||
| import { cn } from "@/libs/cn"; | ||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; | ||||
| import type { | ||||
| 	SwitchControlProps, | ||||
| 	SwitchThumbProps, | ||||
| } from "@kobalte/core/switch"; | ||||
| import { Switch as SwitchPrimitive } from "@kobalte/core/switch"; | ||||
| import type { ParentProps, ValidComponent, VoidProps } from "solid-js"; | ||||
| import { splitProps } from "solid-js"; | ||||
| import { Label } from "./label"; | ||||
| 
 | ||||
| export const SwitchLabel = SwitchPrimitive.Label; | ||||
| export const Switch = SwitchPrimitive; | ||||
| export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage; | ||||
| export const SwitchDescription = SwitchPrimitive.Description; | ||||
| 
 | ||||
| type switchControlProps<T extends ValidComponent = "input"> = ParentProps< | ||||
| 	SwitchControlProps<T> & { class?: string } | ||||
| >; | ||||
| 
 | ||||
| export const SwitchControl = <T extends ValidComponent = "input">( | ||||
| 	props: PolymorphicProps<T, switchControlProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as switchControlProps, [ | ||||
| 		"class", | ||||
| 		"children", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<> | ||||
| 			<SwitchPrimitive.Input class="[&:focus-visible+div]:(outline-none ring-1.5 ring-ring ring-offset-2 ring-offset-background)" /> | ||||
| 			<SwitchPrimitive.Control | ||||
| 				class={cn( | ||||
| 					"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input shadow-sm transition-shadow data-[disabled]:(cursor-not-allowed opacity-50) data-[checked]:bg-primary transition-property-[box-shadow,color,background-color]", | ||||
| 					local.class, | ||||
| 				)} | ||||
| 				{...rest} | ||||
| 			> | ||||
| 				{local.children} | ||||
| 			</SwitchPrimitive.Control> | ||||
| 		</> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type switchThumbProps<T extends ValidComponent = "div"> = VoidProps< | ||||
| 	SwitchThumbProps<T> & { class?: string } | ||||
| >; | ||||
| 
 | ||||
| export const SwitchThumb = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, switchThumbProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as switchThumbProps, ["class"]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<SwitchPrimitive.Thumb | ||||
| 			class={cn( | ||||
| 				"pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg transition-transform data-[checked]:translate-x-4", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type SwitchCardProps = { | ||||
| 	label: string; | ||||
| 	description: string; | ||||
| 	checked: boolean; | ||||
| 	onChange: (checked: boolean) => void; | ||||
| } | ||||
| 
 | ||||
| export const SwitchCard = (props: SwitchCardProps) => { | ||||
| 
 | ||||
| 	return <Switch checked={props.checked} onChange={props.onChange} class="flex items-center justify-between gap-2 border rounded-md py-2 px-4"> | ||||
| 		<div > | ||||
| 			<SwitchLabel class="text-sm font-medium">{props.label}</SwitchLabel> | ||||
| 			<SwitchDescription class="text-sm text-muted-foreground">{props.description}</SwitchDescription> | ||||
| 		</div> | ||||
| 			<SwitchControl  > | ||||
| 				<SwitchThumb /> | ||||
| 			</SwitchControl> | ||||
| 		</Switch> | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										28
									
								
								apps/it-tools/src/components/ui/textarea.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								apps/it-tools/src/components/ui/textarea.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| import { cn } from "@/libs/cn"; | ||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; | ||||
| import type { TextFieldTextAreaProps } from "@kobalte/core/text-field"; | ||||
| import { TextArea as TextFieldPrimitive } from "@kobalte/core/text-field"; | ||||
| import type { ValidComponent, VoidProps } from "solid-js"; | ||||
| import { splitProps } from "solid-js"; | ||||
| 
 | ||||
| type textAreaProps<T extends ValidComponent = "textarea"> = VoidProps< | ||||
| 	TextFieldTextAreaProps<T> & { | ||||
| 		class?: string; | ||||
| 	} | ||||
| >; | ||||
| 
 | ||||
| export const TextArea = <T extends ValidComponent = "textarea">( | ||||
| 	props: PolymorphicProps<T, textAreaProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as textAreaProps, ["class"]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<TextFieldPrimitive | ||||
| 			class={cn( | ||||
| 				"flex min-h-[30px] w-full rounded-md border border-input bg-inherit px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
							
								
								
									
										126
									
								
								apps/it-tools/src/components/ui/textfield.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								apps/it-tools/src/components/ui/textfield.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | ||||
| import { cn } from "@/libs/cn"; | ||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; | ||||
| import type { | ||||
| 	TextFieldDescriptionProps, | ||||
| 	TextFieldErrorMessageProps, | ||||
| 	TextFieldInputProps, | ||||
| 	TextFieldLabelProps, | ||||
| 	TextFieldRootProps, | ||||
| } from "@kobalte/core/text-field"; | ||||
| import { TextField as TextFieldPrimitive } from "@kobalte/core/text-field"; | ||||
| import { cva } from "class-variance-authority"; | ||||
| import type { ValidComponent, VoidProps } from "solid-js"; | ||||
| import { splitProps } from "solid-js"; | ||||
| 
 | ||||
| type textFieldProps<T extends ValidComponent = "div"> = | ||||
| 	TextFieldRootProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const TextFieldRoot = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, textFieldProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as textFieldProps, ["class"]); | ||||
| 
 | ||||
| 	return <TextFieldPrimitive class={cn("space-y-1", local.class)} {...rest} />; | ||||
| }; | ||||
| 
 | ||||
| export const textfieldLabel = cva( | ||||
| 	"text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium", | ||||
| 	{ | ||||
| 		variants: { | ||||
| 			label: { | ||||
| 				true: "data-[invalid]:text-destructive", | ||||
| 			}, | ||||
| 			error: { | ||||
| 				true: "text-destructive text-xs", | ||||
| 			}, | ||||
| 			description: { | ||||
| 				true: "font-normal text-muted-foreground", | ||||
| 			}, | ||||
| 		}, | ||||
| 		defaultVariants: { | ||||
| 			label: true, | ||||
| 		}, | ||||
| 	}, | ||||
| ); | ||||
| 
 | ||||
| type textFieldLabelProps<T extends ValidComponent = "label"> = | ||||
| 	TextFieldLabelProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const TextFieldLabel = <T extends ValidComponent = "label">( | ||||
| 	props: PolymorphicProps<T, textFieldLabelProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as textFieldLabelProps, ["class"]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<TextFieldPrimitive.Label | ||||
| 			class={cn(textfieldLabel(), local.class)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type textFieldErrorMessageProps<T extends ValidComponent = "div"> = | ||||
| 	TextFieldErrorMessageProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const TextFieldErrorMessage = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, textFieldErrorMessageProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as textFieldErrorMessageProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<TextFieldPrimitive.ErrorMessage | ||||
| 			class={cn(textfieldLabel({ error: true }), local.class)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type textFieldDescriptionProps<T extends ValidComponent = "div"> = | ||||
| 	TextFieldDescriptionProps<T> & { | ||||
| 		class?: string; | ||||
| 	}; | ||||
| 
 | ||||
| export const TextFieldDescription = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, textFieldDescriptionProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as textFieldDescriptionProps, [ | ||||
| 		"class", | ||||
| 	]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<TextFieldPrimitive.Description | ||||
| 			class={cn(textfieldLabel({ description: true }), local.class)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
| 
 | ||||
| type textFieldInputProps<T extends ValidComponent = "input"> = VoidProps< | ||||
| 	TextFieldInputProps<T> & { | ||||
| 		class?: string; | ||||
| 	} | ||||
| >; | ||||
| 
 | ||||
| export const TextField = <T extends ValidComponent = "input">( | ||||
| 	props: PolymorphicProps<T, textFieldInputProps<T>>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props as textFieldInputProps, ["class"]); | ||||
| 
 | ||||
| 	return ( | ||||
| 		<TextFieldPrimitive.Input | ||||
| 			class={cn( | ||||
| 				"flex h-9 w-full rounded-md border border-input bg-inherit px-3 py-1 text-sm shadow-sm file:(border-0 bg-transparent text-sm font-medium) placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow", | ||||
| 				local.class, | ||||
| 			)} | ||||
| 			{...rest} | ||||
| 		/> | ||||
| 	); | ||||
| }; | ||||
							
								
								
									
										19
									
								
								apps/it-tools/src/i18n/i18n.models.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								apps/it-tools/src/i18n/i18n.models.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| import { buildLocalizedUrl } from './i18n.models'; | ||||
| import { describe, test, expect } from 'vitest'; | ||||
| 
 | ||||
| describe('i18n models', () => { | ||||
|   describe('buildLocalizedUrl', () => { | ||||
|     test('build an url prefixed with the language', () => { | ||||
|       expect(buildLocalizedUrl({lang: 'fr', path: '/tools/token-generator'})).toBe('/fr/tools/token-generator'); | ||||
|       expect(buildLocalizedUrl({lang: 'en', path: '/token-generator'})).toBe('/en/token-generator'); | ||||
|       expect(buildLocalizedUrl({path: '/token-generator'})).toBe('/token-generator'); | ||||
|     }); | ||||
| 
 | ||||
|     test('the path may not start with a slash', () => { | ||||
|       expect(buildLocalizedUrl({lang: 'fr', path: 'tools/token-generator'})).toBe('/fr/tools/token-generator'); | ||||
|       expect(buildLocalizedUrl({lang: 'en', path: 'token-generator'})).toBe('/en/token-generator'); | ||||
|       expect(buildLocalizedUrl({path: 'token-generator'})).toBe('/token-generator'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										9
									
								
								apps/it-tools/src/i18n/i18n.models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								apps/it-tools/src/i18n/i18n.models.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| export function buildLocalizedUrl({lang, path}: {lang?: string, path: string}) { | ||||
|    const slashlessPath = path.replace(/^\//, ""); | ||||
| 
 | ||||
|     if (lang) { | ||||
|         return `/${lang}/${slashlessPath}`; | ||||
|     } | ||||
| 
 | ||||
|     return `/${slashlessPath}`; | ||||
| } | ||||
							
								
								
									
										8
									
								
								apps/it-tools/src/i18n/i18n.routing.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								apps/it-tools/src/i18n/i18n.routing.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| import { locales } from "./languages"; | ||||
| 
 | ||||
| export function getStaticPaths() { | ||||
|   return [ | ||||
|     { params: { lang: undefined },  }, | ||||
|     ...locales.map((lang) => ({params: { lang },})), | ||||
|   ]; | ||||
| } | ||||
							
								
								
									
										9
									
								
								apps/it-tools/src/i18n/languages.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								apps/it-tools/src/i18n/languages.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| export const languages = { | ||||
|   en: "English", | ||||
|   fr: "Français", | ||||
| } as const; | ||||
| 
 | ||||
| export type LocaleKey = keyof typeof languages; | ||||
| 
 | ||||
| export const locales = Object.keys(languages) as LocaleKey[]; | ||||
| export const defaultLocale: LocaleKey = "en"; | ||||
							
								
								
									
										39
									
								
								apps/it-tools/src/layouts/base.layout.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								apps/it-tools/src/layouts/base.layout.astro
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| --- | ||||
| import { ColorModeScript } from "@kobalte/core"; | ||||
| import Header from "@/components/header.astro"; | ||||
| import '@/assets/app.css' | ||||
| import Footer from "@/components/footer.astro"; | ||||
| import { ClientRouter } from "astro:transitions"; | ||||
| import { defaultLocale } from "@/i18n/languages"; | ||||
| 
 | ||||
| const info = { | ||||
| 	title: 'IT-Tools', | ||||
| 	description: 'Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT.', | ||||
| }; | ||||
| 
 | ||||
| const { lang } = Astro.params; | ||||
| const locale = lang ?? defaultLocale; | ||||
| --- | ||||
| 
 | ||||
| <html lang={locale}> | ||||
| 	<head> | ||||
| 		<meta charset="utf-8" /> | ||||
| 		<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> | ||||
| 		<meta name="viewport" content="width=device-width" /> | ||||
| 		<meta name="generator" content={Astro.generator} /> | ||||
| 		<title>{info.title}</title> | ||||
| 		<meta name="description" content={info.description} /> | ||||
|         <ClientRouter /> | ||||
| 
 | ||||
|         <script is:inline>function e(){document.documentElement.setAttribute("data-kb-theme",localStorage.getItem("it-tools-theme")??(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"))}e(),document.addEventListener("astro:page-load",e)</script> | ||||
| 	</head> | ||||
| 	<body class="bg-background text-foreground font-sans min-h-screen text-sm antialiased flex flex-col"> | ||||
| 
 | ||||
| 
 | ||||
|             <Header /> | ||||
|             <main class="flex-1"> | ||||
|                 <slot /> | ||||
|             </main> | ||||
|             <Footer /> | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										5
									
								
								apps/it-tools/src/libs/cn.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/it-tools/src/libs/cn.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| import type { ClassValue } from "clsx"; | ||||
| import clsx from "clsx"; | ||||
| import { twMerge } from "tailwind-merge"; | ||||
| 
 | ||||
| export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists)); | ||||
							
								
								
									
										21
									
								
								apps/it-tools/src/pages/[...lang]/404.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								apps/it-tools/src/pages/[...lang]/404.astro
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| --- | ||||
| import { getStaticPaths } from "@/i18n/i18n.routing"; | ||||
| import { defaultLocale } from "@/i18n/languages"; | ||||
| import BaseLayout from "@/layouts/base.layout.astro"; | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { toolDefinitions } from "@/tools/definitions/tools.registry"; | ||||
| 
 | ||||
| export { getStaticPaths }; | ||||
| 
 | ||||
| const { lang= defaultLocale } = Astro.params; | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| <BaseLayout> | ||||
| 	<div class="max-w-screen-md mx-auto px-6 py-12"> | ||||
| 		<h1 class="text-2xl font-bold text-center">404</h1> | ||||
| 		<p class="text-sm text-muted-foreground text-center"> | ||||
| 			Page not found | ||||
| 		</p> | ||||
| 	</div> | ||||
| </BaseLayout> | ||||
							
								
								
									
										22
									
								
								apps/it-tools/src/pages/[...lang]/[toolSlug].astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								apps/it-tools/src/pages/[...lang]/[toolSlug].astro
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| --- | ||||
| 
 | ||||
| import BaseLayout from "@/layouts/base.layout.astro"; | ||||
| import { toolDefinitions } from "@/tools/definitions/tools.registry"; | ||||
| import { defaultLocale, locales } from "@/i18n/languages"; | ||||
| 
 | ||||
| export function getStaticPaths() { | ||||
|   return [...locales, undefined].flatMap((lang) => toolDefinitions.map((tool) => ({ | ||||
|     params: { lang: lang , toolSlug: tool.getLocalizedInfo({locale: lang ?? defaultLocale}).slug }, | ||||
|   }))); | ||||
| } | ||||
| 
 | ||||
| const { toolSlug, lang = defaultLocale } = Astro.params; | ||||
| 
 | ||||
| const toolDefinition = toolDefinitions.find((tool) => tool.getLocalizedInfo({locale: lang ?? defaultLocale}).slug === toolSlug)!; | ||||
| 
 | ||||
| 
 | ||||
| const { default: Tool } = await toolDefinition.entrypoint(); | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| <Tool /> | ||||
							
								
								
									
										72
									
								
								apps/it-tools/src/pages/[...lang]/index.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								apps/it-tools/src/pages/[...lang]/index.astro
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | ||||
| --- | ||||
| import { getStaticPaths } from "@/i18n/i18n.routing"; | ||||
| import { defaultLocale } from "@/i18n/languages"; | ||||
| import BaseLayout from "@/layouts/base.layout.astro"; | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { toolDefinitions } from "@/tools/definitions/tools.registry"; | ||||
| import ToolCard from "@/components/tool-card.astro"; | ||||
| import { cn } from "@/libs/cn"; | ||||
| 
 | ||||
| export { getStaticPaths }; | ||||
| 
 | ||||
| const { lang } = Astro.params; | ||||
| const locale = lang ?? defaultLocale; | ||||
| 
 | ||||
| const stats = [ | ||||
| 	{ | ||||
| 		label: "Tools", | ||||
| 		value: toolDefinitions.length, | ||||
| 	}, | ||||
| 	{ | ||||
| 		label: "Contributors", | ||||
| 		value: Intl.NumberFormat('en-US').format(10), | ||||
| 	}, | ||||
| 	{ | ||||
| 		label: "Self hosted instances", | ||||
| 		value: Intl.NumberFormat('en-US').format(1000), | ||||
| 	}, | ||||
| 	{ | ||||
| 		label: "GitHub Stars", | ||||
| 		value: Intl.NumberFormat('en-US').format(1000), | ||||
| 	}, | ||||
| ] | ||||
| --- | ||||
| 
 | ||||
| <BaseLayout> | ||||
| 
 | ||||
| 		<div class="max-w-screen-md mx-auto px-6 mt-32 flex items-center gap-12"> | ||||
| 			<div class="max-w-md"> | ||||
| 				<h1 class="text-4xl font-semibold">IT-Tools</h1> | ||||
| 				<p class="text-lg text-muted-foreground mt-4"> | ||||
| 					The open-source and self-hostable collection of handy online tools for developers and people working in IT. | ||||
| 				</p> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class={cn('i-solar-programming-line-duotone size-42 text-muted-foreground flex-shrink-0')} /> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<!-- Stats section --> | ||||
| 		<div class="px-6 mt-24 bg-muted/20 light:border-y"> | ||||
| 			<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 max-w-screen-lg mx-auto"> | ||||
| 				{stats.map((stat) => { | ||||
| 					return <div class="flex flex-col gap-2 items-center justify-center py-12"> | ||||
| 						<span class="text-2xl font-semibold">{stat.value}</span> | ||||
| 						<span class="text-sm text-muted-foreground">{stat.label}</span> | ||||
| 					</div>; | ||||
| 				})} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 	<div class="max-w-screen-lg mx-auto px-6 py-12"> | ||||
| 
 | ||||
| 
 | ||||
| 		<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4"> | ||||
| 			{toolDefinitions.map((tool) => { | ||||
| 				const { icon, getLocalizedInfo } = tool; | ||||
| 				const { slug, title, description } = getLocalizedInfo({locale}); | ||||
| 
 | ||||
| 				return <ToolCard slug={slug} name={title} description={description} icon={icon} />; | ||||
| 			})} | ||||
| 		</div> | ||||
| 	</div> | ||||
| </BaseLayout> | ||||
| @ -0,0 +1,54 @@ | ||||
| ## What is a Token Generator? | ||||
| 
 | ||||
| A token generator creates random strings of characters used as unique identifiers for authentication, API keys, session tokens, and other security purposes. | ||||
| 
 | ||||
| ## Features | ||||
| 
 | ||||
| ### Token Generation | ||||
| - Adjustable length from 1 to 512 characters | ||||
| - Customizable character sets (uppercase, lowercase, numbers, symbols) | ||||
| - Generate multiple tokens at once | ||||
| - Real-time generation as settings change | ||||
| 
 | ||||
| ### Security | ||||
| - Uses secure random number generation | ||||
| - Client-side generation for privacy | ||||
| - Configurable complexity levels | ||||
| 
 | ||||
| ### Interface | ||||
| - Visual controls for easy configuration | ||||
| - Copy functionality for generated tokens | ||||
| - Refresh option for new tokens | ||||
| - Responsive design | ||||
| 
 | ||||
| ## Use Cases | ||||
| 
 | ||||
| - API keys for web services and applications | ||||
| - Password reset verification tokens | ||||
| - Session identifiers for authentication | ||||
| - File upload and sharing tokens | ||||
| - Database record identifiers | ||||
| - Testing and development tokens | ||||
| 
 | ||||
| ## How to Use | ||||
| 
 | ||||
| 1. Set token length using the slider or number input (1-512 characters) | ||||
| 2. Choose character sets by toggling the switches | ||||
| 3. Tokens are generated automatically based on your settings | ||||
| 4. Copy tokens to clipboard or refresh to generate new ones | ||||
| 
 | ||||
| ## Technical Details | ||||
| 
 | ||||
| - Character sets: uppercase letters (A-Z), lowercase letters (a-z), numbers (0-9), symbols | ||||
| - Length range: 1 to 512 characters (or more) | ||||
| - Generation method: secure random sampling | ||||
| - Output format: plain text | ||||
| 
 | ||||
| ## Benefits | ||||
| 
 | ||||
| - Client-side generation for privacy | ||||
| - Fast and responsive interface | ||||
| - Highly customizable settings | ||||
| - Simple integration with development workflows | ||||
| 
 | ||||
| > **Security Notice**: For high-value production systems, always rotate tokens regularly and store them using secure hashing algorithms (bcrypt/scrypt). | ||||
| @ -0,0 +1,5 @@ | ||||
| export default { | ||||
|   title: "Token Generator", | ||||
|   description: "A token is a random string of characters that is often used for unique identifiers, such as API keys or verification URLs.", | ||||
|   slug: "token-generator", | ||||
| }; | ||||
| @ -0,0 +1,5 @@ | ||||
| export default { | ||||
|     title: "Générateur de tokens aléatoires", | ||||
|     description: "Générez des tokens aléatoires pour vos API, vos clés d'accès, ou tout autre cas où vous avez besoin d'un identifiant unique.", | ||||
|     slug: "generateur-de-tokens", | ||||
|   }; | ||||
| @ -0,0 +1,14 @@ | ||||
| import type { LocaleKey } from "@/i18n/languages"; | ||||
| import type { ToolDefinition } from "../tools.types"; | ||||
| import { locales } from "./token-generator.locales"; | ||||
| 
 | ||||
| export default { | ||||
|     id: "token-generator", | ||||
|     icon: "i-tabler-key", | ||||
|     entrypoint: () => import("./token-generator.entry.astro"), | ||||
|     getLocalizedInfo: ({locale}: {locale: LocaleKey}) => ({ | ||||
|         slug: locales[locale].slug, | ||||
|         title: locales[locale].title, | ||||
|         description: locales[locale].description, | ||||
|     }) | ||||
| } satisfies ToolDefinition; | ||||
| @ -0,0 +1,49 @@ | ||||
| --- | ||||
| import { defaultLocale, locales, type LocaleKey } from "@/i18n/languages"; | ||||
| import BaseLayout from "@/layouts/base.layout.astro"; | ||||
| import { TokenGenerator as TokenGeneratorComponent } from "@/tools/definitions/token-generator/token-generator"; | ||||
| 
 | ||||
| 
 | ||||
| export function getStaticPaths() { | ||||
|   return [ | ||||
|     { params: { lang: undefined } }, | ||||
|     ...locales.map((lang) => ({params: { lang },})) | ||||
|   ]; | ||||
| } | ||||
| 
 | ||||
| const { lang = defaultLocale } = Astro.params; | ||||
| const { default: toolTranslations } = await import(`./i18n/${lang}.ts`); | ||||
| 
 | ||||
| async function getContent(lang: LocaleKey) { | ||||
|   try { | ||||
|     return await  import(`./i18n/${lang}.md`).then(module => module.default); | ||||
|   } catch (error) { | ||||
|     return await import(`./i18n/en.md`).then(module => module.default); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| const Content = await getContent(lang); | ||||
| 
 | ||||
| 
 | ||||
| --- | ||||
| <BaseLayout> | ||||
|   <div class="max-w-screen-md mx-auto px-6 py-12 mt-12"> | ||||
|     <div class="flex flex-col sm:items-center gap-4 mb-2"> | ||||
|       <span class="flex-shrink-0 size-10 flex items-center justify-center bg-muted rounded-md p-2"> | ||||
|         <span class="i-tabler-key flex-shrink-0 size-6"></span> | ||||
|       </span> | ||||
|       <h1 class="text-3xl font-bold text-center">{toolTranslations.title}</h1> | ||||
|     </div> | ||||
|     <!-- <p class="text-sm text-muted-foreground mb-6 text-center text-balance">{toolTranslations.description}</p> --> | ||||
| 
 | ||||
|       <TokenGeneratorComponent client:load data={toolTranslations} /> | ||||
| 	</div> | ||||
| 
 | ||||
| 
 | ||||
|   <div class="border-b w-full my-12"></div> | ||||
|   <div class="prose max-w-screen-md mx-auto pb-24 dark:prose-invert px-6"> | ||||
|     <Content /> | ||||
|   </div> | ||||
| </BaseLayout> | ||||
|   | ||||
| @ -0,0 +1,7 @@ | ||||
| import en from "./i18n/en.ts"; | ||||
| import fr from "./i18n/fr.ts"; | ||||
| 
 | ||||
| export const locales = { | ||||
|     en, | ||||
|     fr, | ||||
| } as const; | ||||
| @ -0,0 +1,33 @@ | ||||
| export function generateToken({ | ||||
|   withUppercase = true, | ||||
|   withLowercase = true, | ||||
|   withNumbers = true, | ||||
|   withSymbols = true, | ||||
|   length = 64, | ||||
|   sample = (corpus) => corpus[Math.floor(Math.random() * corpus.length)], | ||||
| }: { | ||||
|   withUppercase?: boolean; | ||||
|   withLowercase?: boolean; | ||||
|   withNumbers?: boolean; | ||||
|   withSymbols?: boolean; | ||||
|   length?: number; | ||||
|   sample?: (corpus: string[]) => string; | ||||
| }) { | ||||
|   const corpus = [ | ||||
|     ...(withUppercase ? "ABCDEFGHIJKLMNOPQRSTUVWXYZ" : []), | ||||
|     ...(withLowercase ? "abcdefghijklmnopqrstuvwxyz" : []), | ||||
|     ...(withNumbers ? "0123456789" : []), | ||||
|     ...(withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : []), | ||||
|   ]; | ||||
| 
 | ||||
|   if(corpus.length === 0) { | ||||
|     return ""; | ||||
|   } | ||||
| 
 | ||||
|   let token = ""; | ||||
|   // imperative for loop for performance
 | ||||
|   for (let i = 0; i < length; i++) { | ||||
|     token += sample(corpus); | ||||
|   } | ||||
|   return token; | ||||
| } | ||||
| @ -0,0 +1,111 @@ | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import { TextArea } from "@/components/ui/textarea"; | ||||
| import { TextField, TextFieldRoot } from "@/components/ui/textfield"; | ||||
| import type { Component } from "solid-js"; | ||||
| import { createEffect, createSignal, on, onMount } from "solid-js"; | ||||
| import { generateToken } from "./token-generator.models"; | ||||
| import { SwitchCard } from "@/components/ui/switch"; | ||||
| import { NumberField, NumberFieldDecrementTrigger, NumberFieldIncrementTrigger, NumberFieldGroup, NumberFieldLabel, NumberFieldInput } from "@/components/ui/number-field"; | ||||
| import { Label } from "@/components/ui/label"; | ||||
| import { SliderLabel, SliderTrack, SliderValueLabel, SliderThumb, SliderFill } from "@/components/ui/slider"; | ||||
| import { Slider } from "@/components/ui/slider"; | ||||
| 
 | ||||
| export const TokenGenerator: Component<{ data: { title: string, description: string } }> = (props) => { | ||||
|   const [getLength, setLength] = createSignal<number>(64); | ||||
|   const [getTokenCount, setTokenCount] = createSignal<number>(1); | ||||
|   const [getWithUppercase, setWithUppercase] = createSignal<boolean>(true); | ||||
|   const [getWithLowercase, setWithLowercase] = createSignal<boolean>(true); | ||||
|   const [getWithNumbers, setWithNumbers] = createSignal<boolean>(true); | ||||
|   const [getWithSymbols, setWithSymbols] = createSignal<boolean>(false); | ||||
|   const [getTokens, setTokens] = createSignal<string[]>(buildTokens()); | ||||
| 
 | ||||
|   function buildTokens() { | ||||
|     return Array.from( | ||||
|       { length: getTokenCount() },  | ||||
|       () => generateToken({ | ||||
|       length: getLength(), | ||||
|       withUppercase: getWithUppercase(), | ||||
|       withLowercase: getWithLowercase(), | ||||
|       withNumbers: getWithNumbers(), | ||||
|       withSymbols: getWithSymbols(), | ||||
|     })); | ||||
|   } | ||||
| 
 | ||||
|   const refreshTokens = () => { | ||||
|     setTokens(buildTokens()); | ||||
|   } | ||||
| 
 | ||||
|   const getRowCount = () => { | ||||
|     const tokenCount = getTokenCount(); | ||||
| 
 | ||||
|     if(tokenCount < 10) { | ||||
|       return tokenCount; | ||||
|     } | ||||
| 
 | ||||
|     return 10; | ||||
|   } | ||||
| 
 | ||||
|   createEffect( | ||||
|     on([getWithUppercase, getWithLowercase, getWithNumbers, getWithSymbols, getLength, getTokenCount], () => refreshTokens()) | ||||
|   ); | ||||
| 
 | ||||
|   const copyTokens = () => { | ||||
|     navigator.clipboard.writeText(getTokens().join("\n")); | ||||
|   } | ||||
| 
 | ||||
|   return <div> | ||||
|       <TextFieldRoot class="w-full mb-4 mt-12"> | ||||
|         <TextArea value={getTokens().join("\n")} placeholder="Tokens will appear here" rows={getRowCount()} class="text-center font-mono" autoResize /> | ||||
|       </TextFieldRoot> | ||||
| 
 | ||||
|       <div class="flex justify-center gap-2 mb-8"> | ||||
|         <Button variant="outline" onClick={refreshTokens} class="gap-2"> | ||||
|           <div class="i-tabler-refresh size-4 text-muted-foreground" /> | ||||
|           Refresh | ||||
|         </Button> | ||||
|         <Button onClick={copyTokens} class="gap-2" > | ||||
|           <span class="i-tabler-copy size-4 text-muted" /> | ||||
|           Copy | ||||
|         </Button> | ||||
|       </div> | ||||
| 
 | ||||
| <Label>Token length</Label> | ||||
|       <div class="flex flex-row gap-8 items-center mb-4"> | ||||
| 
 | ||||
|     | ||||
| 
 | ||||
| 
 | ||||
|       <TextFieldRoot class="max-w-160px my-2"> | ||||
|           <NumberField value={getLength()} onRawValueChange={(value) => setLength(value)} > | ||||
| 			<NumberFieldGroup> | ||||
| 				<NumberFieldDecrementTrigger aria-label="Decrement" /> | ||||
| 				<NumberFieldInput /> | ||||
| 				<NumberFieldIncrementTrigger aria-label="Increment" /> | ||||
| 			</NumberFieldGroup> | ||||
|        </NumberField> | ||||
|         </TextFieldRoot> | ||||
| 
 | ||||
|         <Slider | ||||
|       minValue={1} | ||||
|       maxValue={512} | ||||
|       class="flex-1" | ||||
|       value={[Math.min(512, Math.max(1, getLength()))]} | ||||
|       onChange={(value) => setLength(value[0])} | ||||
|     > | ||||
|       <SliderTrack> | ||||
|         <SliderFill /> | ||||
|         <SliderThumb /> | ||||
|         <SliderThumb /> | ||||
|       </SliderTrack> | ||||
|     </Slider> | ||||
|       </div> | ||||
| 
 | ||||
| <Label>Character set</Label> | ||||
|       <div class="grid grid-cols-1 md:grid-cols-2 gap-2 my-2"> | ||||
|       <SwitchCard label="Uppercase" description="Include uppercase letters" checked={getWithUppercase()} onChange={setWithUppercase} /> | ||||
|       <SwitchCard label="Lowercase" description="Include lowercase letters" checked={getWithLowercase()} onChange={setWithLowercase} /> | ||||
|       <SwitchCard label="Numbers" description="Include numbers" checked={getWithNumbers()} onChange={setWithNumbers} /> | ||||
|       <SwitchCard label="Symbols" description="Include symbols" checked={getWithSymbols()} onChange={setWithSymbols} /> | ||||
|       </div> | ||||
|   </div>; | ||||
| }; | ||||
							
								
								
									
										6
									
								
								apps/it-tools/src/tools/definitions/tools.registry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								apps/it-tools/src/tools/definitions/tools.registry.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| import tokenGeneratorDefinition from "./token-generator/token-generator.definition"; | ||||
| import type { ToolDefinition } from "./tools.types"; | ||||
| 
 | ||||
| export const toolDefinitions: ToolDefinition[] = [ | ||||
|   tokenGeneratorDefinition, | ||||
| ]; | ||||
							
								
								
									
										12
									
								
								apps/it-tools/src/tools/definitions/tools.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								apps/it-tools/src/tools/definitions/tools.types.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| import type { LocaleKey } from "@/i18n/languages"; | ||||
| 
 | ||||
| export type ToolDefinition = { | ||||
|     id: string; | ||||
|     entrypoint: () => Promise<{ default: any }>; | ||||
|     icon: string; | ||||
|     getLocalizedInfo: ({locale}: {locale: LocaleKey}) => { | ||||
|         slug: string; | ||||
|         title: string; | ||||
|         description: string; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										18
									
								
								apps/it-tools/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								apps/it-tools/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| { | ||||
|   "extends": "astro/tsconfigs/strict", | ||||
|   "include": [ | ||||
|     ".astro/types.d.ts", | ||||
|     "**/*" | ||||
|   ], | ||||
|   "exclude": [ | ||||
|     "dist" | ||||
|   ], | ||||
|   "compilerOptions": { | ||||
|     "jsx": "preserve", | ||||
|     "jsxImportSource": "solid-js", | ||||
|     "baseUrl": "./", | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"] | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										99
									
								
								apps/it-tools/uno.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								apps/it-tools/uno.config.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | ||||
| import { defineConfig, presetUno, transformerDirectives, transformerVariantGroup, presetIcons, presetTypography, presetWebFonts } from "unocss"; | ||||
| import presetAnimations from "unocss-preset-animations"; | ||||
| import { toolDefinitions } from "./src/tools/definitions/tools.registry"; | ||||
| 
 | ||||
| export default defineConfig({ | ||||
|   presets: [ | ||||
|     presetUno({ | ||||
|       dark: { | ||||
|         dark: '[data-kb-theme="dark"]', | ||||
|         light: '[data-kb-theme="light"]' | ||||
|       } | ||||
|     }), | ||||
|     presetAnimations(), | ||||
|     presetIcons(), | ||||
|     presetTypography(), | ||||
|     presetWebFonts({ | ||||
|       provider: 'bunny', | ||||
|       fonts: { | ||||
|         sans: 'Inter:300,400,500,600,700,800', | ||||
|       }, | ||||
|     }), | ||||
|   ], | ||||
|   transformers: [transformerVariantGroup(), transformerDirectives()], | ||||
|   theme: { | ||||
|     colors: { | ||||
|       border: "hsl(var(--border))", | ||||
|       input: "hsl(var(--input))", | ||||
|       ring: "hsl(var(--ring))", | ||||
|       background: "hsl(var(--background))", | ||||
|       foreground: "hsl(var(--foreground))", | ||||
|       primary: { | ||||
|         DEFAULT: "hsl(var(--primary))", | ||||
|         foreground: "hsl(var(--primary-foreground))" | ||||
|       }, | ||||
|       secondary: { | ||||
|         DEFAULT: "hsl(var(--secondary))", | ||||
|         foreground: "hsl(var(--secondary-foreground))" | ||||
|       }, | ||||
|       destructive: { | ||||
|         DEFAULT: "hsl(var(--destructive))", | ||||
|         foreground: "hsl(var(--destructive-foreground))" | ||||
|       }, | ||||
|       muted: { | ||||
|         DEFAULT: "hsl(var(--muted))", | ||||
|         foreground: "hsl(var(--muted-foreground))" | ||||
|       }, | ||||
|       accent: { | ||||
|         DEFAULT: "hsl(var(--accent))", | ||||
|         foreground: "hsl(var(--accent-foreground))" | ||||
|       }, | ||||
|       popover: { | ||||
|         DEFAULT: "hsl(var(--popover))", | ||||
|         foreground: "hsl(var(--popover-foreground))" | ||||
|       }, | ||||
|       card: { | ||||
|         DEFAULT: "hsl(var(--card))", | ||||
|         foreground: "hsl(var(--card-foreground))" | ||||
|       } | ||||
|     }, | ||||
|     borderRadius: { | ||||
|       lg: `var(--radius)`, | ||||
|       md: `calc(var(--radius) - 2px)`, | ||||
|       sm: "calc(var(--radius) - 4px)" | ||||
|     }, | ||||
|     animation: { | ||||
|       keyframes: { | ||||
|         "accordion-down": | ||||
|           "{ from { height: 0 } to { height: var(--kb-accordion-content-height) } }", | ||||
|         "accordion-up": "{ from { height: var(--kb-accordion-content-height) } to { height: 0 } }", | ||||
|         "collapsible-down": | ||||
|           "{ from { height: 0 } to { height: var(--kb-collapsible-content-height) } }", | ||||
|         "collapsible-up": | ||||
|           "{ from { height: var(--kb-collapsible-content-height) } to { height: 0 } }" | ||||
|       }, | ||||
|       timingFns: { | ||||
|         "accordion-down": "ease-out", | ||||
|         "accordion-up": "ease-out", | ||||
|         "collapsible-down": "ease-out", | ||||
|         "collapsible-up": "ease-out" | ||||
|       }, | ||||
|       durations: { | ||||
|         "accordion-down": "0.2s", | ||||
|         "accordion-up": "0.2s", | ||||
|         "collapsible-down": "0.2s", | ||||
|         "collapsible-up": "0.2s" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   safelist: [ | ||||
|     'sm:grid-cols-2', | ||||
|     'sm:grid-cols-3', | ||||
|     'sm:grid-cols-4', | ||||
|     'sm:grid-cols-5', | ||||
|     'sm:grid-cols-6', | ||||
|     'sm:grid-cols-7', | ||||
|     'sm:grid-cols-8', | ||||
|     ...toolDefinitions.map((tool) => tool.icon), | ||||
|   ] | ||||
| }); | ||||
| @ -1,21 +0,0 @@ | ||||
| import antfu from '@antfu/eslint-config'; | ||||
| 
 | ||||
| export default antfu({ | ||||
|   stylistic: { | ||||
|     semi: true, | ||||
|   }, | ||||
| 
 | ||||
|   rules: { | ||||
|     // To allow export on top of files
 | ||||
|     'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }], | ||||
|     'curly': ['error', 'all'], | ||||
|     'vitest/consistent-test-it': ['error', { fn: 'test' }], | ||||
|     'ts/consistent-type-definitions': ['error', 'type'], | ||||
|     'style/brace-style': ['error', '1tbs', { allowSingleLine: false }], | ||||
|     'unused-imports/no-unused-vars': ['error', { | ||||
|       argsIgnorePattern: '^_', | ||||
|       varsIgnorePattern: '^_', | ||||
|       caughtErrorsIgnorePattern: '^_', | ||||
|     }], | ||||
|   }, | ||||
| }); | ||||
| @ -1,21 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
|     <meta name="theme-color" content="#000000" /> | ||||
|     <title>IT Tools - Handy online tools for developers</title> | ||||
| 
 | ||||
|     <meta name="title" content="IT Tools - Handy online tools for developers" /> | ||||
|     <meta name="description" content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT." /> | ||||
|   | ||||
|     <link rel="author" href="humans.txt" /> | ||||
|     <link rel="canonical" href="https://enclosed.cc/" /> | ||||
|   </head> | ||||
|   <body> | ||||
|     <noscript>You need to enable JavaScript to run this app.</noscript> | ||||
|     <div id="root"></div> | ||||
| 
 | ||||
|     <script src="/src/client.tsx" type="module"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										2381
									
								
								packages/app/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2381
									
								
								packages/app/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,54 +0,0 @@ | ||||
| { | ||||
|   "name": "@it-tools/app", | ||||
|   "type": "module", | ||||
|   "version": "0.0.0", | ||||
|   "packageManager": "pnpm@9.11.0", | ||||
|   "description": "Collection of handy online tools for developers, with great UX.", | ||||
|   "author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)", | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "https://github.com/CorentinTh/it-tools" | ||||
|   }, | ||||
|   "keywords": [], | ||||
|   "scripts": { | ||||
|     "dev": "vite", | ||||
|     "build": "vite build", | ||||
|     "serve": "vite preview", | ||||
|     "lint": "eslint .", | ||||
|     "lint:fix": "eslint --fix .", | ||||
|     "typecheck": "tsc --noEmit", | ||||
|     "test": "pnpm run test:unit", | ||||
|     "test:unit": "vitest run", | ||||
|     "test:unit:watch": "vitest watch", | ||||
|     "create:tool": "HYGEN_TMPLS=templates hygen tools new" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@corentinth/chisels": "^1.1.0", | ||||
|     "@kobalte/core": "^0.13.6", | ||||
|     "@solid-primitives/i18n": "^2.1.1", | ||||
|     "@solid-primitives/storage": "^4.2.1", | ||||
|     "@solidjs/router": "^0.14.7", | ||||
|     "@unocss/reset": "^0.62.4", | ||||
|     "class-variance-authority": "^0.7.0", | ||||
|     "clsx": "^2.1.1", | ||||
|     "cmdk-solid": "^1.1.0", | ||||
|     "lodash-es": "^4.17.21", | ||||
|     "solid-js": "^1.9.1", | ||||
|     "solid-sonner": "^0.2.8", | ||||
|     "tailwind-merge": "^2.5.2" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@antfu/eslint-config": "^3.7.3", | ||||
|     "@iconify-json/tabler": "^1.2.3", | ||||
|     "@types/lodash-es": "^4.17.12", | ||||
|     "@vitest/coverage-v8": "2.1.2", | ||||
|     "eslint": "^9.11.1", | ||||
|     "hygen": "^6.2.11", | ||||
|     "typescript": "^5.6.2", | ||||
|     "unocss": "^0.62.4", | ||||
|     "unocss-preset-animations": "^1.1.0", | ||||
|     "vite": "^5.4.8", | ||||
|     "vite-plugin-solid": "^2.10.2", | ||||
|     "vitest": "^2.1.2" | ||||
|   } | ||||
| } | ||||
| @ -1,4 +0,0 @@ | ||||
| /* TEAM */ | ||||
| Developer: Corentin Thomasset | ||||
| Site: https://corentin.tech | ||||
| Twitter: @cthmsst | ||||
| @ -1,2 +0,0 @@ | ||||
| User-agent: * | ||||
| Disallow: | ||||
| @ -1,75 +0,0 @@ | ||||
| :root { | ||||
|     --background: 0 0% 100%; | ||||
|     --foreground: 0 0% 3.9%; | ||||
|    | ||||
|     --card: 0 0% 100%; | ||||
|     --card-foreground: 0 0% 3.9%; | ||||
|    | ||||
|     --popover: 0 0% 100%; | ||||
|     --popover-foreground: 0 0% 3.9%; | ||||
|    | ||||
|     --primary: 0 0% 9%; | ||||
|     --primary-foreground: 0 0% 98%; | ||||
|    | ||||
|     --secondary: 0 0% 96.1%; | ||||
|     --secondary-foreground: 0 0% 9%; | ||||
|    | ||||
|     --muted: 0 0% 96.1%; | ||||
|     --muted-foreground: 0 0% 45.1%; | ||||
|    | ||||
|     --accent: 0 0% 96.1%; | ||||
|     --accent-foreground: 0 0% 9%; | ||||
|    | ||||
|     --destructive: 0 84.2% 60.2%; | ||||
|     --destructive-foreground: 0 0% 98%; | ||||
|    | ||||
|     --warning: 31 98% 50%; | ||||
|     --warning-foreground: 0 0% 98%; | ||||
|    | ||||
|     --border: 0 0% 89.8%; | ||||
|     --input: 0 0% 89.8%; | ||||
|     --ring: 0 0% 3.9%; | ||||
|    | ||||
|     --radius: 0.5rem; | ||||
|   } | ||||
|    | ||||
|   [data-kb-theme="dark"] { | ||||
|     --background: 0 0% 9%; | ||||
|     --foreground: 0 0% 98%; | ||||
|    | ||||
|     --card: 0 0% 7%; | ||||
|     --card-foreground: 0 0% 98%; | ||||
|    | ||||
|     --popover: 0 0% 3.9%; | ||||
|     --popover-foreground: 0 0% 98%; | ||||
|    | ||||
|     --primary: 83 79% 55%; | ||||
|     --primary-foreground: 0 0% 9%; | ||||
|    | ||||
|     --secondary: 0 0% 14.9%; | ||||
|     --secondary-foreground: 0 0% 98%; | ||||
|    | ||||
|     --muted: 0 0% 14.9%; | ||||
|     --muted-foreground: 0 0% 63.9%; | ||||
|    | ||||
|     --accent: 0 0% 14.9%; | ||||
|     --accent-foreground: 0 0% 98%; | ||||
|    | ||||
|     --destructive: 0 62.8% 30.6%; | ||||
|     --destructive-foreground: 0 0% 98%; | ||||
|    | ||||
|     --warning: 31 98% 50%; | ||||
|     --warning-foreground: 0 0% 98%; | ||||
|    | ||||
|     --border: 0 0% 14.9%; | ||||
|     --input: 0 0% 14.9%; | ||||
|     --ring: 0 0% 83.1%; | ||||
|   } | ||||
|    | ||||
|    | ||||
|   * { | ||||
|     @apply border-border; | ||||
|   } | ||||
|   body { | ||||
|     @apply bg-background text-foreground; | ||||
|   }  | ||||
| @ -1,69 +0,0 @@ | ||||
| import type { LocaleKey } from './modules/i18n/i18n.types'; | ||||
| import { A, Navigate, type RouteDefinition, useParams } from '@solidjs/router'; | ||||
| import { localeKeys } from './modules/i18n/i18n.constants'; | ||||
| import { useI18n } from './modules/i18n/i18n.provider'; | ||||
| import { HomePage } from './modules/pages/home.page'; | ||||
| import { ToolPage } from './modules/tools/pages/tool.page'; | ||||
| import { toolSlugs } from './modules/tools/tools.registry'; | ||||
| import { Button } from './modules/ui/components/button'; | ||||
| import { AppLayout } from './modules/ui/layouts/app.layout'; | ||||
| 
 | ||||
| export const routes: RouteDefinition[] = [ | ||||
|   { | ||||
|     path: '/', | ||||
|     component: () => { | ||||
|       const { getLocale } = useI18n(); | ||||
| 
 | ||||
|       return <Navigate href={`/${getLocale()}`} />; | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     path: '/', | ||||
|     component: AppLayout, | ||||
|     children: [ | ||||
|       { | ||||
|         path: '/:localeKey', | ||||
|         matchFilters: { | ||||
|           localeKey: localeKeys, | ||||
|         }, | ||||
|         component: (props) => { | ||||
|           const params = useParams(); | ||||
|           const { setLocale } = useI18n(); | ||||
| 
 | ||||
|           setLocale(params.localeKey as LocaleKey); | ||||
| 
 | ||||
|           return props.children; | ||||
|         }, | ||||
|         children: [ | ||||
|           { | ||||
|             path: '/', | ||||
|             component: HomePage, | ||||
|           }, | ||||
|           { | ||||
|             path: '/:toolSlug', | ||||
|             matchFilters: { | ||||
|               toolSlug: toolSlugs, | ||||
|             }, | ||||
|             component: ToolPage, | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|         path: '*404', | ||||
|         component: () => ( | ||||
|           <div class="flex flex-col items-center justify-center mt-6"> | ||||
|             <div class="text-3xl font-light text-muted-foreground">404</div> | ||||
|             <h1 class="font-semibold text-lg my-2">Page Not Found</h1> | ||||
|             <p class="text-muted-foreground">The page you are looking for does not exist.</p> | ||||
|             <p class="text-muted-foreground">Please check the URL and try again.</p> | ||||
|             <Button as={A} href="/" class="mt-4" variant="secondary"> | ||||
|               <div class="i-tabler-arrow-left mr-2"></div> | ||||
|               Go back home | ||||
|             </Button> | ||||
|           </div> | ||||
|         ), | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
| 
 | ||||
| ]; | ||||
| @ -1,43 +0,0 @@ | ||||
| /* @refresh reload */ | ||||
| 
 | ||||
| import { ColorModeProvider, ColorModeScript, createLocalStorageManager } from '@kobalte/core/color-mode'; | ||||
| import { Router } from '@solidjs/router'; | ||||
| import { render, Suspense } from 'solid-js/web'; | ||||
| import { routes } from './client-routes'; | ||||
| import { CommandPaletteProvider } from './modules/command-palette/command-palette.provider'; | ||||
| import { RootI18nProvider } from './modules/i18n/i18n.provider'; | ||||
| import { Toaster } from './modules/ui/components/sonner'; | ||||
| import '@unocss/reset/tailwind.css'; | ||||
| import 'virtual:uno.css'; | ||||
| import './app.css'; | ||||
| 
 | ||||
| render( | ||||
|   () => { | ||||
|     const initialColorMode = 'system'; | ||||
|     const colorModeStorageKey = 'it_tools_color_mode'; | ||||
|     const localStorageManager = createLocalStorageManager(colorModeStorageKey); | ||||
| 
 | ||||
|     return ( | ||||
|       <Router | ||||
|         children={routes} | ||||
|         root={props => ( | ||||
|           <Suspense> | ||||
|             <RootI18nProvider> | ||||
|               <ColorModeScript storageType={localStorageManager.type} storageKey={colorModeStorageKey} initialColorMode={initialColorMode} /> | ||||
|               <ColorModeProvider | ||||
|                 initialColorMode={initialColorMode} | ||||
|                 storageManager={localStorageManager} | ||||
|               > | ||||
|                 <CommandPaletteProvider> | ||||
|                   <Toaster /> | ||||
|                   <div class="min-h-screen font-sans text-sm font-400">{props.children}</div> | ||||
|                 </CommandPaletteProvider> | ||||
|               </ColorModeProvider> | ||||
|             </RootI18nProvider> | ||||
|           </Suspense> | ||||
|         )} | ||||
|       /> | ||||
|     ); | ||||
|   }, | ||||
|   document.getElementById('root')!, | ||||
| ); | ||||
| @ -1,73 +0,0 @@ | ||||
| { | ||||
|   "app": { | ||||
|     "title": "IT-Tools", | ||||
|     "description": "The open-source collection of handy online tools to help developers in their daily life." | ||||
|   }, | ||||
|   "navbar": { | ||||
|     "theme": { | ||||
|       "theme": "Theme", | ||||
|       "light-mode": "Light mode", | ||||
|       "dark-mode": "Dark mode", | ||||
|       "system-mode": "System" | ||||
|     }, | ||||
|     "language": "Language", | ||||
|     "contribute-to-i18n": "Contribute to i18n", | ||||
|     "github": "GitHub", | ||||
|     "support": "Support IT-Tools", | ||||
|     "report-bug": "Report a bug" | ||||
|   }, | ||||
|   "footer": { | ||||
|     "resources": { | ||||
|       "title": "Resources", | ||||
|       "all-tools": "All the tools", | ||||
|       "github": "GitHub repository", | ||||
|       "support": "Support IT-Tools", | ||||
|       "license": "License" | ||||
|     }, | ||||
|     "support": { | ||||
|       "title": "Support", | ||||
|       "report-bug": "Report a bug", | ||||
|       "request-feature": "Request a feature", | ||||
|       "contribute": "Contribute to the project", | ||||
|       "contact": "Contact me" | ||||
|     }, | ||||
|     "friends": { | ||||
|       "title": "Friends" | ||||
|     } | ||||
|   }, | ||||
|   "commandPalette": { | ||||
|     "input-placeholder": "Type to search for a tool or a command...", | ||||
|     "go-home": "Go to home", | ||||
|     "sections": { | ||||
|       "tools": "Tools", | ||||
|       "navigation": "Navigation", | ||||
|       "language": "Language", | ||||
|       "theme": "Theme" | ||||
|     }, | ||||
|     "theme": { | ||||
|       "switch-to-light": "Switch to light theme", | ||||
|       "switch-to-dark": "Switch to dark theme", | ||||
|       "switch-to-system": "Use to system theme" | ||||
|     }, | ||||
|     "trigger": { | ||||
|       "search": "Search for a tool" | ||||
|     } | ||||
|   }, | ||||
|   "home": { | ||||
|     "all-tools": "All the tools", | ||||
|     "search-tools": "Search for a tool", | ||||
|     "open-source": "Open Source", | ||||
|     "free": "Free", | ||||
|     "self-hostable": "Self-hostable" | ||||
|   }, | ||||
|   "tools": { | ||||
|     "token-generator": { | ||||
|       "name": "Token Generator", | ||||
|       "description": "Generate random string with the characters you want, uppercase, lowercase letters, numbers and/or symbols." | ||||
|     }, | ||||
|     "random-port-generator": { | ||||
|       "name": "Random Port Generator", | ||||
|       "description": "Generate a random port number outside of the reserved ports range (0-1023)." | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,59 +0,0 @@ | ||||
| { | ||||
|   "app": { | ||||
|     "title": "IT-Tools", | ||||
|     "description": "La collection open-source d'outils en ligne pour aider les devs dans leur vie quotidienne." | ||||
|   }, | ||||
|   "navbar": { | ||||
|     "theme": { | ||||
|       "theme": "Thème", | ||||
|       "light-mode": "Mode clair", | ||||
|       "dark-mode": "Mode sombre", | ||||
|       "system-mode": "Système" | ||||
|     }, | ||||
|     "language": "Langue", | ||||
|     "contribute-to-i18n": "Contribuer à l'i18n", | ||||
|     "github": "GitHub", | ||||
|     "support": "Soutenir IT-Tools", | ||||
|     "report-bug": "Signaler un bug" | ||||
|   }, | ||||
|   "footer": { | ||||
|     "resources": { | ||||
|       "title": "Ressources", | ||||
|       "all-tools": "Tous les outils", | ||||
|       "github": "Dépôt GitHub", | ||||
|       "support": "Soutenir IT-Tools", | ||||
|       "license": "Licence" | ||||
|     }, | ||||
|     "support": { | ||||
|       "title": "Support", | ||||
|       "report-bug": "Signaler un bug", | ||||
|       "request-feature": "Demander une fonctionnalité", | ||||
|       "contribute": "Contribuer au projet", | ||||
|       "contact": "Me contacter" | ||||
|     }, | ||||
|     "friends": { | ||||
|       "title": "Ami·e·s" | ||||
|     } | ||||
|   }, | ||||
|   "commandPalette": { | ||||
|     "input-placeholder": "Tapez pour rechercher un outil...", | ||||
|     "go-home": "Aller à l'accueil", | ||||
|     "sections": { | ||||
|       "tools": "Outils", | ||||
|       "navigation": "Navigation", | ||||
|       "theme": "Thème" | ||||
|     } | ||||
|   }, | ||||
|   "home": { | ||||
|     "all-tools": "Tous les outils", | ||||
|     "open-source": "Open Source", | ||||
|     "free": "Gratuit", | ||||
|     "self-hostable": "Self-hostable" | ||||
|   }, | ||||
|   "tools": { | ||||
|     "token-generator": { | ||||
|       "name": "Générateur de token", | ||||
|       "description": "Générer des string aléatoires, contrôlez les caractères que vous voulez, lettres majuscules, minuscules, chiffres et/ou symboles." | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,144 +0,0 @@ | ||||
| import type { Accessor, ParentComponent } from 'solid-js'; | ||||
| import { useNavigate } from '@solidjs/router'; | ||||
| import { createContext, createMemo, createSignal, For, onCleanup, onMount, useContext } from 'solid-js'; | ||||
| import { locales } from '../i18n/i18n.constants'; | ||||
| import { useI18n } from '../i18n/i18n.provider'; | ||||
| import { useToolsStore } from '../tools/tools.store'; | ||||
| import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../ui/components/command'; | ||||
| import { useThemeStore } from '../ui/themes/theme.store'; | ||||
| import { cn } from '../ui/utils/cn'; | ||||
| 
 | ||||
| const CommandPaletteContext = createContext<{ | ||||
|   getIsCommandPaletteOpen: Accessor<boolean>; | ||||
|   openCommandPalette: () => void; | ||||
|   closeCommandPalette: () => void; | ||||
| }>(); | ||||
| 
 | ||||
| export function useCommandPalette() { | ||||
|   const context = useContext(CommandPaletteContext); | ||||
| 
 | ||||
|   if (!context) { | ||||
|     throw new Error('CommandPalette context not found'); | ||||
|   } | ||||
| 
 | ||||
|   return context; | ||||
| } | ||||
| 
 | ||||
| export const CommandPaletteProvider: ParentComponent = (props) => { | ||||
|   const [getIsCommandPaletteOpen, setIsCommandPaletteOpen] = createSignal(false); | ||||
| 
 | ||||
|   const handleKeyDown = (e: KeyboardEvent) => { | ||||
|     if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { | ||||
|       e.preventDefault(); | ||||
|       setIsCommandPaletteOpen(true); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   onMount(() => { | ||||
|     document.addEventListener('keydown', handleKeyDown); | ||||
|   }); | ||||
| 
 | ||||
|   onCleanup(() => { | ||||
|     document.removeEventListener('keydown', handleKeyDown); | ||||
|   }); | ||||
| 
 | ||||
|   const { getTools } = useToolsStore(); | ||||
|   const navigate = useNavigate(); | ||||
|   const { t, createLocalizedUrl, changeLocale } = useI18n(); | ||||
|   const { setColorMode } = useThemeStore(); | ||||
| 
 | ||||
|   const getCommandData = createMemo(() => [ | ||||
|     { | ||||
|       label: t('commandPalette.sections.tools'), | ||||
|       options: [ | ||||
|         ...getTools().map(tool => ({ | ||||
|           label: tool.name, | ||||
|           icon: tool.icon, | ||||
|           action: () => navigate(createLocalizedUrl({ path: tool.slug })), | ||||
|         })), | ||||
|       ], | ||||
|     }, | ||||
|     { | ||||
|       label: t('commandPalette.sections.navigation'), | ||||
|       options: [ | ||||
|         { | ||||
|           label: t('commandPalette.go-home'), | ||||
|           icon: 'i-tabler-home', | ||||
|           action: () => navigate(createLocalizedUrl({ path: '' })), | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|     { | ||||
|       label: t('commandPalette.sections.language'), | ||||
|       options: [ | ||||
|         ...locales.map(locale => ({ | ||||
|           label: locale.switchToLabel, | ||||
|           icon: 'i-custom-language', | ||||
|           action: () => changeLocale(locale.key), | ||||
|           keywords: [locale.name, locale.key], | ||||
|         })), | ||||
|       ], | ||||
|     }, | ||||
|     { | ||||
|       label: t('commandPalette.sections.theme'), | ||||
|       options: [ | ||||
|         { | ||||
|           label: t('commandPalette.theme.switch-to-light'), | ||||
|           icon: 'i-tabler-sun', | ||||
|           action: () => setColorMode({ mode: 'light' }), | ||||
|         }, | ||||
|         { | ||||
|           label: t('commandPalette.theme.switch-to-dark'), | ||||
|           icon: 'i-tabler-moon', | ||||
|           action: () => setColorMode({ mode: 'dark' }), | ||||
|         }, | ||||
|         { | ||||
|           label: t('commandPalette.theme.switch-to-system'), | ||||
|           icon: 'i-tabler-device-laptop', | ||||
|           action: () => setColorMode({ mode: 'system' }), | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|   ]); | ||||
| 
 | ||||
|   const onCommandSelect = ({ action }: { action: () => void }) => { | ||||
|     action(); | ||||
|     setIsCommandPaletteOpen(false); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <CommandPaletteContext.Provider value={{ | ||||
|       getIsCommandPaletteOpen, | ||||
|       openCommandPalette: () => setIsCommandPaletteOpen(true), | ||||
|       closeCommandPalette: () => setIsCommandPaletteOpen(false), | ||||
|     }} | ||||
|     > | ||||
|       <CommandDialog | ||||
|         class="rounded-lg border shadow-md" | ||||
|         open={getIsCommandPaletteOpen()} | ||||
|         onOpenChange={setIsCommandPaletteOpen} | ||||
|       > | ||||
|         <CommandInput placeholder={t('commandPalette.input-placeholder')} /> | ||||
|         <CommandList> | ||||
|           <CommandEmpty>No results found.</CommandEmpty> | ||||
|           <For each={getCommandData()}> | ||||
|             {section => ( | ||||
|               <CommandGroup heading={section.label}> | ||||
|                 <For each={section.options}> | ||||
|                   {item => ( | ||||
|                     <CommandItem onSelect={() => onCommandSelect(item)}> | ||||
|                       <span class={cn('mr-2 ml-1 size-4 text-muted-foreground', item.icon)} /> | ||||
|                       <span>{item.label}</span> | ||||
|                     </CommandItem> | ||||
|                   )} | ||||
|                 </For> | ||||
|               </CommandGroup> | ||||
|             )} | ||||
|           </For> | ||||
|         </CommandList> | ||||
|       </CommandDialog> | ||||
| 
 | ||||
|       {props.children} | ||||
|     </CommandPaletteContext.Provider> | ||||
|   ); | ||||
| }; | ||||
| @ -1,18 +0,0 @@ | ||||
| import { map } from 'lodash-es'; | ||||
| 
 | ||||
| export const locales = [ | ||||
|   { | ||||
|     key: 'en', | ||||
|     file: 'en', | ||||
|     name: 'English', | ||||
|     switchToLabel: 'Change language to English', | ||||
|   }, | ||||
|   { | ||||
|     key: 'fr', | ||||
|     file: 'fr', | ||||
|     name: 'Français', | ||||
|     switchToLabel: 'Changer la langue en Français', | ||||
|   }, | ||||
| ] as const; | ||||
| 
 | ||||
| export const localeKeys = map(locales, 'key'); | ||||
| @ -1,96 +0,0 @@ | ||||
| import type { ParentComponent } from 'solid-js'; | ||||
| import type { LocaleKey } from './i18n.types'; | ||||
| import { joinUrlPaths } from '@corentinth/chisels'; | ||||
| import * as i18n from '@solid-primitives/i18n'; | ||||
| import { makePersisted } from '@solid-primitives/storage'; | ||||
| import { useNavigate } from '@solidjs/router'; | ||||
| import { merge } from 'lodash-es'; | ||||
| import { createContext, createResource, createSignal, Show, useContext } from 'solid-js'; | ||||
| import defaultDict from '../../locales/en.json'; | ||||
| import { locales } from './i18n.constants'; | ||||
| 
 | ||||
| export { | ||||
|   useI18n, | ||||
| }; | ||||
| 
 | ||||
| type RawDictionary = typeof defaultDict; | ||||
| type Dictionary = i18n.Flatten<RawDictionary>; | ||||
| 
 | ||||
| const RootI18nContext = createContext<{ | ||||
|   t: i18n.Translator<Dictionary>; | ||||
|   getLocale: () => LocaleKey; | ||||
|   setLocale: (locale: LocaleKey) => void; | ||||
|   locales: typeof locales; | ||||
| } | undefined>(undefined); | ||||
| 
 | ||||
| function useI18n() { | ||||
|   const context = useContext(RootI18nContext); | ||||
|   const navigate = useNavigate(); | ||||
| 
 | ||||
|   if (!context) { | ||||
|     throw new Error('I18n context not found'); | ||||
|   } | ||||
| 
 | ||||
|   const { t, getLocale, setLocale, locales } = context; | ||||
| 
 | ||||
|   return { | ||||
|     t, | ||||
|     getLocale, | ||||
|     setLocale, | ||||
|     locales, | ||||
|     createLocalizedUrl: ({ path }: { path: string }) => { | ||||
|       const newPath = joinUrlPaths(getLocale(), path); | ||||
| 
 | ||||
|       return `/${newPath}`; | ||||
|     }, | ||||
|     changeLocale: (locale: LocaleKey) => { | ||||
|       setLocale(locale); | ||||
| 
 | ||||
|       const pathWithoutLocale = location.pathname.split('/').slice(2).join('/'); | ||||
|       const newPath = joinUrlPaths(locale, pathWithoutLocale); | ||||
|       navigate(`/${newPath}`); | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| async function fetchDictionary(locale: LocaleKey): Promise<Dictionary> { | ||||
|   const dict: RawDictionary = (await import(`../../locales/${locale}.json`)); | ||||
|   const mergedDict = merge({}, defaultDict, dict); | ||||
|   const flattened = i18n.flatten(mergedDict); | ||||
| 
 | ||||
|   return flattened; | ||||
| } | ||||
| 
 | ||||
| export function getBrowserLocale(): LocaleKey { | ||||
|   const browserLocale = navigator.language?.split('-')[0]; | ||||
| 
 | ||||
|   if (!browserLocale) { | ||||
|     return 'en'; | ||||
|   } | ||||
| 
 | ||||
|   return locales.find(locale => locale.key === browserLocale)?.key ?? 'en'; | ||||
| } | ||||
| 
 | ||||
| export const RootI18nProvider: ParentComponent = (props) => { | ||||
|   const browserLocale = getBrowserLocale(); | ||||
|   const [getLocale, setLocale] = makePersisted(createSignal<LocaleKey>(browserLocale), { name: 'it_tools_locale', storage: localStorage }); | ||||
| 
 | ||||
|   const [dict] = createResource(getLocale, fetchDictionary); | ||||
| 
 | ||||
|   return ( | ||||
|     <Show when={dict()}> | ||||
|       {dict => ( | ||||
|         <RootI18nContext.Provider | ||||
|           value={{ | ||||
|             t: i18n.translator(dict), | ||||
|             getLocale, | ||||
|             setLocale, | ||||
|             locales, | ||||
|           }} | ||||
|         > | ||||
|           {props.children} | ||||
|         </RootI18nContext.Provider> | ||||
|       )} | ||||
|     </Show> | ||||
|   ); | ||||
| }; | ||||
| @ -1,3 +0,0 @@ | ||||
| import type { locales } from './i18n.constants'; | ||||
| 
 | ||||
| export type LocaleKey = typeof locales[number]['key']; | ||||
| @ -1,87 +0,0 @@ | ||||
| import type { Component } from 'solid-js'; | ||||
| import { A } from '@solidjs/router'; | ||||
| import { useCommandPalette } from '../command-palette/command-palette.provider'; | ||||
| import { useI18n } from '../i18n/i18n.provider'; | ||||
| import { useToolsStore } from '../tools/tools.store'; | ||||
| import { Badge } from '../ui/components/badge'; | ||||
| import { Button } from '../ui/components/button'; | ||||
| import { Card, CardDescription, CardHeader, CardTitle } from '../ui/components/card'; | ||||
| import { cn } from '../ui/utils/cn'; | ||||
| 
 | ||||
| export const HomePage: Component = () => { | ||||
|   const { t } = useI18n(); | ||||
|   const { getTools } = useToolsStore(); | ||||
|   const { openCommandPalette } = useCommandPalette(); | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <div class="w-full bg-[linear-gradient(to_right,#80808010_1px,transparent_1px),linear-gradient(to_bottom,#80808010_1px,transparent_1px)] bg-[size:48px_48px] pt-20"> | ||||
| 
 | ||||
|         <div class="flex justify-center gap-24 items-center p-6"> | ||||
|           <div class="max-w-xl flex flex-col gap-6 "> | ||||
|             <div class="flex items-center gap-2"> | ||||
|               <Badge class="text-primary bg-primary/10 hover:bg-primary/10"> | ||||
|                 {t('home.open-source')} | ||||
|               </Badge> | ||||
| 
 | ||||
|               <Badge class="text-primary bg-primary/10 hover:bg-primary/10"> | ||||
|                 {t('home.free')} | ||||
|               </Badge> | ||||
| 
 | ||||
|               <Badge class="text-primary bg-primary/10 hover:bg-primary/10"> | ||||
|                 {t('home.self-hostable')} | ||||
|               </Badge> | ||||
|             </div> | ||||
|             <h1 class="text-5xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250"> | ||||
|               <span class="font-bold ">IT</span> | ||||
|               <span class="text-90% text-primary font-extrabold border border-5px leading-none border-current rounded-xl px-2 py-0.5 ml-3">TOOLS</span> | ||||
|             </h1> | ||||
| 
 | ||||
|             <p class="text-xl text-muted-foreground"> | ||||
|               {t('app.description')} | ||||
|             </p> | ||||
| 
 | ||||
|             <div class="flex items-center gap-4"> | ||||
|               <Button variant="default" as={A} href="tools"> | ||||
|                 {t('home.all-tools')} | ||||
|                 <div class="i-tabler-arrow-right ml-2 text-base"></div> | ||||
|               </Button> | ||||
| 
 | ||||
|               <Button variant="outline" onClick={openCommandPalette}> | ||||
|                 <div class="i-tabler-search mr-2 text-base" /> | ||||
|                 {t('home.search-tools')} | ||||
|               </Button> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="relative hidden md:block"> | ||||
|             <div class="absolute top-4 left-0 w-full h-full flex items-center justify-center blur-2xl rounded-full opacity-20 bg-gradient-to-br from-primary to-transparent" /> | ||||
|             <div class="i-tabler-terminal text-9xl text-primary m-8" /> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="bg-gradient-to-t dark:from-background to-transparent h-24 mt-24"></div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 max-w-1200px mx-auto p-6"> | ||||
|         {getTools().map(tool => ( | ||||
|           <A href={tool.slug} class="h-full"> | ||||
|             <Card class="hover:(shadow-md transform scale-101) transition-transform h-full"> | ||||
|               <CardHeader> | ||||
|                 <div class={cn(tool.icon, 'size-12 text-muted-foreground/60')} /> | ||||
| 
 | ||||
|                 <CardTitle class="text-base font-semibold"> | ||||
|                   {tool.name} | ||||
|                 </CardTitle> | ||||
| 
 | ||||
|                 <CardDescription> | ||||
|                   {tool.description} | ||||
|                 </CardDescription> | ||||
|               </CardHeader> | ||||
|             </Card> | ||||
|           </A> | ||||
|         ))} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @ -1,37 +0,0 @@ | ||||
| import type { Accessor, Component, ComponentProps } from 'solid-js'; | ||||
| import { Button } from '@/modules/ui/components/button'; | ||||
| import { omit } from 'lodash-es'; | ||||
| import { Show, splitProps } from 'solid-js'; | ||||
| import { useCopy } from './copy'; | ||||
| 
 | ||||
| export const CopyButton: Component<{ textToCopy: Accessor<string | number>; toastMessage?: string } & ComponentProps<typeof Button>> = (props) => { | ||||
|   const [localProps, buttonProps] = splitProps(props, ['textToCopy', 'toastMessage']); | ||||
|   const { copy, getIsJustCopied } = useCopy(localProps.textToCopy, { toastMessage: localProps.toastMessage }); | ||||
| 
 | ||||
|   return ( | ||||
|     <Button onClick={copy} {...omit(buttonProps, ['textToCopy', 'toastMessage'])}> | ||||
|       <Show | ||||
|         when={buttonProps.children} | ||||
|         fallback={( | ||||
| 
 | ||||
|           getIsJustCopied() | ||||
|             ? ( | ||||
|                 <> | ||||
|                   <div class="i-tabler-check mr-2 text-base" /> | ||||
|                   Copied! | ||||
|                 </> | ||||
|               ) | ||||
|             : ( | ||||
|                 <> | ||||
|                   <div class="i-tabler-copy mr-2 text-base" /> | ||||
|                   Copy to clipboard | ||||
|                 </> | ||||
|               ) | ||||
| 
 | ||||
|         )} | ||||
|       > | ||||
|         {buttonProps.children} | ||||
|       </Show> | ||||
|     </Button> | ||||
|   ); | ||||
| }; | ||||
| @ -1,23 +0,0 @@ | ||||
| import type { Accessor } from 'solid-js'; | ||||
| import { createSignal } from 'solid-js'; | ||||
| import { toast } from '../../ui/components/sonner'; | ||||
| 
 | ||||
| export { useCopy, writeTextToClipboard }; | ||||
| 
 | ||||
| function writeTextToClipboard({ text }: { text: string }) { | ||||
|   return navigator.clipboard.writeText(text); | ||||
| } | ||||
| 
 | ||||
| function useCopy(getText: Accessor<string | number>, { toastMessage = 'Copied to clipboard' }: { toastMessage?: string } = {}) { | ||||
|   const [getIsJustCopied, setIsJustCopied] = createSignal(false); | ||||
| 
 | ||||
|   return { | ||||
|     getIsJustCopied, | ||||
|     copy: () => { | ||||
|       writeTextToClipboard({ text: String(getText()) }); | ||||
|       setIsJustCopied(true); | ||||
|       setTimeout(() => setIsJustCopied(false), 2000); | ||||
|       toast(toastMessage); | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| @ -1,33 +0,0 @@ | ||||
| import { describe, expect, test } from 'vitest'; | ||||
| import { createRefreshableSignal } from './signals'; | ||||
| 
 | ||||
| describe('signals', () => { | ||||
|   describe('createRefreshableSignal', () => { | ||||
|     test('the state initially has the value returned by the getter', () => { | ||||
|       const [getState] = createRefreshableSignal(() => 42); | ||||
|       expect(getState()).to.eql(42); | ||||
|     }); | ||||
| 
 | ||||
|     test('calling the refresh function updates the state', () => { | ||||
|       let value = 0; | ||||
|       const [getState, refresh] = createRefreshableSignal(() => value++); | ||||
| 
 | ||||
|       expect(getState()).to.eql(0); | ||||
| 
 | ||||
|       refresh(); | ||||
| 
 | ||||
|       expect(getState()).to.eql(1); | ||||
|       expect(getState()).to.eql(1); | ||||
|     }); | ||||
| 
 | ||||
|     test('the state can be muted using the setState function', () => { | ||||
|       const [getState, , { setState }] = createRefreshableSignal(() => 0); | ||||
| 
 | ||||
|       expect(getState()).to.eql(0); | ||||
| 
 | ||||
|       setState(42); | ||||
| 
 | ||||
|       expect(getState()).to.eql(42); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -1,13 +0,0 @@ | ||||
| import { createSignal } from 'solid-js'; | ||||
| 
 | ||||
| export { createRefreshableSignal }; | ||||
| 
 | ||||
| function createRefreshableSignal<T>(getValue: () => T) { | ||||
|   const [getState, setState] = createSignal<T>(getValue()); | ||||
| 
 | ||||
|   return [ | ||||
|     getState, | ||||
|     () => setState(() => getValue()), | ||||
|     { setState }, | ||||
|   ] as const; | ||||
| } | ||||
| @ -1,30 +0,0 @@ | ||||
| import type { Component, ParentComponent } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| 
 | ||||
| export const ToolHeader: ParentComponent<{ name: string; description: string; icon: string }> = (props) => { | ||||
|   return ( | ||||
|     <div> | ||||
|       <div class="w-full bg-[linear-gradient(to_right,#80808010_1px,transparent_1px),linear-gradient(to_bottom,#80808010_1px,transparent_1px)] bg-[size:48px_48px] pt-12"> | ||||
| 
 | ||||
|         <div class="flex gap-4 mb-8 max-w-1200px mx-auto px-6 items-start flex-col md:flex-row md:items-center"> | ||||
|           <div class="bg-card p-4 rounded-lg"> | ||||
|             <div class={cn(props.icon, 'size-8 md:size-12 text-muted-foreground')} /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|             <h1 class="text-xl font-semibold"> | ||||
|               {props.name} | ||||
|             </h1> | ||||
|             <div class="text-muted-foreground text-base"> | ||||
|               {props.description} | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="bg-gradient-to-t dark:from-background to-transparent h-24 mt-12 mb--24"></div> | ||||
|       </div> | ||||
| 
 | ||||
|       {props.children} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @ -1,6 +0,0 @@ | ||||
| { | ||||
|   "name": "Random Port Generator", | ||||
|   "description": "Generate a random port number outside of the reserved ports range (0-1023).", | ||||
|   "refresh": "Refresh port", | ||||
|   "copy-toast": "Port copied to clipboard" | ||||
| } | ||||
| @ -1,49 +0,0 @@ | ||||
| import type { Component } from 'solid-js'; | ||||
| import { CopyButton } from '@/modules/shared/copy/copy-button'; | ||||
| import { createRefreshableSignal } from '@/modules/shared/signals'; | ||||
| import { Button } from '@/modules/ui/components/button'; | ||||
| import { Card, CardContent, CardHeader } from '@/modules/ui/components/card'; | ||||
| import { ToolHeader } from '../../components/tool-header'; | ||||
| import { useCurrentTool } from '../../tools.provider'; | ||||
| import defaultDictionary from './locales/en.json'; | ||||
| import { generateRandomPort } from './random-port-generator.services'; | ||||
| 
 | ||||
| const RandomPortGenerator: Component = () => { | ||||
|   const [getPort, refreshPort] = createRefreshableSignal(generateRandomPort); | ||||
|   const { t, getTool } = useCurrentTool({ defaultDictionary }); | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <ToolHeader {...getTool()} /> | ||||
| 
 | ||||
|       <div class="max-w-600px mx-auto px-6"> | ||||
|         <Card> | ||||
|           <CardHeader class="flex justify-between items-center"> | ||||
|             <div class="my-6 text-center"> | ||||
| 
 | ||||
|               <div class="text-base text-muted-foreground mb-2"> | ||||
|                 Random port: | ||||
|               </div> | ||||
| 
 | ||||
|               <div class="text-4xl font-mono"> | ||||
| 
 | ||||
|                 {getPort()} | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex gap-2 md:gap-4 mt-4 flex-col md:flex-row w-full justify-center"> | ||||
|               <Button onClick={refreshPort} variant="outline"> | ||||
|                 <div class="i-tabler-refresh mr-2 text-base text-muted-foreground" /> | ||||
|                 {t('refresh')} | ||||
|               </Button> | ||||
| 
 | ||||
|               <CopyButton textToCopy={getPort} toastMessage={t('copy-toast')} /> | ||||
|             </div> | ||||
|           </CardHeader> | ||||
|         </Card> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default RandomPortGenerator; | ||||
| @ -1,5 +0,0 @@ | ||||
| import { random } from 'lodash-es'; | ||||
| 
 | ||||
| export function generateRandomPort() { | ||||
|   return random(1024, 65535); | ||||
| } | ||||
| @ -1,9 +0,0 @@ | ||||
| import { defineTool } from '../../tools.models'; | ||||
| 
 | ||||
| export const randomPortGeneratorTool = defineTool({ | ||||
|   slug: 'random-port-generator', | ||||
|   entryFile: () => import('./random-port-generator.page'), | ||||
|   icon: 'i-tabler-server', | ||||
|   createdAt: new Date('2024-10-03'), | ||||
|   dirName: 'random-port-generator', | ||||
| }); | ||||
| @ -1,10 +0,0 @@ | ||||
| { | ||||
|   "name": "Token Generator", | ||||
|   "description": "Generate random string with the characters you want, uppercase, lowercase letters, numbers and/or symbols.", | ||||
|   "uppercase": "Uppercase letters (A-Z)", | ||||
|   "lowercase": "Lowercase letters (a-z)", | ||||
|   "numbers": "Numbers (0-9)", | ||||
|   "symbols": "Special characters (!@#...)", | ||||
|   "length": "Length", | ||||
|   "result-placeholder": "Your token will appear here" | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| { | ||||
|   "name": "Générateur de token", | ||||
|   "description": "Génère une chaîne de caractères aléatoire, contrôlez les caractères que vous voulez, lettres majuscules, minuscules, chiffres et/ou symboles.", | ||||
|   "uppercase": "Lettres majuscules (A-Z)", | ||||
|   "lowercase": "Lettres minuscules (a-z)", | ||||
|   "numbers": "Chiffres (0-9)", | ||||
|   "symbols": "Caractères spéciaux (!@#...)", | ||||
|   "length": "Longueur", | ||||
|   "result-placeholder": "Le token apparaîtra ici" | ||||
| } | ||||
| @ -1,28 +0,0 @@ | ||||
| import { sample as sampleImpl, times } from 'lodash-es'; | ||||
| 
 | ||||
| export function createToken({ | ||||
|   withUppercase = true, | ||||
|   withLowercase = true, | ||||
|   withNumbers = true, | ||||
|   withSymbols = false, | ||||
|   length = 64, | ||||
|   alphabet, | ||||
|   sample = sampleImpl, | ||||
| }: { | ||||
|   withUppercase?: boolean; | ||||
|   withLowercase?: boolean; | ||||
|   withNumbers?: boolean; | ||||
|   withSymbols?: boolean; | ||||
|   length?: number; | ||||
|   alphabet?: string; | ||||
|   sample?: (str: string) => string; | ||||
| }) { | ||||
|   const allAlphabet = alphabet ?? [ | ||||
|     withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : '', | ||||
|     withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '', | ||||
|     withNumbers ? '0123456789' : '', | ||||
|     withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '', | ||||
|   ].join(''); | ||||
| 
 | ||||
|   return times(length, () => sample(allAlphabet)).join(''); | ||||
| } | ||||
| @ -1,109 +0,0 @@ | ||||
| import { CopyButton } from '@/modules/shared/copy/copy-button'; | ||||
| import { createRefreshableSignal } from '@/modules/shared/signals'; | ||||
| import { Button } from '@/modules/ui/components/button'; | ||||
| import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/modules/ui/components/card'; | ||||
| import { Switch, SwitchControl, SwitchLabel, SwitchThumb } from '@/modules/ui/components/switch'; | ||||
| import { TextArea } from '@/modules/ui/components/textarea'; | ||||
| import { TextFieldRoot } from '@/modules/ui/components/textfield'; | ||||
| import { type Component, createSignal } from 'solid-js'; | ||||
| import { ToolHeader } from '../../components/tool-header'; | ||||
| import { useCurrentTool } from '../../tools.provider'; | ||||
| import defaultDictionary from './locales/en.json'; | ||||
| import { createToken } from './token-generator.models'; | ||||
| 
 | ||||
| const TokenGeneratorTool: Component = () => { | ||||
|   const [getUseUpperCase, setUseUpperCase] = createSignal(true); | ||||
|   const [getUseLowerCase, setUseLowerCase] = createSignal(true); | ||||
|   const [getUseNumbers, setUseNumbers] = createSignal(true); | ||||
|   const [getUseSpecialCharacters, setUseSpecialCharacters] = createSignal(false); | ||||
|   const [getLength] = createSignal(64); | ||||
| 
 | ||||
|   const { t, getTool } = useCurrentTool({ defaultDictionary }); | ||||
| 
 | ||||
|   const [getToken, refreshToken] = createRefreshableSignal(() => createToken({ | ||||
|     withUppercase: getUseUpperCase(), | ||||
|     withLowercase: getUseLowerCase(), | ||||
|     withNumbers: getUseNumbers(), | ||||
|     withSymbols: getUseSpecialCharacters(), | ||||
|     length: getLength(), | ||||
|   })); | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <ToolHeader {...getTool()} /> | ||||
| 
 | ||||
|       <div class="mx-auto max-w-1200px p-6 flex flex-col gap-4 md:flex-row items-start"> | ||||
|         <Card> | ||||
|           <CardHeader class="border-b border-border"> | ||||
|             <CardTitle class="text-muted-foreground"> | ||||
|               Configuration | ||||
|             </CardTitle> | ||||
|           </CardHeader> | ||||
| 
 | ||||
|           <CardContent class="pt-6 flex flex-col gap-2"> | ||||
|             <Switch class="flex items-center gap-2" checked={getUseUpperCase()} onChange={setUseUpperCase}> | ||||
|               <SwitchControl> | ||||
|                 <SwitchThumb /> | ||||
|               </SwitchControl> | ||||
|               <SwitchLabel class="text-sm font-medium leading-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70"> | ||||
|                 {t('uppercase')} | ||||
|               </SwitchLabel> | ||||
|             </Switch> | ||||
| 
 | ||||
|             <Switch class="flex items-center gap-2" checked={getUseLowerCase()} onChange={setUseLowerCase}> | ||||
|               <SwitchControl> | ||||
|                 <SwitchThumb /> | ||||
|               </SwitchControl> | ||||
|               <SwitchLabel class="text-sm font-medium leading-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70"> | ||||
|                 {t('lowercase')} | ||||
|               </SwitchLabel> | ||||
|             </Switch> | ||||
| 
 | ||||
|             <Switch class="flex items-center gap-2" checked={getUseNumbers()} onChange={setUseNumbers}> | ||||
|               <SwitchControl> | ||||
|                 <SwitchThumb /> | ||||
|               </SwitchControl> | ||||
|               <SwitchLabel class="text-sm font-medium leading-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70"> | ||||
|                 {t('numbers')} | ||||
|               </SwitchLabel> | ||||
|             </Switch> | ||||
| 
 | ||||
|             <Switch class="flex items-center gap-2" checked={getUseSpecialCharacters()} onChange={setUseSpecialCharacters}> | ||||
|               <SwitchControl> | ||||
|                 <SwitchThumb /> | ||||
|               </SwitchControl> | ||||
|               <SwitchLabel class="text-sm font-medium leading-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70"> | ||||
|                 {t('symbols')} | ||||
|               </SwitchLabel> | ||||
|             </Switch> | ||||
|           </CardContent> | ||||
| 
 | ||||
|         </Card> | ||||
| 
 | ||||
|         <Card class="flex-1"> | ||||
|           <CardHeader class="border-b border-border flex justify-between flex-row py-3 items-center"> | ||||
|             <CardTitle class="text-muted-foreground"> | ||||
|               Your token | ||||
|             </CardTitle> | ||||
| 
 | ||||
|             <div class="flex justify-center items-center gap-2"> | ||||
|               <Button onClick={refreshToken} variant="outline"> | ||||
|                 <div class="i-tabler-refresh mr-2 text-base text-muted-foreground" /> | ||||
|                 Refresh token | ||||
|               </Button> | ||||
| 
 | ||||
|               <CopyButton textToCopy={getToken} toastMessage={t('copy-toast')} /> | ||||
|             </div> | ||||
|           </CardHeader> | ||||
| 
 | ||||
|           <CardContent class="pt-6 text-center"> | ||||
|             {getToken()} | ||||
|           </CardContent> | ||||
| 
 | ||||
|         </Card> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default TokenGeneratorTool; | ||||
| @ -1,9 +0,0 @@ | ||||
| import { defineTool } from '../../tools.models'; | ||||
| 
 | ||||
| export const tokenGeneratorTool = defineTool({ | ||||
|   slug: 'token-generator', | ||||
|   entryFile: () => import('./token-generator.page'), | ||||
|   icon: 'i-tabler-key', | ||||
|   createdAt: new Date('2024-02-13'), | ||||
|   dirName: 'token-generator', | ||||
| }); | ||||
| @ -1,43 +0,0 @@ | ||||
| import { useI18n } from '@/modules/i18n/i18n.provider'; | ||||
| import { safely } from '@corentinth/chisels'; | ||||
| import { useParams } from '@solidjs/router'; | ||||
| import { type Component, createResource, lazy, Show } from 'solid-js'; | ||||
| import { CurrentToolProvider } from '../tools.provider'; | ||||
| import { getToolDefinitionBySlug } from '../tools.registry'; | ||||
| 
 | ||||
| export const ToolPage: Component = () => { | ||||
|   const params = useParams(); | ||||
|   const { getLocale, t } = useI18n(); | ||||
| 
 | ||||
|   const toolDefinition = getToolDefinitionBySlug({ slug: params.toolSlug }); | ||||
|   const ToolComponent = lazy(toolDefinition.entryFile); | ||||
| 
 | ||||
|   const [toolDict] = createResource(getLocale, async (locale) => { | ||||
|     const [dict, error] = await safely(import(`../definitions/${toolDefinition.dirName}/locales/${locale}.json`)); | ||||
| 
 | ||||
|     if (error) { | ||||
|       console.error(error); | ||||
|     } | ||||
| 
 | ||||
|     return dict ?? { default: {} }; | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <Show when={toolDict()}> | ||||
|       {toolLocaleDict => ( | ||||
|         <CurrentToolProvider | ||||
|           toolLocaleDict={toolLocaleDict} | ||||
|           tool={() => ({ | ||||
|             icon: toolDefinition.icon, | ||||
|             dirName: toolDefinition.dirName, | ||||
|             createdAt: toolDefinition.createdAt, | ||||
|             name: t(`tools.${toolDefinition.slug}.name` as any), | ||||
|             description: t(`tools.${toolDefinition.slug}.description` as any), | ||||
|           })} | ||||
|         > | ||||
|           <ToolComponent /> | ||||
|         </CurrentToolProvider> | ||||
|       )} | ||||
|     </Show> | ||||
|   ); | ||||
| }; | ||||
| @ -1,16 +0,0 @@ | ||||
| import type { Component } from 'solid-js'; | ||||
| 
 | ||||
| export { defineTool }; | ||||
| 
 | ||||
| function defineTool(toolDefinition: { | ||||
|   slug: string; | ||||
|   entryFile: () => Promise<{ default: Component }>; | ||||
|   dirName: string; | ||||
|   icon: string; | ||||
|   createdAt: Date; | ||||
| }) { | ||||
|   return { | ||||
|     ...toolDefinition, | ||||
|     key: toolDefinition.slug, | ||||
|   }; | ||||
| } | ||||
| @ -1,35 +0,0 @@ | ||||
| import type { Accessor, ParentComponent } from 'solid-js'; | ||||
| import type { ToolDefinition } from './tools.types'; | ||||
| import { flatten, translator } from '@solid-primitives/i18n'; | ||||
| import { merge } from 'lodash-es'; | ||||
| import { createContext, useContext } from 'solid-js'; | ||||
| 
 | ||||
| type ToolProviderContext = { | ||||
|   toolLocaleDict: Accessor<Record<string, string>>; | ||||
|   tool: Accessor<Pick<ToolDefinition, 'icon' | 'dirName' | 'createdAt'> & { name: string; description: string }>; | ||||
| }; | ||||
| 
 | ||||
| const CurrentToolContext = createContext<ToolProviderContext>(); | ||||
| 
 | ||||
| export function useCurrentTool<T>({ defaultDictionary }: { defaultDictionary: T }) { | ||||
|   const context = useContext(CurrentToolContext); | ||||
| 
 | ||||
|   if (!context) { | ||||
|     throw new Error('useCurrentTool must be used within a CurrentToolProvider'); | ||||
|   } | ||||
| 
 | ||||
|   const { toolLocaleDict, tool } = context; | ||||
| 
 | ||||
|   return { | ||||
|     t: translator(() => flatten(merge({}, defaultDictionary, toolLocaleDict()))), | ||||
|     getTool: tool, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export const CurrentToolProvider: ParentComponent<ToolProviderContext> = (props) => { | ||||
|   return ( | ||||
|     <CurrentToolContext.Provider value={props}> | ||||
|       {props.children} | ||||
|     </CurrentToolContext.Provider> | ||||
|   ); | ||||
| }; | ||||
| @ -1,17 +0,0 @@ | ||||
| import { keyBy, map } from 'lodash-es'; | ||||
| import { randomPortGeneratorTool } from './definitions/random-port-generator/random-port-generator.tool'; | ||||
| import { tokenGeneratorTool } from './definitions/token-generator/token-generator.tool'; | ||||
| 
 | ||||
| export const toolDefinitions = [ | ||||
|   tokenGeneratorTool, | ||||
|   randomPortGeneratorTool, | ||||
| ]; | ||||
| 
 | ||||
| export const toolSlugs = map(toolDefinitions, 'slug'); | ||||
| export const toolDefinitionBySlug = keyBy(toolDefinitions, 'slug'); | ||||
| 
 | ||||
| export { getToolDefinitionBySlug }; | ||||
| 
 | ||||
| function getToolDefinitionBySlug({ slug }: { slug: string }) { | ||||
|   return toolDefinitionBySlug[slug]; | ||||
| } | ||||
| @ -1,19 +0,0 @@ | ||||
| import { createMemo } from 'solid-js'; | ||||
| import { useI18n } from '../i18n/i18n.provider'; | ||||
| import { toolDefinitions } from './tools.registry'; | ||||
| 
 | ||||
| export { useToolsStore }; | ||||
| 
 | ||||
| function useToolsStore() { | ||||
|   const { t } = useI18n(); | ||||
| 
 | ||||
|   const getTools = createMemo(() => toolDefinitions.map((tool) => { | ||||
|     return { | ||||
|       ...tool, | ||||
|       name: t(`tools.${tool.slug}.name` as any) ?? tool.slug, | ||||
|       description: t(`tools.${tool.slug}.description` as any) ?? tool.slug, | ||||
|     }; | ||||
|   })); | ||||
| 
 | ||||
|   return { getTools }; | ||||
| } | ||||
| @ -1,6 +0,0 @@ | ||||
| import type { Flatten, Translator } from '@solid-primitives/i18n'; | ||||
| import type { defineTool } from './tools.models'; | ||||
| 
 | ||||
| export type ToolI18nFactory = <T extends Record<string, string>>(args: { defaultDictionary: T }) => { t: Translator<Flatten<T>> }; | ||||
| 
 | ||||
| export type ToolDefinition = ReturnType<typeof defineTool>; | ||||
| @ -1,37 +0,0 @@ | ||||
| import type { VariantProps } from 'class-variance-authority'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { cva } from 'class-variance-authority'; | ||||
| import { type ComponentProps, splitProps } from 'solid-js'; | ||||
| 
 | ||||
| export const badgeVariants = cva( | ||||
|   'inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-shadow focus-visible:(outline-none ring-1.5 ring-ring)', | ||||
|   { | ||||
|     variants: { | ||||
|       variant: { | ||||
|         default: 'bg-primary text-primary-foreground shadow hover:bg-primary/80', | ||||
|         secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', | ||||
|         destructive: 'bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', | ||||
|         outline: 'border text-foreground', | ||||
|       }, | ||||
|     }, | ||||
|     defaultVariants: { | ||||
|       variant: 'default', | ||||
|     }, | ||||
|   }, | ||||
| ); | ||||
| 
 | ||||
| export function Badge(props: ComponentProps<'div'> & VariantProps<typeof badgeVariants>) { | ||||
|   const [local, rest] = splitProps(props, ['class', 'variant']); | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       class={cn( | ||||
|         badgeVariants({ | ||||
|           variant: local.variant, | ||||
|         }), | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -1,60 +0,0 @@ | ||||
| import type { ButtonRootProps } from '@kobalte/core/button'; | ||||
| import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||
| import type { VariantProps } from 'class-variance-authority'; | ||||
| import type { ValidComponent } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { Button as ButtonPrimitive } from '@kobalte/core/button'; | ||||
| import { cva } from 'class-variance-authority'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| 
 | ||||
| export const buttonVariants = cva( | ||||
|   'inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:(outline-none ring-1.5 ring-ring) disabled:(pointer-events-none opacity-50) bg-inherit', | ||||
|   { | ||||
|     variants: { | ||||
|       variant: { | ||||
|         default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', | ||||
|         destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', | ||||
|         outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', | ||||
|         secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', | ||||
|         ghost: 'hover:(bg-accent text-accent-foreground)', | ||||
|         link: 'text-primary underline-offset-4 hover:underline', | ||||
|       }, | ||||
|       size: { | ||||
|         default: 'h-9 px-4 py-2', | ||||
|         sm: 'h-8 px-3 text-xs', | ||||
|         lg: 'h-10 px-8', | ||||
|         icon: 'h-9 w-9', | ||||
|       }, | ||||
|     }, | ||||
|     defaultVariants: { | ||||
|       variant: 'default', | ||||
|       size: 'default', | ||||
|     }, | ||||
|   }, | ||||
| ); | ||||
| 
 | ||||
| type buttonProps<T extends ValidComponent = 'button'> = ButtonRootProps<T> & | ||||
|   VariantProps<typeof buttonVariants> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function Button<T extends ValidComponent = 'button'>(props: PolymorphicProps<T, buttonProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as buttonProps, [ | ||||
|     'class', | ||||
|     'variant', | ||||
|     'size', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <ButtonPrimitive | ||||
|       class={cn( | ||||
|         buttonVariants({ | ||||
|           size: local.size, | ||||
|           variant: local.variant, | ||||
|         }), | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -1,60 +0,0 @@ | ||||
| import type { ComponentProps, ParentComponent } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| 
 | ||||
| export function Card(props: ComponentProps<'div'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       class={cn( | ||||
|         'rounded-xl border bg-card text-card-foreground shadow', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function CardHeader(props: ComponentProps<'div'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <div class={cn('flex flex-col space-y-1.5 p-6', local.class)} {...rest} /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export const CardTitle: ParentComponent<ComponentProps<'h1'>> = (props) => { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <h1 | ||||
|       class={cn('font-semibold leading-none tracking-tight', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const CardDescription: ParentComponent<ComponentProps<'h3'>> = ( | ||||
|   props, | ||||
| ) => { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <h3 class={cn('text-sm text-muted-foreground', local.class)} {...rest} /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export function CardContent(props: ComponentProps<'div'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return <div class={cn('p-6 pt-0', local.class)} {...rest} />; | ||||
| } | ||||
| 
 | ||||
| export function CardFooter(props: ComponentProps<'div'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <div class={cn('flex items-center p-6 pt-0', local.class)} {...rest} /> | ||||
|   ); | ||||
| } | ||||
| @ -1,151 +0,0 @@ | ||||
| import type { | ||||
|   CommandDialogProps, | ||||
|   CommandEmptyProps, | ||||
|   CommandGroupProps, | ||||
|   CommandInputProps, | ||||
|   CommandItemProps, | ||||
|   CommandListProps, | ||||
|   CommandRootProps, | ||||
| } from 'cmdk-solid'; | ||||
| import type { ComponentProps, VoidProps } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { Command as CommandPrimitive } from 'cmdk-solid'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| import { Dialog, DialogContent } from './dialog'; | ||||
| 
 | ||||
| export function Command(props: CommandRootProps) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <CommandPrimitive | ||||
|       class={cn( | ||||
|         'flex size-full flex-col overflow-hidden bg-popover text-popover-foreground', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function CommandList(props: CommandListProps) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <CommandPrimitive.List | ||||
|       class={cn( | ||||
|         'max-h-[300px] overflow-y-auto overflow-x-hidden p-1', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function CommandInput(props: VoidProps<CommandInputProps>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <div class="flex items-center border-b px-3" cmdk-input-wrapper=""> | ||||
|       <svg | ||||
|         xmlns="http://www.w3.org/2000/svg" | ||||
|         viewBox="0 0 24 24" | ||||
|         class="mr-2 h-4 w-4 shrink-0 opacity-50" | ||||
|       > | ||||
|         <path | ||||
|           fill="none" | ||||
|           stroke="currentColor" | ||||
|           stroke-linecap="round" | ||||
|           stroke-linejoin="round" | ||||
|           stroke-width="2" | ||||
|           d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0m18 11l-6-6" | ||||
|         /> | ||||
|         <title>Search</title> | ||||
|       </svg> | ||||
|       <CommandPrimitive.Input | ||||
|         class={cn( | ||||
|           'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:(cursor-not-allowed opacity-50)', | ||||
|           local.class, | ||||
|         )} | ||||
|         {...rest} | ||||
|       /> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function CommandItem(props: CommandItemProps) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <CommandPrimitive.Item | ||||
|       class={cn( | ||||
|         'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5! text-sm outline-none aria-selected:(bg-accent text-accent-foreground) aria-disabled:(pointer-events-none opacity-50)', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function CommandShortcut(props: ComponentProps<'span'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <span | ||||
|       class={cn( | ||||
|         'ml-auto text-xs tracking-widest text-muted-foreground', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function CommandDialog(props: CommandDialogProps) { | ||||
|   const [local, rest] = splitProps(props, ['children']); | ||||
| 
 | ||||
|   return ( | ||||
|     <Dialog {...rest}> | ||||
|       <DialogContent class="overflow-hidden p-0"> | ||||
|         <Command class="[&_[cmdk-group-heading]]:(px-2 font-medium text-muted-foreground) [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:(px-2 py-3) [&_[cmdk-item]_svg]:size-5"> | ||||
|           {local.children} | ||||
|         </Command> | ||||
|       </DialogContent> | ||||
|     </Dialog> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function CommandEmpty(props: CommandEmptyProps) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <CommandPrimitive.Empty | ||||
|       class={cn('py-6 text-center text-sm', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function CommandGroup(props: CommandGroupProps) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <CommandPrimitive.Group | ||||
|       class={cn( | ||||
|         'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:(px-2 py-1.5 text-xs font-medium text-muted-foreground)', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function CommandSeparator(props: CommandEmptyProps) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <CommandPrimitive.Separator | ||||
|       class={cn('-mx-1 h-px bg-border', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -1,122 +0,0 @@ | ||||
| import type { | ||||
|   DialogContentProps, | ||||
|   DialogDescriptionProps, | ||||
|   DialogTitleProps, | ||||
| } from '@kobalte/core/dialog'; | ||||
| import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||
| import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { Dialog as DialogPrimitive } from '@kobalte/core/dialog'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| 
 | ||||
| export const Dialog = DialogPrimitive; | ||||
| export const DialogTrigger = DialogPrimitive.Trigger; | ||||
| 
 | ||||
| type dialogContentProps<T extends ValidComponent = 'div'> = ParentProps< | ||||
|   DialogContentProps<T> & { | ||||
|     class?: string; | ||||
|   } | ||||
| >; | ||||
| 
 | ||||
| export function DialogContent<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dialogContentProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dialogContentProps, [ | ||||
|     'class', | ||||
|     'children', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DialogPrimitive.Portal> | ||||
|       <DialogPrimitive.Overlay | ||||
|         class={cn( | ||||
|           'fixed inset-0 z-50 bg-background/80 data-[expanded]:(animate-in fade-in-0) data-[closed]:(animate-out fade-out-0)', | ||||
|         )} | ||||
|         {...rest} | ||||
|       /> | ||||
|       <DialogPrimitive.Content | ||||
|         class={cn( | ||||
|           'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[expanded]:(animate-in fade-in-0 zoom-in-95 slide-in-from-left-1/2 slide-in-from-top-48% duration-200) data-[closed]:(animate-out fade-out-0 zoom-out-95 slide-out-to-left-1/2 slide-out-to-top-48% duration-200) md:w-full sm:rounded-lg', | ||||
|           local.class, | ||||
|         )} | ||||
|         {...rest} | ||||
|       > | ||||
|         {local.children} | ||||
|         <DialogPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:(outline-none ring-1.5 ring-ring ring-offset-2) disabled:pointer-events-none bg-inherit transition-property-[opacity,box-shadow]"> | ||||
|           <svg | ||||
|             xmlns="http://www.w3.org/2000/svg" | ||||
|             viewBox="0 0 24 24" | ||||
|             class="h-4 w-4" | ||||
|           > | ||||
|             <path | ||||
|               fill="none" | ||||
|               stroke="currentColor" | ||||
|               stroke-linecap="round" | ||||
|               stroke-linejoin="round" | ||||
|               stroke-width="2" | ||||
|               d="M18 6L6 18M6 6l12 12" | ||||
|             /> | ||||
|             <title>Close</title> | ||||
|           </svg> | ||||
|         </DialogPrimitive.CloseButton> | ||||
|       </DialogPrimitive.Content> | ||||
|     </DialogPrimitive.Portal> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type dialogTitleProps<T extends ValidComponent = 'h2'> = DialogTitleProps<T> & { | ||||
|   class?: string; | ||||
| }; | ||||
| 
 | ||||
| export function DialogTitle<T extends ValidComponent = 'h2'>(props: PolymorphicProps<T, dialogTitleProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dialogTitleProps, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <DialogPrimitive.Title | ||||
|       class={cn('text-lg font-semibold text-foreground', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type dialogDescriptionProps<T extends ValidComponent = 'p'> = | ||||
|   DialogDescriptionProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function DialogDescription<T extends ValidComponent = 'p'>(props: PolymorphicProps<T, dialogDescriptionProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dialogDescriptionProps, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <DialogPrimitive.Description | ||||
|       class={cn('text-sm text-muted-foreground', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function DialogHeader(props: ComponentProps<'div'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       class={cn( | ||||
|         'flex flex-col space-y-2 text-center sm:text-left', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function DialogFooter(props: ComponentProps<'div'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       class={cn( | ||||
|         'flex flex-col-reverse sm:(flex-row justify-end space-x-2)', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -1,286 +0,0 @@ | ||||
| import type { | ||||
|   DropdownMenuCheckboxItemProps, | ||||
|   DropdownMenuContentProps, | ||||
|   DropdownMenuGroupLabelProps, | ||||
|   DropdownMenuItemLabelProps, | ||||
|   DropdownMenuItemProps, | ||||
|   DropdownMenuRadioItemProps, | ||||
|   DropdownMenuRootProps, | ||||
|   DropdownMenuSeparatorProps, | ||||
|   DropdownMenuSubTriggerProps, | ||||
| } from '@kobalte/core/dropdown-menu'; | ||||
| import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||
| import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { DropdownMenu as DropdownMenuPrimitive } from '@kobalte/core/dropdown-menu'; | ||||
| import { mergeProps, splitProps } from 'solid-js'; | ||||
| 
 | ||||
| export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; | ||||
| export const DropdownMenuGroup = DropdownMenuPrimitive.Group; | ||||
| export const DropdownMenuSub = DropdownMenuPrimitive.Sub; | ||||
| export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; | ||||
| 
 | ||||
| export function DropdownMenu(props: DropdownMenuRootProps) { | ||||
|   const merge = mergeProps<DropdownMenuRootProps[]>({ gutter: 4 }, props); | ||||
| 
 | ||||
|   return <DropdownMenuPrimitive {...merge} />; | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuContentProps<T extends ValidComponent = 'div'> = | ||||
|   DropdownMenuContentProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function DropdownMenuContent<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuContentProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuContentProps, [ | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.Portal> | ||||
|       <DropdownMenuPrimitive.Content | ||||
|         class={cn( | ||||
|           'min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95) focus-visible:(outline-none ring-1.5 ring-ring) transition-shadow', | ||||
|           local.class, | ||||
|         )} | ||||
|         {...rest} | ||||
|       /> | ||||
|     </DropdownMenuPrimitive.Portal> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuItemProps<T extends ValidComponent = 'div'> = | ||||
|   DropdownMenuItemProps<T> & { | ||||
|     class?: string; | ||||
|     inset?: boolean; | ||||
|   }; | ||||
| 
 | ||||
| export function DropdownMenuItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuItemProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuItemProps, [ | ||||
|     'class', | ||||
|     'inset', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.Item | ||||
|       class={cn( | ||||
|         'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)', | ||||
|         local.inset && 'pl-8', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuGroupLabelProps<T extends ValidComponent = 'span'> = | ||||
|   DropdownMenuGroupLabelProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function DropdownMenuGroupLabel<T extends ValidComponent = 'span'>(props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [ | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.GroupLabel | ||||
|       as="div" | ||||
|       class={cn('px-2 py-1.5 text-sm font-semibold', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuItemLabelProps<T extends ValidComponent = 'div'> = | ||||
|   DropdownMenuItemLabelProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function DropdownMenuItemLabel<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [ | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.ItemLabel | ||||
|       as="div" | ||||
|       class={cn('px-2 py-1.5 text-sm font-semibold', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuSeparatorProps<T extends ValidComponent = 'hr'> = | ||||
|   DropdownMenuSeparatorProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function DropdownMenuSeparator<T extends ValidComponent = 'hr'>(props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [ | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.Separator | ||||
|       class={cn('-mx-1 my-1 h-px bg-muted', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function DropdownMenuShortcut(props: ComponentProps<'span'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <span | ||||
|       class={cn('ml-auto text-xs tracking-widest opacity-60', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuSubTriggerProps<T extends ValidComponent = 'div'> = ParentProps<DropdownMenuSubTriggerProps<T> & { class?: string }>; | ||||
| 
 | ||||
| export function DropdownMenuSubTrigger<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [ | ||||
|     'class', | ||||
|     'children', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.SubTrigger | ||||
|       class={cn( | ||||
|         'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[expanded]:bg-accent', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     > | ||||
|       {local.children} | ||||
|       <svg | ||||
|         xmlns="http://www.w3.org/2000/svg" | ||||
|         width="1em" | ||||
|         height="1em" | ||||
|         viewBox="0 0 24 24" | ||||
|         class="ml-auto h-4 w-4" | ||||
|       > | ||||
|         <path | ||||
|           fill="none" | ||||
|           stroke="currentColor" | ||||
|           stroke-linecap="round" | ||||
|           stroke-linejoin="round" | ||||
|           stroke-width="2" | ||||
|           d="m9 6l6 6l-6 6" | ||||
|         /> | ||||
|         <title>Arrow</title> | ||||
|       </svg> | ||||
|     </DropdownMenuPrimitive.SubTrigger> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuSubContentProps<T extends ValidComponent = 'div'> = | ||||
|   DropdownMenuSubTriggerProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function DropdownMenuSubContent<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [ | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.Portal> | ||||
|       <DropdownMenuPrimitive.SubContent | ||||
|         class={cn( | ||||
|           'min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95)', | ||||
|           local.class, | ||||
|         )} | ||||
|         {...rest} | ||||
|       /> | ||||
|     </DropdownMenuPrimitive.Portal> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuCheckboxItemProps<T extends ValidComponent = 'div'> = ParentProps<DropdownMenuCheckboxItemProps<T> & { class?: string }>; | ||||
| 
 | ||||
| export function DropdownMenuCheckboxItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [ | ||||
|     'class', | ||||
|     'children', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.CheckboxItem | ||||
|       class={cn( | ||||
|         'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     > | ||||
|       <DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center"> | ||||
|         <svg | ||||
|           xmlns="http://www.w3.org/2000/svg" | ||||
|           viewBox="0 0 24 24" | ||||
|           class="h-4 w-4" | ||||
|         > | ||||
|           <path | ||||
|             fill="none" | ||||
|             stroke="currentColor" | ||||
|             stroke-linecap="round" | ||||
|             stroke-linejoin="round" | ||||
|             stroke-width="2" | ||||
|             d="m5 12l5 5L20 7" | ||||
|           /> | ||||
|           <title>Checkbox</title> | ||||
|         </svg> | ||||
|       </DropdownMenuPrimitive.ItemIndicator> | ||||
|       {props.children} | ||||
|     </DropdownMenuPrimitive.CheckboxItem> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuRadioItemProps<T extends ValidComponent = 'div'> = ParentProps< | ||||
|   DropdownMenuRadioItemProps<T> & { | ||||
|     class?: string; | ||||
|   } | ||||
| >; | ||||
| 
 | ||||
| export function DropdownMenuRadioItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [ | ||||
|     'class', | ||||
|     'children', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.RadioItem | ||||
|       class={cn( | ||||
|         'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     > | ||||
|       <DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center"> | ||||
|         <svg | ||||
|           xmlns="http://www.w3.org/2000/svg" | ||||
|           viewBox="0 0 24 24" | ||||
|           class="h-2 w-2" | ||||
|         > | ||||
|           <g | ||||
|             fill="none" | ||||
|             stroke-linecap="round" | ||||
|             stroke-linejoin="round" | ||||
|             stroke-width="2" | ||||
|           > | ||||
|             <path d="M0 0h24v24H0z" /> | ||||
|             <path | ||||
|               fill="currentColor" | ||||
|               d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34" | ||||
|             /> | ||||
|           </g> | ||||
|           <title>Radio</title> | ||||
|         </svg> | ||||
|       </DropdownMenuPrimitive.ItemIndicator> | ||||
|       {props.children} | ||||
|     </DropdownMenuPrimitive.RadioItem> | ||||
|   ); | ||||
| } | ||||
| @ -1,20 +0,0 @@ | ||||
| import { Toaster as Sonner, toast } from 'solid-sonner'; | ||||
| 
 | ||||
| export { toast }; | ||||
| 
 | ||||
| export function Toaster(props: Parameters<typeof Sonner>[0]) { | ||||
|   return ( | ||||
|     <Sonner | ||||
|       class="toaster group" | ||||
|       toastOptions={{ | ||||
|         classes: { | ||||
|           toast: 'group toast group-[.toaster]:(bg-background text-foreground border-border shadow-lg)', | ||||
|           description: 'group-[.toast]:text-muted-foreground', | ||||
|           actionButton: 'group-[.toast]:(bg-primary text-primary-foreground)', | ||||
|           cancelButton: 'group-[.toast]:(bg-muted text-muted-foreground)', | ||||
|         }, | ||||
|       }} | ||||
|       {...props} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -1,54 +0,0 @@ | ||||
| import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||
| import type { | ||||
|   SwitchControlProps, | ||||
|   SwitchThumbProps, | ||||
| } from '@kobalte/core/switch'; | ||||
| import type { ParentProps, ValidComponent, VoidProps } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { Switch as SwitchPrimitive } from '@kobalte/core/switch'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| 
 | ||||
| export const SwitchLabel = SwitchPrimitive.Label; | ||||
| export const Switch = SwitchPrimitive; | ||||
| export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage; | ||||
| export const SwitchDescription = SwitchPrimitive.Description; | ||||
| 
 | ||||
| type switchControlProps<T extends ValidComponent = 'input'> = ParentProps<SwitchControlProps<T> & { class?: string }>; | ||||
| 
 | ||||
| export function SwitchControl<T extends ValidComponent = 'input'>(props: PolymorphicProps<T, switchControlProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as switchControlProps, [ | ||||
|     'class', | ||||
|     'children', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <SwitchPrimitive.Input class="[&:focus-visible+div]:(outline-none ring-1.5 ring-ring ring-offset-2 ring-offset-background)" /> | ||||
|       <SwitchPrimitive.Control | ||||
|         class={cn( | ||||
|           'inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input shadow-sm transition-shadow data-[disabled]:(cursor-not-allowed opacity-50) data-[checked]:bg-primary transition-property-[box-shadow,color,background-color]', | ||||
|           local.class, | ||||
|         )} | ||||
|         {...rest} | ||||
|       > | ||||
|         {local.children} | ||||
|       </SwitchPrimitive.Control> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type switchThumbProps<T extends ValidComponent = 'div'> = VoidProps<SwitchThumbProps<T> & { class?: string }>; | ||||
| 
 | ||||
| export function SwitchThumb<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, switchThumbProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as switchThumbProps, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <SwitchPrimitive.Thumb | ||||
|       class={cn( | ||||
|         'pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg transition-transform data-[checked]:translate-x-4', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -1,26 +0,0 @@ | ||||
| import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||
| import type { TextFieldTextAreaProps } from '@kobalte/core/text-field'; | ||||
| import type { ValidComponent, VoidProps } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { TextArea as TextFieldPrimitive } from '@kobalte/core/text-field'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| 
 | ||||
| type textAreaProps<T extends ValidComponent = 'textarea'> = VoidProps< | ||||
|   TextFieldTextAreaProps<T> & { | ||||
|     class?: string; | ||||
|   } | ||||
| >; | ||||
| 
 | ||||
| export function TextArea<T extends ValidComponent = 'textarea'>(props: PolymorphicProps<T, textAreaProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textAreaProps, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <TextFieldPrimitive | ||||
|       class={cn( | ||||
|         'flex min-h-[60px] w-full rounded-md border border-input bg-inherit px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -1,116 +0,0 @@ | ||||
| import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||
| import type { | ||||
|   TextFieldDescriptionProps, | ||||
|   TextFieldErrorMessageProps, | ||||
|   TextFieldInputProps, | ||||
|   TextFieldLabelProps, | ||||
|   TextFieldRootProps, | ||||
| } from '@kobalte/core/text-field'; | ||||
| import type { ValidComponent, VoidProps } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { TextField as TextFieldPrimitive } from '@kobalte/core/text-field'; | ||||
| import { cva } from 'class-variance-authority'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| 
 | ||||
| type textFieldProps<T extends ValidComponent = 'div'> = | ||||
|   TextFieldRootProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function TextFieldRoot<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textFieldProps, ['class']); | ||||
| 
 | ||||
|   return <TextFieldPrimitive class={cn('space-y-1', local.class)} {...rest} />; | ||||
| } | ||||
| 
 | ||||
| export const textfieldLabel = cva( | ||||
|   'text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium', | ||||
|   { | ||||
|     variants: { | ||||
|       label: { | ||||
|         true: 'data-[invalid]:text-destructive', | ||||
|       }, | ||||
|       error: { | ||||
|         true: 'text-destructive text-xs', | ||||
|       }, | ||||
|       description: { | ||||
|         true: 'font-normal text-muted-foreground', | ||||
|       }, | ||||
|     }, | ||||
|     defaultVariants: { | ||||
|       label: true, | ||||
|     }, | ||||
|   }, | ||||
| ); | ||||
| 
 | ||||
| type textFieldLabelProps<T extends ValidComponent = 'label'> = | ||||
|   TextFieldLabelProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function TextFieldLabel<T extends ValidComponent = 'label'>(props: PolymorphicProps<T, textFieldLabelProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textFieldLabelProps, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <TextFieldPrimitive.Label | ||||
|       class={cn(textfieldLabel(), local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type textFieldErrorMessageProps<T extends ValidComponent = 'div'> = | ||||
|   TextFieldErrorMessageProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function TextFieldErrorMessage<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldErrorMessageProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textFieldErrorMessageProps, [ | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <TextFieldPrimitive.ErrorMessage | ||||
|       class={cn(textfieldLabel({ error: true }), local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type textFieldDescriptionProps<T extends ValidComponent = 'div'> = | ||||
|   TextFieldDescriptionProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export function TextFieldDescription<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldDescriptionProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textFieldDescriptionProps, [ | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <TextFieldPrimitive.Description | ||||
|       class={cn(textfieldLabel({ description: true }), local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| type textFieldInputProps<T extends ValidComponent = 'input'> = VoidProps< | ||||
|   TextFieldInputProps<T> & { | ||||
|     class?: string; | ||||
|   } | ||||
| >; | ||||
| 
 | ||||
| export function TextField<T extends ValidComponent = 'input'>(props: PolymorphicProps<T, textFieldInputProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textFieldInputProps, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <TextFieldPrimitive.Input | ||||
|       class={cn( | ||||
|         'flex h-9 w-full rounded-md border border-input bg-inherit px-3 py-1 text-sm shadow-sm file:(border-0 bg-transparent text-sm font-medium) placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -1,279 +0,0 @@ | ||||
| import type { Component, ParentComponent } from 'solid-js'; | ||||
| import { useCommandPalette } from '@/modules/command-palette/command-palette.provider'; | ||||
| import { useI18n } from '@/modules/i18n/i18n.provider'; | ||||
| import { Button } from '@/modules/ui/components/button'; | ||||
| import { A } from '@solidjs/router'; | ||||
| import { Badge } from '../components/badge'; | ||||
| import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../components/dropdown-menu'; | ||||
| import { useThemeStore } from '../themes/theme.store'; | ||||
| import { cn } from '../utils/cn'; | ||||
| import { socialLinks } from './app.layouts.constants'; | ||||
| 
 | ||||
| const ThemeSwitcher: Component = () => { | ||||
|   const themeStore = useThemeStore(); | ||||
|   const { t } = useI18n(); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <DropdownMenuItem onClick={() => themeStore.setColorMode({ mode: 'light' })} class="flex items-center gap-2 cursor-pointer"> | ||||
|         <div class="i-tabler-sun text-lg"></div> | ||||
|         {t('navbar.theme.light-mode')} | ||||
|       </DropdownMenuItem> | ||||
|       <DropdownMenuItem onClick={() => themeStore.setColorMode({ mode: 'dark' })} class="flex items-center gap-2 cursor-pointer"> | ||||
|         <div class="i-tabler-moon text-lg"></div> | ||||
|         {t('navbar.theme.dark-mode')} | ||||
|       </DropdownMenuItem> | ||||
|       <DropdownMenuItem onClick={() => themeStore.setColorMode({ mode: 'system' })} class="flex items-center gap-2 cursor-pointer"> | ||||
|         <div class="i-tabler-device-laptop text-lg"></div> | ||||
|         {t('navbar.theme.system-mode')} | ||||
|       </DropdownMenuItem> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const LanguageSwitcher: Component = () => { | ||||
|   const { t, getLocale, changeLocale, locales } = useI18n(); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {locales.map(locale => ( | ||||
|         <DropdownMenuItem onClick={() => changeLocale(locale.key)} class={cn('flex items-center gap-2 cursor-pointer', { 'font-semibold': getLocale() === locale.key })}> | ||||
|           {locale.name} | ||||
|         </DropdownMenuItem> | ||||
|       ))} | ||||
| 
 | ||||
|       <DropdownMenuSeparator /> | ||||
| 
 | ||||
|       <DropdownMenuItem as="a" class="flex items-center gap-2 cursor-pointer" target="_blank" rel="noopener noreferrer" href="https://github.com/CorentinTh/it-tools"> | ||||
|         {t('navbar.contribute-to-i18n')} | ||||
|       </DropdownMenuItem> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const Navbar: Component = () => { | ||||
|   const themeStore = useThemeStore(); | ||||
|   const { t } = useI18n(); | ||||
|   const { openCommandPalette } = useCommandPalette(); | ||||
|   const getIsMacOs = () => navigator?.userAgent?.match(/Macintosh;/); | ||||
| 
 | ||||
|   return ( | ||||
|     <div class="border-b border-border bg-surface"> | ||||
|       <div class="flex items-center justify-between px-6 py-3 mx-auto max-w-1200px"> | ||||
|         <div class="flex items-center gap-4"> | ||||
|           <Button variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250" as={A} href="/" aria-label="Home"> | ||||
|             <span class="font-bold text-foreground">IT</span> | ||||
|             <span class="text-80% font-extrabold border border-2px leading-none border-current rounded-md px-1 py-0.5 ml-1 text-primary">TOOLS</span> | ||||
|           </Button> | ||||
| 
 | ||||
|           <Button size="sm" variant="outline" class="bg-card transition flex items-center gap-2 text-muted-foreground" onClick={openCommandPalette}> | ||||
|             <div class="i-tabler-search text-base"></div> | ||||
|             {t('commandPalette.trigger.search')} | ||||
|             <Badge variant="secondary" class="text-muted-foreground text-10px!"> | ||||
|               {getIsMacOs() ? '⌘ + K' : 'Ctrl + K'} | ||||
|             </Badge> | ||||
|           </Button> | ||||
|         </div> | ||||
| 
 | ||||
|         <div> | ||||
| 
 | ||||
|           <Button variant="ghost" class="text-lg px-0 size-9 hidden md:inline-flex" as={A} href="https://github.com/CorentinTh/enclosed" target="_blank" rel="noopener noreferrer" aria-label="GitHub repository"> | ||||
|             <div class="i-tabler-brand-github"></div> | ||||
|           </Button> | ||||
| 
 | ||||
|           <DropdownMenu> | ||||
|             <DropdownMenuTrigger as={Button} class="text-lg px-0 size-9 hidden md:inline-flex" variant="ghost" aria-label="Change theme"> | ||||
|               <div classList={{ 'i-tabler-moon': themeStore.getColorMode() === 'dark', 'i-tabler-sun': themeStore.getColorMode() === 'light' }}></div> | ||||
|             </DropdownMenuTrigger> | ||||
|             <DropdownMenuContent class="w-42"> | ||||
|               <ThemeSwitcher /> | ||||
|             </DropdownMenuContent> | ||||
|           </DropdownMenu> | ||||
| 
 | ||||
|           <DropdownMenu> | ||||
|             <DropdownMenuTrigger as={Button} class="text-lg px-0 size-9 hidden md:inline-flex" variant="ghost" aria-label="Language"> | ||||
|               <div class="i-custom-language size-4"></div> | ||||
|             </DropdownMenuTrigger> | ||||
|             <DropdownMenuContent> | ||||
|               <LanguageSwitcher /> | ||||
|             </DropdownMenuContent> | ||||
|           </DropdownMenu> | ||||
| 
 | ||||
|           <DropdownMenu> | ||||
| 
 | ||||
|             <DropdownMenuTrigger as={Button} class="text-lg px-0 size-9" variant="ghost" aria-label="Menu icon"> | ||||
|               <div class="i-tabler-dots-vertical hidden md:block"></div> | ||||
|               <div class="i-tabler-menu-2 block md:hidden"></div> | ||||
|             </DropdownMenuTrigger> | ||||
| 
 | ||||
|             <DropdownMenuContent class="w-46"> | ||||
| 
 | ||||
|               {/* Mobile only items */} | ||||
|               <DropdownMenuItem as="a" class="flex items-center gap-2 cursor-pointer md:hidden" target="_blank" href="https://github.com/CorentinTh/enclosed" rel="noopener noreferrer"> | ||||
|                 <div class="i-tabler-brand-github text-lg"></div> | ||||
|                 {t('navbar.github')} | ||||
|               </DropdownMenuItem> | ||||
| 
 | ||||
|               <DropdownMenuSub> | ||||
|                 <DropdownMenuSubTrigger as="a" class="flex items-center gap-2 md:hidden" aria-label="Change theme"> | ||||
|                   <div class="text-lg" classList={{ 'i-tabler-moon': themeStore.getColorMode() === 'dark', 'i-tabler-sun': themeStore.getColorMode() === 'light' }}></div> | ||||
|                   {t('navbar.theme.theme')} | ||||
|                 </DropdownMenuSubTrigger> | ||||
| 
 | ||||
|                 <DropdownMenuSubContent> | ||||
|                   <ThemeSwitcher /> | ||||
|                 </DropdownMenuSubContent> | ||||
| 
 | ||||
|               </DropdownMenuSub> | ||||
| 
 | ||||
|               <DropdownMenuSub> | ||||
|                 <DropdownMenuSubTrigger as="a" class="flex items-center text-medium gap-2 md:hidden" aria-label="Change language"> | ||||
|                   <div class="i-custom-language size-4"></div> | ||||
|                   {t('navbar.language')} | ||||
|                 </DropdownMenuSubTrigger> | ||||
|                 <DropdownMenuSubContent> | ||||
|                   <LanguageSwitcher /> | ||||
|                 </DropdownMenuSubContent> | ||||
|               </DropdownMenuSub> | ||||
| 
 | ||||
|               {/* Default items */} | ||||
| 
 | ||||
|               <DropdownMenuItem as="a" class="flex items-center gap-2 cursor-pointer" target="_blank" href="https://github.com/CorentinTh/it-tools/issues/new/choose" rel="noopener noreferrer"> | ||||
|                 <div class="i-tabler-bug text-lg"></div> | ||||
|                 {t('navbar.report-bug')} | ||||
|               </DropdownMenuItem> | ||||
| 
 | ||||
|               <DropdownMenuItem as="a" class="flex items-center gap-2 cursor-pointer" target="_blank" href="https://buymeacoffee.com/cthmsst" rel="noopener noreferrer"> | ||||
|                 <div class="i-tabler-pig-money text-lg"></div> | ||||
|                 {t('navbar.support')} | ||||
|               </DropdownMenuItem> | ||||
| 
 | ||||
|             </DropdownMenuContent> | ||||
| 
 | ||||
|           </DropdownMenu> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const Footer: Component = () => { | ||||
|   const { t, createLocalizedUrl } = useI18n(); | ||||
| 
 | ||||
|   const getFooterSections = () => [ | ||||
|     { | ||||
|       title: t('footer.resources.title'), | ||||
|       items: [ | ||||
|         { label: t('footer.resources.all-tools'), to: createLocalizedUrl({ path: '/tools' }) }, | ||||
|         { label: t('footer.resources.github'), href: 'https://github.com/CorentinTh/it-tools' }, | ||||
|         { label: t('footer.resources.support'), href: 'https://buymeacoffee.com/cthmsst' }, | ||||
|         { label: 'Humans.txt', href: '/humans.txt' }, | ||||
|         { label: t('footer.resources.license'), href: 'https://github.com/CorentinTh/it-tools/blob/main/LICENSE' }, | ||||
|       ], | ||||
|     }, | ||||
|     { | ||||
|       title: t('footer.support.title'), | ||||
|       items: [ | ||||
|         { label: t('footer.support.report-bug'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' }, | ||||
|         { label: t('footer.support.request-feature'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' }, | ||||
|         { label: t('footer.support.contribute'), href: 'https://github.com/CorentinTh/it-tools/blob/main/CONTRIBUTING.md' }, | ||||
|         { label: t('footer.support.contact'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' }, | ||||
|       ], | ||||
|     }, | ||||
|     { | ||||
|       title: t('footer.friends.title'), | ||||
|       items: [ | ||||
|         { label: 'Jugly.io', href: 'https://jugly.io' }, | ||||
|         { label: 'Enclosed.cc', href: 'https://enclosed.cc' }, | ||||
|       ], | ||||
|     }, | ||||
| 
 | ||||
|   ]; | ||||
| 
 | ||||
|   return ( | ||||
|     <footer class="bg-card border-t border-border"> | ||||
|       <div class="py-12 px-6 mx-auto max-w-1200px"> | ||||
| 
 | ||||
|         <div class="flex items-start justify-between flex-col md:flex-row gap-12"> | ||||
|           <div> | ||||
|             <div class="flex items-center gap-2"> | ||||
|               <A href="/" class="text-2xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250 group text-muted-foreground flex items-center gap-1"> | ||||
|                 <span class="font-bold group-hover:text-foreground transition">IT</span> | ||||
|                 <span class="text-80% font-extrabold border border-2px leading-none border-current rounded-md px-1 py-0.5 ml-1 group-hover:text-primary transition">TOOLS</span> | ||||
|               </A> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex items-center gap-2 mt-4"> | ||||
|               {socialLinks.map(({ icon, href, label }) => ( | ||||
|                 <a href={href} target="_blank" rel="noopener noreferrer" class="text-2xl text-muted-foreground hover:text-primary transition" aria-label={label}> | ||||
|                   <div class={icon}></div> | ||||
|                 </a> | ||||
|               ))} | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="text-muted-foreground mt-2"> | ||||
|               Crafted with | ||||
|               {' '} | ||||
|               <span class="i-tabler-heart inline-block text-base mb--0.5"></span> | ||||
|               {' '} | ||||
|               by | ||||
|               {' '} | ||||
|               <a href="https://corentin.tech" target="_blank" rel="noopener" class="hover:text-primary transition"> | ||||
|                 Corentin Thomasset | ||||
|               </a> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="grid grid-cols-1 sm:grid-cols-3 gap-12"> | ||||
|             {getFooterSections().map(({ title, items }) => ( | ||||
|               <div> | ||||
|                 <h4 class="font-semibold text-foreground">{title}</h4> | ||||
|                 <ul class="mt-4"> | ||||
|                   {items.map(({ label, to, href }) => ( | ||||
|                     <li class="mt-1"> | ||||
|                       {to | ||||
|                         ? ( | ||||
|                             <A href={to} class="text-muted-foreground hover:text-primary transition"> | ||||
|                               {label} | ||||
|                             </A> | ||||
|                           ) | ||||
|                         : ( | ||||
|                             <a href={href} target="_blank" rel="noopener" class="text-muted-foreground hover:text-primary transition"> | ||||
|                               {label} | ||||
|                             </a> | ||||
|                           )} | ||||
|                     </li> | ||||
|                   ))} | ||||
|                 </ul> | ||||
|               </div> | ||||
|             ))} | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="text-xs text-muted-foreground border-t border-border pt-4 mt-12"> | ||||
|           <span> | ||||
|             © | ||||
|             {new Date().getFullYear()} | ||||
|             {' '} | ||||
|             Corentin Thomasset | ||||
|           </span> | ||||
|         </div> | ||||
|         <div class="text-xs text-foreground opacity-80%"> | ||||
|         </div> | ||||
|       </div> | ||||
|     </footer> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const AppLayout: ParentComponent = (props) => { | ||||
|   return ( | ||||
|     <div class="flex flex-col h-screen min-h-0"> | ||||
| 
 | ||||
|       <Navbar /> | ||||
| 
 | ||||
|       <div class="flex-1 pb-20 ">{props.children}</div> | ||||
| 
 | ||||
|       <Footer /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @ -1,17 +0,0 @@ | ||||
| export const socialLinks = [ | ||||
|   { | ||||
|     icon: 'i-tabler-brand-github', | ||||
|     href: 'https://github.com/CorentinTh/it-tools', | ||||
|     label: 'GitHub', | ||||
|   }, | ||||
|   { | ||||
|     icon: 'i-tabler-brand-x', | ||||
|     href: 'https://x.com/ittoolsdottech', | ||||
|     label: 'X', | ||||
|   }, | ||||
|   { | ||||
|     icon: 'i-tabler-coffee', | ||||
|     href: 'https://buymeacoffee.com/cthmsst', | ||||
|     label: 'Support the project', | ||||
|   }, | ||||
| ]; | ||||
| @ -1,11 +0,0 @@ | ||||
| import type { ConfigColorMode } from '@kobalte/core/color-mode'; | ||||
| import { useColorMode } from '@kobalte/core/color-mode'; | ||||
| 
 | ||||
| export function useThemeStore() { | ||||
|   const { setColorMode, colorMode: getColorMode } = useColorMode(); | ||||
| 
 | ||||
|   return { | ||||
|     setColorMode: ({ mode }: { mode: ConfigColorMode }) => setColorMode(mode), | ||||
|     getColorMode, | ||||
|   }; | ||||
| } | ||||
| @ -1,5 +0,0 @@ | ||||
| import type { ClassValue } from 'clsx'; | ||||
| import clsx from 'clsx'; | ||||
| import { twMerge } from 'tailwind-merge'; | ||||
| 
 | ||||
| export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists)); | ||||
| @ -1,12 +0,0 @@ | ||||
| --- | ||||
| to: src/modules/tools/definitions/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.tool.ts | ||||
| --- | ||||
| import { defineTool } from '../../tools.models' | ||||
| 
 | ||||
| export const <%= h.changeCase.camel(name) %>Tool = defineTool({ | ||||
|   slug: '<%= h.changeCase.param(name) %>', | ||||
|   entryFile: () => import('./<%= h.changeCase.param(name) %>.page'), | ||||
|   icon: 'i-tabler-question-mark', | ||||
|   createdAt: new Date('<%= new Date().toISOString().split('T')[0] %>'), | ||||
|   dirName: '<%= h.changeCase.param(name) %>', | ||||
| }) | ||||
| @ -1,4 +0,0 @@ | ||||
| --- | ||||
| to: src/modules/tools/definitions/<%= h.changeCase.param(name) %>/locales/en.json | ||||
| --- | ||||
| {} | ||||
| @ -1,6 +0,0 @@ | ||||
| --- | ||||
| inject: true | ||||
| to: src/modules/tools/tools.registry.ts | ||||
| at_line: 0 | ||||
| --- | ||||
| import { <%= h.changeCase.camel(name) %>Tool } from './definitions/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.tool'; | ||||
| @ -1,6 +0,0 @@ | ||||
| --- | ||||
| inject: true | ||||
| to: src/modules/tools/tools.registry.ts | ||||
| before: "^]" | ||||
| --- | ||||
|   <%= h.changeCase.camel(name) %>Tool, | ||||
| @ -1,14 +0,0 @@ | ||||
| --- | ||||
| to: src/modules/tools/definitions/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.page.tsx | ||||
| --- | ||||
| import type { Component } from 'solid-js'; | ||||
| 
 | ||||
| const <%= h.changeCase.pascal(name) %>: Component = () => { | ||||
|   return ( | ||||
|     <div class="mx-auto max-w-1200px p-6"> | ||||
|       <h1><%= h.changeCase.title(name) %></h1> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export default <%= h.changeCase.pascal(name) %>; | ||||
| @ -1,22 +0,0 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "ESNext", | ||||
|     "jsx": "preserve", | ||||
|     "jsxImportSource": "solid-js", | ||||
|     "baseUrl": "./", | ||||
|     "module": "ESNext", | ||||
|     "moduleResolution": "node", | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"] | ||||
|     }, | ||||
|     "resolveJsonModule": true, | ||||
|     "types": ["vite/client"], | ||||
|     "strict": true, | ||||
|     "noEmit": true, | ||||
|     "allowSyntheticDefaultImports": true, | ||||
|     "esModuleInterop": true, | ||||
|     "isolatedModules": true, | ||||
|     "skipLibCheck": true | ||||
|   }, | ||||
|   "include": ["src"] | ||||
| } | ||||
| @ -1,120 +0,0 @@ | ||||
| import { | ||||
|   defineConfig, | ||||
|   presetIcons, | ||||
|   presetUno, | ||||
|   presetWebFonts, | ||||
|   transformerDirectives, | ||||
|   transformerVariantGroup, | ||||
| } from 'unocss'; | ||||
| import presetAnimations from 'unocss-preset-animations'; | ||||
| import { toolDefinitions } from './src/modules/tools/tools.registry'; | ||||
| import { socialLinks } from './src/modules/ui/layouts/app.layouts.constants'; | ||||
| 
 | ||||
| export default defineConfig({ | ||||
|   presets: [ | ||||
|     presetUno({ | ||||
|       dark: { | ||||
|         dark: '[data-kb-theme="dark"]', | ||||
|         light: '[data-kb-theme="light"]', | ||||
|       }, | ||||
|       prefix: '', | ||||
|     }), | ||||
|     presetAnimations(), | ||||
|     presetWebFonts({ | ||||
|       fonts: { | ||||
|         sans: 'Inter:400,500,600,700,800,900', | ||||
|       }, | ||||
|     }), | ||||
|     presetIcons({ | ||||
|       collections: { | ||||
|         custom: { | ||||
|           language: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><path fill="currentColor" d="m478.33 433.6l-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4ZM334.83 362L368 281.65L401.17 362Zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73c39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36c-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93c.92 1.19 1.83 2.35 2.74 3.51c-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59c22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9Z" /></svg>', | ||||
|         }, | ||||
|       }, | ||||
|     }), | ||||
|   ], | ||||
|   transformers: [transformerVariantGroup(), transformerDirectives()], | ||||
|   theme: { | ||||
|     colors: { | ||||
|       border: 'hsl(var(--border))', | ||||
|       input: 'hsl(var(--input))', | ||||
|       ring: 'hsl(var(--ring))', | ||||
|       background: 'hsl(var(--background))', | ||||
|       foreground: 'hsl(var(--foreground))', | ||||
|       primary: { | ||||
|         DEFAULT: 'hsl(var(--primary))', | ||||
|         foreground: 'hsl(var(--primary-foreground))', | ||||
|       }, | ||||
|       secondary: { | ||||
|         DEFAULT: 'hsl(var(--secondary))', | ||||
|         foreground: 'hsl(var(--secondary-foreground))', | ||||
|       }, | ||||
|       destructive: { | ||||
|         DEFAULT: 'hsl(var(--destructive))', | ||||
|         foreground: 'hsl(var(--destructive-foreground))', | ||||
|       }, | ||||
|       warning: { | ||||
|         DEFAULT: 'hsl(var(--warning))', | ||||
|         foreground: 'hsl(var(--warning-foreground))', | ||||
|       }, | ||||
|       muted: { | ||||
|         DEFAULT: 'hsl(var(--muted))', | ||||
|         foreground: 'hsl(var(--muted-foreground))', | ||||
|       }, | ||||
|       accent: { | ||||
|         DEFAULT: 'hsl(var(--accent))', | ||||
|         foreground: 'hsl(var(--accent-foreground))', | ||||
|       }, | ||||
|       popover: { | ||||
|         DEFAULT: 'hsl(var(--popover))', | ||||
|         foreground: 'hsl(var(--popover-foreground))', | ||||
|       }, | ||||
|       card: { | ||||
|         DEFAULT: 'hsl(var(--card))', | ||||
|         foreground: 'hsl(var(--card-foreground))', | ||||
|       }, | ||||
|     }, | ||||
|     borderRadius: { | ||||
|       lg: 'var(--radius)', | ||||
|       md: 'calc(var(--radius) - 2px)', | ||||
|       sm: 'calc(var(--radius) - 4px)', | ||||
|     }, | ||||
|     animation: { | ||||
|       keyframes: { | ||||
|         'accordion-down': | ||||
|           '{ from { height: 0 } to { height: var(--kb-accordion-content-height) } }', | ||||
|         'accordion-up': | ||||
|           '{ from { height: var(--kb-accordion-content-height) } to { height: 0 } }', | ||||
|         'collapsible-down': | ||||
|           '{ from { height: 0 } to { height: var(--kb-collapsible-content-height) } }', | ||||
|         'collapsible-up': | ||||
|           '{ from { height: var(--kb-collapsible-content-height) } to { height: 0 } }', | ||||
|         'caret-blink': '{ 0%,70%,100% { opacity: 1 } 20%,50% { opacity: 0 } }', | ||||
|       }, | ||||
|       timingFns: { | ||||
|         'accordion-down': 'ease-out', | ||||
|         'accordion-up': 'ease-out', | ||||
|         'collapsible-down': 'ease-out', | ||||
|         'collapsible-up': 'ease-out', | ||||
|         'caret-blink': 'ease-out', | ||||
|       }, | ||||
|       durations: { | ||||
|         'accordion-down': '0.2s', | ||||
|         'accordion-up': '0.2s', | ||||
|         'collapsible-down': '0.2s', | ||||
|         'collapsible-up': '0.2s', | ||||
|         'caret-blink': '1.25s', | ||||
|       }, | ||||
|       counts: { | ||||
|         'caret-blink': 'infinite', | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   safelist: [ | ||||
|     ...toolDefinitions.map(tool => tool.icon), | ||||
|     ...socialLinks.map(({ icon }) => icon), | ||||
|   ], | ||||
|   shortcuts: { | ||||
|     'i-logo': 'i-tabler-terminal', | ||||
|   }, | ||||
| }); | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user