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