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": { |   "uno": { | ||||||
|     "config": "uno.config.ts", |     "config": "uno.config.ts", | ||||||
|     "css": { |     "css": { | ||||||
|       "path": "src/client/app.css", |       "path": "src/assets/app.css", | ||||||
|       "variable": true |       "variable": true | ||||||
|     }, |     }, | ||||||
|     "color": "neutral", |     "color": "neutral", | ||||||
|     "prefix": "" |     "prefix": "" | ||||||
|   }, |   }, | ||||||
|   "alias": { |   "alias": { | ||||||
|     "component": "@/modules/ui/components", |     "component": "@/components", | ||||||
|     "ui": "@/modules/ui/components", |     "cn": "@/libs/cn" | ||||||
|     "cn": "@/modules/ui/utils/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