This commit is contained in:
Corentin Thomasset
2025-08-14 23:51:29 +02:00
parent 00fd51a8e3
commit 78e185a281
103 changed files with 5862 additions and 9988 deletions

View File

@@ -1 +0,0 @@
# IT-Tools

24
apps/it-tools/.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,47 @@
# Astro Starter Kit: Minimal
```sh
pnpm create astro@latest -- --template minimal
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](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).

View 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: {
},
});

View File

@@ -3,15 +3,14 @@
"uno": {
"config": "uno.config.ts",
"css": {
"path": "src/client/app.css",
"path": "src/assets/app.css",
"variable": true
},
"color": "neutral",
"prefix": ""
},
"alias": {
"component": "@/modules/ui/components",
"ui": "@/modules/ui/components",
"cn": "@/modules/ui/utils/cn"
"component": "@/components",
"cn": "@/libs/cn"
}
}

814
apps/it-tools/package-lock.json generated Normal file
View 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
}
}
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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

View 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;
}

View 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>

View 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>

View 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>
)
}

View 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>

View 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}
/>
);
};

View 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>
);
};

View 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 }

View 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>
);
};

View 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 }

View 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>
}

View 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}
/>
);
};

View 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}
/>
);
};

View 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');
});
});
});

View 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}`;
}

View File

@@ -0,0 +1,8 @@
import { locales } from "./languages";
export function getStaticPaths() {
return [
{ params: { lang: undefined }, },
...locales.map((lang) => ({params: { lang },})),
];
}

View 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";

View 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>

View 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));

View 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>

View 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 />

View 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>

View File

@@ -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).

View File

@@ -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",
};

View File

@@ -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",
};

View File

@@ -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;

View File

@@ -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>

View File

@@ -0,0 +1,7 @@
import en from "./i18n/en.ts";
import fr from "./i18n/fr.ts";
export const locales = {
en,
fr,
} as const;

View File

@@ -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;
}

View File

@@ -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>;
};

View File

@@ -0,0 +1,6 @@
import tokenGeneratorDefinition from "./token-generator/token-generator.definition";
import type { ToolDefinition } from "./tools.types";
export const toolDefinitions: ToolDefinition[] = [
tokenGeneratorDefinition,
];

View 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;
}
}

View 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/*"]
}
}
}

View 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),
]
});

View File

@@ -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: '^_',
}],
},
});

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -1,4 +0,0 @@
/* TEAM */
Developer: Corentin Thomasset
Site: https://corentin.tech
Twitter: @cthmsst

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow:

View File

@@ -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;
}

View File

@@ -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>
),
},
],
},
];

View File

@@ -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')!,
);

View File

@@ -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)."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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>
);
};

View File

@@ -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');

View File

@@ -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>
);
};

View File

@@ -1,3 +0,0 @@
import type { locales } from './i18n.constants';
export type LocaleKey = typeof locales[number]['key'];

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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);
},
};
}

View File

@@ -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);
});
});
});

View File

@@ -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;
}

View File

@@ -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>
);
};

View File

@@ -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"
}

View File

@@ -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;

View File

@@ -1,5 +0,0 @@
import { random } from 'lodash-es';
export function generateRandomPort() {
return random(1024, 65535);
}

View File

@@ -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',
});

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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('');
}

View File

@@ -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;

View File

@@ -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',
});

View File

@@ -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>
);
};

View File

@@ -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,
};
}

View File

@@ -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>
);
};

View File

@@ -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];
}

View File

@@ -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 };
}

View File

@@ -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>;

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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} />
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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>
&copy;
{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>
);
};

View File

@@ -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',
},
];

View File

@@ -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,
};
}

View File

@@ -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));

View File

@@ -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) %>',
})

View File

@@ -1,4 +0,0 @@
---
to: src/modules/tools/definitions/<%= h.changeCase.param(name) %>/locales/en.json
---
{}

View File

@@ -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';

View File

@@ -1,6 +0,0 @@
---
inject: true
to: src/modules/tools/tools.registry.ts
before: "^]"
---
<%= h.changeCase.camel(name) %>Tool,

View File

@@ -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) %>;

View File

@@ -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"]
}

View File

@@ -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