Compare commits

..

3 Commits
v3 ... next

Author SHA1 Message Date
Corentin Thomasset
00fd51a8e3 feat(ui): tool header 2024-10-06 11:32:23 +02:00
Corentin Thomasset
161b9e6bca chore(cd): added deploy on cloudflare pages 2024-10-05 21:13:40 +02:00
Corentin Thomasset
f8b5cbfd87 feat: it-tools v3 base 2024-10-02 22:15:38 +02:00
178 changed files with 7646 additions and 11468 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -3,7 +3,7 @@ name: CD - Production
on:
push:
branches:
- v3
- next
jobs:
publish-app-prod:
@@ -25,7 +25,7 @@ jobs:
run: pnpm i
- name: Build the app
run: pnpm -F @it-tools/app build:cloudflare-pages
run: pnpm -F @it-tools/app build
- name: Publish to Cloudflare Pages
uses: AdrianGonz97/refined-cf-pages-action@v1
@@ -37,7 +37,7 @@ jobs:
workingDirectory: packages/app
directory: dist
deploymentName: Production App
branch: v3
branch: next
wranglerVersion: '3'

View File

@@ -24,8 +24,8 @@ jobs:
- name: Run linters
run: pnpm lint
# - name: Run unit test
# run: pnpm test:unit
- name: Run unit test
run: pnpm test
- name: Type check
run: pnpm typecheck

2
.npmrc
View File

@@ -1,2 +0,0 @@
shamefully-hoist=true
strict-peer-dependencies=false

View File

@@ -1,3 +1 @@
# IT-Tools
It-Tools v3 nuxt exploration branch

View File

@@ -1,393 +0,0 @@
home:
categories:
newestTools: Nyeste verktøy
favoriteTools: 'Dine favoritt verktøy'
allTools: 'Alle verktøyene'
subtitle: 'Nyttige verktøy for utviklere'
toggleMenu: 'Vekslemenmy'
home: Hjem
uiLib: 'UI Bib'
support: 'Støtt utviklingen av IT-Tools'
buyMeACoffee: 'Kjøp en kaffe til meg'
follow:
title: 'Liker du it-tools?'
p1: 'Gi oss en stjerne på'
githubRepository: 'IT-Tools GitHub-depotet'
p2: 'eller følg oss på'
twitterAccount: 'IT-Tools sin twitter konto'
thankYou: 'Tusen takk!'
nav:
github: 'GitHub-depot'
githubRepository: 'IT-Tools GitHub-depot'
twitter: 'Twitter konto'
twitterAccount: 'IT Tools Twitter konto'
about: 'Om IT-Tools'
aboutLabel: 'Om'
darkMode: 'Mørk modus'
lightMode: 'Lys modus'
mode: 'Veksle mørk/lys modus'
about:
content: >
# Om IT-Tools
Denne vidunderlige nettsiden, laget med ❤ av [Corentin Thomasset](https://corentin.tech?utm_source=it-tools&utm_medium=about) , sammenstiller nyttige verktøy for utviklere og folk som jobber innen IT. Hvis du finner dette nyttig, Del det gjerne med andre som du tror kan få nytte av dette, og ikke glem å lage et bokmerke!
IT Tools er åpen kildekode (under MIT lisensen) og gratis, og det vil det alltid være, men det koster å drifte og å fornye domenet. Hvis du ønsker å støtte arbeidet mitt, og motivere meg til å legge til flere verktøy, gjerne støtt meg ved å [sponse meg](https://www.buymeacoffee.com/cthmsst).
## Teknologier
IT Tools er laget i Vue.js (Vue 3) med Naive UI komponent bibliotektet og er hosted og kontinuerlig deployet av Vercel. Tredjeparts åpen-kildekode biblioteker er brukt i noen verktøy, du kan finne den komplette listen i [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) filen i depoet.
## Funnet en feil? Et verktøy som mangler?
Hvis du trenger et verktøy som foreløpig ikke er tilgjengelig her, og du tenker det kan være nyttig for andre, så er du velkommen til å legge til en funksjonsforespørsel i [problem seksjonen](https://github.com/CorentinTh/it-tools/issues/new/choose) i github-depotet.
Og hvis du har funnet en feil, eller noe ikke oppfører seg som forventet, vennligst send inn en feilrapport i [problem seksjonen](https://github.com/CorentinTh/it-tools/issues/new/choose) i github-depotet.
404:
notFound: '404 ikke funnet'
sorry: 'Beklager, denne siden ser ikke ut til å eksistere'
maybe: 'Kanskje informasjonskapslene oppfører seg rart, prøvd en tvungen oppfriskning?'
backHome: 'Tilbake til start'
favoriteButton:
remove: 'Fjern fra favoritter'
add: 'Legg til favoritter'
toolCard:
new: Ny
search:
label: Søk
tools:
categories:
favorite-tools: 'Dine favoritt verktøy'
crypto: Krypto
converter: Konvertering
web: Web
images and videos: 'Bilder & Videoer'
development: Utvikling
network: Nettverk
math: Matte
measurement: Måling
text: Tekst
data: Data
password-strength-analyser:
title: Analyseverktøy for passordstyrke
description: Oppdag styrken av passordet ditt med dette kun-klient-maskin passordstyrke analyse verktøyet og se den estimerte knekketiden.
chronometer:
title: Kronometer
description: Overvåk varigheten av noe. I bunn og grunn et kronometer med enkle funksjoner.
token-generator:
title: Token generator
description: Generer en tilfeldig streng med store og/eller små bokstaver, siffer og/eller symboler.
uppercase: Store bokstaver (ABC...)
lowercase: Små bokstaver (abc...)
numbers: Siffer (123...)
symbols: Symboler (!-;...)
length: Lengde
tokenPlaceholder: 'Tokenet...'
copied: Tokenet er kopiert til utklippstavlen.
button:
copy: Kopier
refresh: Oppfrisk
percentage-calculator:
title: Prosent kalkulator
description: Beregn enkelt prosenter fra en verdi til en annen, eller fra en prosent til en verdi.
svg-placeholder-generator:
title: SVG plassholder generator
description: Generer svg bilder til å bruke som plassholder i applikasjonen din.
json-to-csv:
title: JSON til CSV
description: Konverter JSON til CSV med automatisk oppdagelse av headeren.
camera-recorder:
title: Kameraopptak
description: Ta et bilde eller spill inn en video med webkamera eller kameraet ditt.
keycode-info:
title: Tastekode info
description: Finn javascript tastekode, kode, plassering og modifikatorer av hvilken som helst tast.
emoji-picker:
title: Emoji velger
description: Klipp og lim emojis og få unicode og kode verdien av hver emoji.
color-converter:
title: Farge konverter
description: Konverter farger mellom de forskjellige formatene (hex, rgb, hsl og css navn).
bcrypt:
title: Bcrypt
description: Hash og sammenlign tekst ved hjelp av bcrypt. Bcrypt er en passord-hashings funksjon basert på Blowfish cipher.
crontab-generator:
title: Crontab generator
description: Verifiser og generer crontab og få den mennesklig leselige beskrivelsen av cron timeplanen.
http-status-codes:
title: HTTP status koder
description: Liste over alle HTTP status koder, navnet dems, og betydningen.
sql-prettify:
title: SQL forskjønning and format
description: Formater og forskjønn SQL spørringene dine (den støtter forskjellige SQL dialekter).
benchmark-builder:
title: Bygg en referansemåler
description: Sammenlign enkelt kjøretiden av oppgaver med denne enkle referansemåls byggeren.
git-memo:
title: Git jukselapp
description: Git er en desentralisert versjons håndterings programvare. Med denne jukselappen vil du få kjapp tilgang til de vanligste kommandoene.
slugify-string:
title: Slugify streng
description: Lag en trygg url, filbane eller id.
encryption:
title: Krypter / decrypter tekst
description: Krypter klartekst og dekrypter ciphertekst ved bruk av krypteringsalgoritmer som AES, TripleDES, Rabbit eller RC4.
random-port-generator:
title: Tilfeldig port generator
description: Generer tilfeldige portnumre utenfor scopet av "kjente" porter (0-1023).
yaml-prettify:
title: YAML forskjønning og formatering
description: Forskjønn YAML strengene dine til et lettlest format.
eta-calculator:
title: ETA kalkulator
description: En ETA (Estimert Tid for Ankomst) kalkulator for å anslå den sannsynelige slutt tiden for en oppgave, for eksempel, slutttiden og varigheten av en filnedlastning.
roman-numeral-converter:
title: Romertall konverter
description: Konverter romertall til tall eller konverter tall til romertall.
hmac-generator:
title: Hmac generator
description: Beregn en hash-basert meldings authentiserings kode (HMAC) ved bruk av en hemmelig nøkkel og din foretrukne hashings funksjon.
bip39-generator:
title: BIP39 nøkkelords generator
description: Generer et BIP39 nøkkelord fra en eksisterende eller tilfeldig huskesetning, eller få ut en huskesetning fra nøkkelordet.
base64-file-converter:
title: Base64 fil konverter
description: Konverter en base64 streng til fil eller en fil, bilde til en base64 representasjon.
list-converter:
title: Liste konverterer
description: Dette verktøyet kan prosessere kolonnebasert data og foreta forskjellige endringer (transposering, legge til prefix og suffix, reversere lister, sortere lister, gjøre om til små bokstaver, trunkere verdier) på hver rad.
base64-string-converter:
title: Base64 string kode/dekoder
description: Enkelt kode eller dekode en tekststreng til base64 representasjonen av strengen.
toml-to-yaml:
title: TOML til YAML
description: Parser og konverter TOML til YAML.
math-evaluator:
title: Matematikkevaluator
description: En Kalkulator for å evaluere matematiske uttrykk. Du kan bruke funksjoner som sqrt, cos, sin, abs, etc.
json-to-yaml-converter:
title: JSON til YAML konverterer
description: Enkelt konverter JSON til YAML med dette verktøyet.
url-parser:
title: URL analyse
description: Parsere en URL ned til bestanddelene (protokoll, opprinnelse, parametre, port, brukernavn-passord, ...).
iban-validator-and-parser:
title: IBAN validering og analysering
description: Valider og parser IBAN numre. Sjekk om et IBAN er gyldig og få landet, BBAN, om det er en QR-IBAN og IBAN i et vennlig format.
user-agent-parser:
title: User-agent analysering
description: Detekter og parser nettleser, motor, OS, CPU, og enhet type/modell fra en user-agent tekst streng.
numeronym-generator:
title: Numeronym generator
description: Et numeronym er et ord hvor et nummer er brukt til å lage en forkortelse. For eksempel, "i18n" er et numeronym for "internasjonalisering" hvor 18 står for antall bokstaver mellom første bokstaven i og den siste bokstaven n i ordet.
case-converter:
title: Bokstavkonvertering
description: Formater bokstavene med store eller små bokstaver, samt andre format.
html-entities:
title: HTML streng rensing
description: Rens bort eller omsvøp HTML entiteter (erstatt tegn som <,>, &, " and \' med deres HTML versjon).
json-prettify:
title: JSON forskjønning og formatering
description: Forskjønn JSON strenger til et lettlest format.
docker-run-to-docker-compose-converter:
title: Docker run til Docker compose konverter
description: Konverter "docker run" kommandoer til docker-compose filer!
mac-address-lookup:
title: MAC address oppslagsverk
description: Finn forhandler og produsent basert på MAC adressen.
mime-types:
title: MIME typer
description: Konverter MIME typer til fil utvidelser og visa-versa.
toml-to-json:
title: TOML til JSON
description: Parser og konverter TOML til JSON.
lorem-ipsum-generator:
title: Lorem ipsum generator
description: Lorem ipsum er brukt som plassholder tekst, vanligvis brukt til å demonstrere den visuelle formen av et dokument eller font-type uten å måtte ha meningsfult innhold.
qrcode-generator:
title: QR Kode generator
description: Generer og last ned en QR kode til en URL (eller ren tekst), og tilpass bakgrunns og forgrunns farger.
wifi-qrcode-generator:
title: WiFi QR Kode generator
description: Generer og last ned QR koder for rask tilkobling til wifi nettverket.
xml-formatter:
title: XML formaterer
description: Forskjønn en XML streng til et lettlest format.
temperature-converter:
title: Temperatur konverter
description: Temperatur konversjoner mellom Kelvin, Celsius, Fahrenheit, Rankine, Delisle, Newton, Réaumur, og Rømer.
chmod-calculator:
title: Chmod kalkulator
description: Beregn chmod tillatelser og kommandoer med denne chmod kalkulatoren.
rsa-key-pair-generator:
title: RSA nøkkelpar generator
description: Generer et nytt tilfeldig RSA privat og offentlig pem sertifikat nøkkel par.
html-wysiwyg-editor:
title: HTML WYSIWYG editor
description: Online, funksjonsrik WYSIWYG HTML editor som genererer kildekoden for innholdet øyeblikkelig.
yaml-to-toml:
title: YAML til TOML
description: Parser og konverter YAML til TOML.
mac-address-generator:
title: MAC adresse generator
description: Sett inn antall og prefix. MAC addressene blir generert i ønsket format
json-diff:
title: JSON diff
description: Sammenlign to JSON objekter og finn forskjellene mellom dem.
jwt-parser:
title: JWT parser
description: Parse og dekode et JSON Web Token (jwt) og vis innholdet.
date-converter:
title: Dato-tid konverter
description: Konverter dato og tid til forskjellige formater.
phone-parser-and-formatter:
title: Telefon format og parserer
description: Parser, valider og formater telefon numre. få innformasjonen om telefon nummeret, slik som landskoden, type etc.
ipv4-subnet-calculator:
title: IPv4 subnet kalkulator
description: Parser IPv4 CIDR blokker of åf all info du trenger om subnettet.
og-meta-generator:
title: Open graph meta generator
description: Generer open-graph og SoMe HTML meta tagger til nettsiden din.
ipv6-ula-generator:
title: IPv6 ULA generator
description: Generer din egen lokale, ikke-rutbare IP adresse til nettverket ditt i henhold til RFC4193.
hash-text:
title: Hash tekst
description: 'Hash en tekst streng med en av algoritmene : MD5, SHA1, SHA256, SHA224, SHA512, SHA384, SHA3 eller RIPEMD160'
json-to-toml:
title: JSON til TOML
description: Parser og konverter JSON til TOML.
device-information:
title: Enhets informasjon
description: Få informasjon om din nåværende enhet (skjermstørrelse, piksel-forhold, user agent, etc.)
pdf-signature-checker:
title: PDF signatur sjekker
description: Bekreft signaturen til en PDF fil. En signert PDF fil inneholder en eller flere signaturer som kan bli brukt til å bestemme om en fil har blitt endret etter at den var signert.
json-minify:
title: JSON minifiser
description: Minifiser og komprimer JSON ved å fjerne unødvendige mellomrom.
ulid-generator:
title: ULID generator
description: Generer tilfeldig Universell Unik Leksikografisk Sorterbar Identifikator (ULID).
string-obfuscator:
title: Streng obfuskator
description: Obfusker en streng (som en hemmelighet, en IBAN, eller et token) og gjør den delbar og identifiserbar uten å vise innholdet.
base-converter:
title: Heltalls konverter
description: Konverter et heltall mellom forskjellige baser (desimal, hexadesimal, binær, oktal, base64, etc.)
yaml-to-json-converter:
title: YAML til JSON konverter
description: Konverterl YAML til JSON.
uuid-generator:
title: UUIDs generator
description: En universell Unik Identifikator (UUID) er et 128-bit nummer, brukt til å identifisere informasjon i datasystemer.
ipv4-address-converter:
title: IPv4 adresse konverter
description: Konverter en IPv4 adresse til desimal, binær, hexadesimal, eller en IPv6 representasjon.
text-statistics:
title: Tekst statistikk
description: Få informasjonen om en tekst, antall karakterer, antall ord, størrelsen i bytes, etc.
text-to-nato-alphabet:
title: Tekst til NATO alfabetet
description: Transformer teksten til det NATO fonetiske alfabetet for muntlig gjengivelse.
basic-auth-generator:
title: Basic auth generator
description: Generer en base64 basic auth header fra et brukernavn og passord.
text-to-unicode:
title: Tekst til Unicode
description: Parser og konverter tekst til unicode og visa-versa
ipv4-range-expander:
title: IPv4 range utvider
description: Gitt en start og en slutt IPv4 adresse, kalkulerer dette verktøyet et gyldig IPv4 subnet sammen med sin CIDR notasjon.
text-diff:
title: Tekst diff
description: Sammenlign to tekster og vis forskjellen mellom dem.
otp-generator:
title: OTP kode generator
description: Generer og valider tidsbasert OTP (one time password) for multi-faktor autentisering.
url-encoder:
title: Kode/dekode URL-formaterte strenger
description: Kode tekst til URL-kodet format (også kjent som "prosent-kodet"), eller dekode fra det.
text-to-binary:
title: Tekst til ASCII binært
description: Konverter tekst til sin ASCII binære representasjon og visa-versa.

View File

@@ -2,8 +2,6 @@
"name": "@it-tools/root",
"version": "0.0.0",
"description": "IT Tools monorepo root",
"packageManager": "pnpm@9.12.2",
"scripts": {
"dev": "pnpm -F @it-tools/app dev"
},
@@ -12,5 +10,6 @@
"repository": {
"type": "git",
"url": "https://github.com/CorentinTh/it-tools"
}
},
"packageManager": "pnpm@9.11.0"
}

View File

@@ -1,24 +0,0 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

View File

@@ -1,75 +0,0 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View File

@@ -1,10 +0,0 @@
<script setup lang="ts">
import CommandPalette from './src/modules/command-palette/components/command-palette.vue';
</script>
<template>
<NuxtLayout>
<CommandPalette />
<NuxtPage />
</NuxtLayout>
</template>

View File

@@ -1,77 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 98%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 95 71% 68%;
--primary-foreground: 240 10% 3.9%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border:240 5.9% 90%;
--input:240 5.9% 90%;
--ring:240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background:240 4% 10%;
--foreground:0 0% 98%;
--card: 240 5% 8%;
--card-foreground:0 0% 98%;
--popover:240 10% 3.9%;
--popover-foreground:0 0% 98%;
--primary: 83 79% 55%;
--primary-foreground:240 5.9% 10%;
--secondary:240 3.7% 15.9%;
--secondary-foreground:0 0% 98%;
--muted:240 3.7% 15.9%;
--muted-foreground:240 5% 64.9%;
--accent:240 3.7% 15.9%;
--accent-foreground:0 0% 98%;
--destructive:0 62.8% 30.6%;
--destructive-foreground:0 0% 98%;
--border:240 3.7% 15.9%;
--input:240 3.7% 15.9%;
--ring:240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,19 +1,17 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tsConfigPath": ".nuxt/tsconfig.json",
"tailwind": {
"config": "tailwind.config.js",
"css": "assets/css/tailwind.css",
"baseColor": "neutral",
"cssVariables": true,
"$schema": "https://shadcn-solid.com/schema.json",
"uno": {
"config": "uno.config.ts",
"css": {
"path": "src/client/app.css",
"variable": true
},
"color": "neutral",
"prefix": ""
},
"framework": "nuxt",
"aliases": {
"components": "@/src/modules/ui/components",
"ui": "@/src/modules/ui/components",
"utils": "@/src/modules/shared/style/cn"
"alias": {
"component": "@/modules/ui/components",
"ui": "@/modules/ui/components",
"cn": "@/modules/ui/utils/cn"
}
}

View File

@@ -5,8 +5,6 @@ export default antfu({
semi: true,
},
vue: true,
rules: {
// To allow export on top of files
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],

View File

@@ -1,5 +0,0 @@
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
}));

21
packages/app/index.html Normal file
View File

@@ -0,0 +1,21 @@
<!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>

View File

@@ -1,60 +0,0 @@
import toolsModule from './src/modules/tools/modules/tools.modules';
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true },
extends: [
'src/modules/app',
],
modules: [
'@nuxtjs/tailwindcss',
'shadcn-nuxt',
'@nuxt/fonts',
'@nuxt/icon',
'@vueuse/nuxt',
'@nuxtjs/color-mode',
toolsModule, // Must be imported before i18n
'@nuxtjs/i18n',
'@nuxtjs/seo',
'@pinia/nuxt',
],
site: {
url: 'https://it-tools.tech',
name: 'IT Tools',
description: 'The open-source collection of handy online tools to help developers in their daily life.',
},
fonts: {
provider: 'bunny',
defaults: {
weights: [400, 500, 600, 700, 800],
},
},
colorMode: {
preference: 'system',
fallback: 'dark',
classSuffix: '',
storage: 'cookie',
storageKey: 'itts-color-mode',
},
i18n: {
strategy: 'prefix',
vueI18n: './i18n.config.ts',
defaultLocale: 'en',
langDir: './src/locales',
locales: [
{ code: 'en', file: 'en.yaml', name: 'English' },
{ code: 'fr', file: 'fr.yaml', name: 'Français' },
],
},
experimental: {
scanPageMeta: false, // Causes some issues with layouts and hook-registered pages
},
});

2381
packages/app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,54 @@
{
"name": "@it-tools/app",
"type": "module",
"private": true,
"packageManager": "pnpm@9.15.1",
"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": "nuxt dev",
"build": "nuxt build",
"build:cloudflare-pages": "nuxt build --preset cloudflare_pages",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"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",
"typecheck": "tsc --noEmit"
"create:tool": "HYGEN_TMPLS=templates hygen tools new"
},
"dependencies": {
"@corentinth/chisels": "^1.1.0",
"@nuxt/fonts": "^0.10.3",
"@nuxt/icon": "^1.10.3",
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/i18n": "^8.5.6",
"@nuxtjs/seo": "2.0.0-rc.23",
"@pinia/nuxt": "^0.5.5",
"class-variance-authority": "^0.7.1",
"@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",
"lucide-vue-next": "^0.453.0",
"nuxt": "^3.15.0",
"radix-vue": "^1.9.11",
"shadcn-nuxt": "^0.10.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"vue": "latest",
"vue-router": "latest"
"solid-js": "^1.9.1",
"solid-sonner": "^0.2.8",
"tailwind-merge": "^2.5.2"
},
"devDependencies": {
"@antfu/eslint-config": "^3.12.1",
"@nuxtjs/tailwindcss": "^6.12.2",
"@tailwindcss/typography": "^0.5.15",
"@antfu/eslint-config": "^3.7.3",
"@iconify-json/tabler": "^1.2.3",
"@types/lodash-es": "^4.17.12",
"@vueuse/core": "^11.3.0",
"@vueuse/nuxt": "^11.3.0",
"eslint": "^9.17.0",
"typescript": "^5.7.2",
"vitest": "^2.1.8"
"@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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

@@ -1,3 +0,0 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

75
packages/app/src/app.css Normal file
View File

@@ -0,0 +1,75 @@
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--warning: 31 98% 50%;
--warning-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
[data-kb-theme="dark"] {
--background: 0 0% 9%;
--foreground: 0 0% 98%;
--card: 0 0% 7%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 83 79% 55%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--warning: 31 98% 50%;
--warning-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}

View File

@@ -0,0 +1,69 @@
import type { LocaleKey } from './modules/i18n/i18n.types';
import { A, Navigate, type RouteDefinition, useParams } from '@solidjs/router';
import { localeKeys } from './modules/i18n/i18n.constants';
import { useI18n } from './modules/i18n/i18n.provider';
import { HomePage } from './modules/pages/home.page';
import { ToolPage } from './modules/tools/pages/tool.page';
import { toolSlugs } from './modules/tools/tools.registry';
import { Button } from './modules/ui/components/button';
import { AppLayout } from './modules/ui/layouts/app.layout';
export const routes: RouteDefinition[] = [
{
path: '/',
component: () => {
const { getLocale } = useI18n();
return <Navigate href={`/${getLocale()}`} />;
},
},
{
path: '/',
component: AppLayout,
children: [
{
path: '/:localeKey',
matchFilters: {
localeKey: localeKeys,
},
component: (props) => {
const params = useParams();
const { setLocale } = useI18n();
setLocale(params.localeKey as LocaleKey);
return props.children;
},
children: [
{
path: '/',
component: HomePage,
},
{
path: '/:toolSlug',
matchFilters: {
toolSlug: toolSlugs,
},
component: ToolPage,
},
],
},
{
path: '*404',
component: () => (
<div class="flex flex-col items-center justify-center mt-6">
<div class="text-3xl font-light text-muted-foreground">404</div>
<h1 class="font-semibold text-lg my-2">Page Not Found</h1>
<p class="text-muted-foreground">The page you are looking for does not exist.</p>
<p class="text-muted-foreground">Please check the URL and try again.</p>
<Button as={A} href="/" class="mt-4" variant="secondary">
<div class="i-tabler-arrow-left mr-2"></div>
Go back home
</Button>
</div>
),
},
],
},
];

View File

@@ -0,0 +1,43 @@
/* @refresh reload */
import { ColorModeProvider, ColorModeScript, createLocalStorageManager } from '@kobalte/core/color-mode';
import { Router } from '@solidjs/router';
import { render, Suspense } from 'solid-js/web';
import { routes } from './client-routes';
import { CommandPaletteProvider } from './modules/command-palette/command-palette.provider';
import { RootI18nProvider } from './modules/i18n/i18n.provider';
import { Toaster } from './modules/ui/components/sonner';
import '@unocss/reset/tailwind.css';
import 'virtual:uno.css';
import './app.css';
render(
() => {
const initialColorMode = 'system';
const colorModeStorageKey = 'it_tools_color_mode';
const localStorageManager = createLocalStorageManager(colorModeStorageKey);
return (
<Router
children={routes}
root={props => (
<Suspense>
<RootI18nProvider>
<ColorModeScript storageType={localStorageManager.type} storageKey={colorModeStorageKey} initialColorMode={initialColorMode} />
<ColorModeProvider
initialColorMode={initialColorMode}
storageManager={localStorageManager}
>
<CommandPaletteProvider>
<Toaster />
<div class="min-h-screen font-sans text-sm font-400">{props.children}</div>
</CommandPaletteProvider>
</ColorModeProvider>
</RootI18nProvider>
</Suspense>
)}
/>
);
},
document.getElementById('root')!,
);

View File

@@ -0,0 +1,73 @@
{
"app": {
"title": "IT-Tools",
"description": "The open-source collection of handy online tools to help developers in their daily life."
},
"navbar": {
"theme": {
"theme": "Theme",
"light-mode": "Light mode",
"dark-mode": "Dark mode",
"system-mode": "System"
},
"language": "Language",
"contribute-to-i18n": "Contribute to i18n",
"github": "GitHub",
"support": "Support IT-Tools",
"report-bug": "Report a bug"
},
"footer": {
"resources": {
"title": "Resources",
"all-tools": "All the tools",
"github": "GitHub repository",
"support": "Support IT-Tools",
"license": "License"
},
"support": {
"title": "Support",
"report-bug": "Report a bug",
"request-feature": "Request a feature",
"contribute": "Contribute to the project",
"contact": "Contact me"
},
"friends": {
"title": "Friends"
}
},
"commandPalette": {
"input-placeholder": "Type to search for a tool or a command...",
"go-home": "Go to home",
"sections": {
"tools": "Tools",
"navigation": "Navigation",
"language": "Language",
"theme": "Theme"
},
"theme": {
"switch-to-light": "Switch to light theme",
"switch-to-dark": "Switch to dark theme",
"switch-to-system": "Use to system theme"
},
"trigger": {
"search": "Search for a tool"
}
},
"home": {
"all-tools": "All the tools",
"search-tools": "Search for a tool",
"open-source": "Open Source",
"free": "Free",
"self-hostable": "Self-hostable"
},
"tools": {
"token-generator": {
"name": "Token Generator",
"description": "Generate random string with the characters you want, uppercase, lowercase letters, numbers and/or symbols."
},
"random-port-generator": {
"name": "Random Port Generator",
"description": "Generate a random port number outside of the reserved ports range (0-1023)."
}
}
}

View File

@@ -1,41 +0,0 @@
app:
title: IT-Tools
description: The open-source collection of handy online tools to help developers in their daily life.
home:
all-the-tools: All the tools
search-tools: Search for a tool
open-source: Open Source
free: Free
self-hostable: Self-hostable
open-tool: Open tool
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
tools:
token-generator:
title: Token Generator
description: >-
Generate random string with the characters you want, uppercase, lowercase
letters, numbers and/or symbols.
placeholder: Generated token will appear here, please select at least one option.
use-uppercase: Include uppercase letters
use-lowercase: Include lowercase letters
use-numbers: Include numbers
use-symbols: Include symbols
exclude-similar: Exclude similar characters
length: Length
refresh: Refresh token
quantity: Quantity
format: Format

View File

@@ -0,0 +1,59 @@
{
"app": {
"title": "IT-Tools",
"description": "La collection open-source d'outils en ligne pour aider les devs dans leur vie quotidienne."
},
"navbar": {
"theme": {
"theme": "Thème",
"light-mode": "Mode clair",
"dark-mode": "Mode sombre",
"system-mode": "Système"
},
"language": "Langue",
"contribute-to-i18n": "Contribuer à l'i18n",
"github": "GitHub",
"support": "Soutenir IT-Tools",
"report-bug": "Signaler un bug"
},
"footer": {
"resources": {
"title": "Ressources",
"all-tools": "Tous les outils",
"github": "Dépôt GitHub",
"support": "Soutenir IT-Tools",
"license": "Licence"
},
"support": {
"title": "Support",
"report-bug": "Signaler un bug",
"request-feature": "Demander une fonctionnalité",
"contribute": "Contribuer au projet",
"contact": "Me contacter"
},
"friends": {
"title": "Ami·e·s"
}
},
"commandPalette": {
"input-placeholder": "Tapez pour rechercher un outil...",
"go-home": "Aller à l'accueil",
"sections": {
"tools": "Outils",
"navigation": "Navigation",
"theme": "Thème"
}
},
"home": {
"all-tools": "Tous les outils",
"open-source": "Open Source",
"free": "Gratuit",
"self-hostable": "Self-hostable"
},
"tools": {
"token-generator": {
"name": "Générateur de token",
"description": "Générer des string aléatoires, contrôlez les caractères que vous voulez, lettres majuscules, minuscules, chiffres et/ou symboles."
}
}
}

View File

@@ -1,29 +0,0 @@
app:
title: IT-Tools
description: La collection open-source d'outils en ligne pour aider les devs dans leur vie quotidienne.
home:
all-the-tools: Tous les outils
search-tools: Rechercher un outil
open-source: Open Source
free: Gratuit
self-hostable: Self-hostable
open-tool: Ouvrir l'outil
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
tools:
token-generator:
title: Générateur de token
description: Générer des chaines de caractères aléatoires, contrôlez les caractères que vous voulez, lettres majuscules, minuscules, chiffres et/ou symboles.

View File

@@ -1,116 +0,0 @@
<script setup>
const localePath = useLocalePath();
const { t } = useI18n();
const sections = computed(() => [
{
title: t('footer.resources.title'),
items: [
{ label: t('footer.resources.all-tools'), to: localePath('/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' },
],
},
]);
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',
},
];
</script>
<template>
<footer class="light:bg-muted/50 dark:bg-black/20 mt-12">
<div class="py-12 px-6 max-w-screen-xl mx-auto ">
<div class="flex items-start justify-between flex-col md:flex-row gap-12">
<div>
<div class="flex items-center gap-2">
<NuxtLink :to="localePath('/')" 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-[2px] leading-none border-current rounded-md px-1 pt-0.5 ml-1 group-hover:text-primary transition">TOOLS</span>
</NuxtLink>
</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}" />
</a>
))} -->
<a
v-for="socialLink in socialLinks" :key="socialLink.label" :href="socialLink.href" target="_blank" rel="noopener noreferrer" class="text-2xl text-muted-foreground hover:text-primary transition" :aria-label="socialLink.label"
>
<Icon :name="socialLink.icon" />
</a>
</div>
<div class="text-muted-foreground mt-2">
Crafted on Earth 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">
<div v-for="section in sections" :key="section.title">
<h4 class="font-semibold text-foreground">
{{ section.title }}
</h4>
<ul class="mt-4">
<li v-for="item in section.items" :key="item.label" class="mt-1">
<NuxtLink v-if="item.to" :to="localePath(item.to)" class="text-muted-foreground hover:text-primary transition">
{{ item.label }}
</NuxtLink>
<a v-else :href="item.href" target="_blank" rel="noopener" class="text-muted-foreground hover:text-primary transition">
{{ item.label }}
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="text-xs text-muted-foreground border-t border-border pt-4 mt-12">
<span>
&copy;
{{ new Date().getFullYear() }}
Corentin Thomasset
</span>
</div>
<div class="text-xs text-foreground opacity-80%" />
</div>
</footer>
</template>

View File

@@ -1,52 +0,0 @@
<script setup>
import { Button } from '@/src/modules/ui/components/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/src/modules/ui/components/dropdown-menu';
import { useCommandPaletteStore } from '../../command-palette/command-palette.store';
const { openCommandPalette } = useCommandPaletteStore();
const colorMode = useColorMode();
</script>
<template>
<div class="w-full flex items-center justify-between">
<div>
<Button variant="outline" class="sm:pr-12 md:pr-24 text-muted-foreground hidden sm:flex" @click="openCommandPalette">
<Icon name="i-tabler-search" class="mr-2 size-4" />
{{ $t('home.search-tools') }}
</Button>
</div>
<div class="flex items-center gap-0.5">
<Button variant="ghost" size="icon" class="sm:hidden" @click="openCommandPalette">
<Icon name="i-tabler-search" class="size-5" />
</Button>
<LocalePicker />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost">
<Icon v-if="colorMode.value === 'dark'" name="i-tabler-moon" class="size-5" />
<Icon v-else name="i-tabler-sun" class="size-5" />
<Icon name="i-tabler-chevron-down" class="ml-1 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem class="cursor-pointer" :class="{ 'font-bold': colorMode.preference === 'light' }" @click="colorMode.preference = 'light'">
<Icon name="i-tabler-sun" class="mr-2 size-4" />
Light
</DropdownMenuItem>
<DropdownMenuItem class="cursor-pointer" :class="{ 'font-bold': colorMode.preference === 'dark' }" @click="colorMode.preference = 'dark'">
<Icon name="i-tabler-moon" class="mr-2 size-4" />
Dark
</DropdownMenuItem>
<DropdownMenuItem class="cursor-pointer" :class="{ 'font-bold': colorMode.preference === 'system' }" @click="colorMode.preference = 'system'">
<Icon name="i-tabler-device-laptop" class="mr-2 size-4" />
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</template>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{ fadeBottom?: boolean; faderClass?: string }>(), { fadeBottom: true });
</script>
<template>
<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">
<slot />
<div v-if="props.fadeBottom" class="bg-gradient-to-t from-background to-transparent h-24 mt-24" :class="props.faderClass" />
</div>
</template>

View File

@@ -1,34 +0,0 @@
<script setup lang="ts">
import { Button } from '../../ui/components/button';
import { DropdownMenu } from '../../ui/components/dropdown-menu';
import DropdownMenuContent from '../../ui/components/dropdown-menu/DropdownMenuContent.vue';
import DropdownMenuItem from '../../ui/components/dropdown-menu/DropdownMenuItem.vue';
import DropdownMenuTrigger from '../../ui/components/dropdown-menu/DropdownMenuTrigger.vue';
const { locale, locales, setLocale } = useI18n();
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<slot>
<Button variant="ghost" aria-label="Change language">
<Icon name="i-tabler-language" class="size-5 mr-1" />
<Icon name="i-tabler-chevron-down" class="text-muted-foreground" />
</Button>
</slot>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
v-for="({ name, code }) in locales"
:key="code"
class="cursor-pointer"
:class="{ 'font-bold': locale === code }"
@click="setLocale(code)"
>
{{ name }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@@ -1,28 +0,0 @@
<script setup>
import { Button } from '@/src/modules/ui/components/button';
import { useToolsStore } from '../../tools/tools.store';
const toolStore = useToolsStore();
const localePath = useLocalePath();
</script>
<template>
<div class="border-b h-[60px] flex items-center justify-between px-6">
<NuxtLink variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto px-1 rounded-none !transition-border-color-250" :as="Button" :to="localePath('/')" aria-label="Home">
<span class="font-bold text-foreground">IT</span>
<span class="text-[80%] font-extrabold border-[2px] leading-none border-current rounded-md px-1 py-0.5 ml-1.5 text-primary">TOOLS</span>
</NuxtLink>
</div>
<div class="pt-4 px-3 flex flex-col gap-0.5">
<NuxtLink to="/" class="py-1.5 px-3 flex items-center text-muted-foreground hover:text-foreground transition hover:bg-muted rounded-lg">
<Icon name="i-tabler-home" class="mr-2 size-4" />
Home
</NuxtLink>
<NuxtLink v-for="tool in toolStore.tools" :key="tool.key" class="py-1.5 px-3 flex items-center text-muted-foreground hover:text-foreground transition hover:bg-muted rounded-lg" :to="tool.path" exact-active-class="bg-secondary !text-foreground">
<Icon :name="tool.icon" class="mr-2 size-4" />
{{ tool.title }}
</NuxtLink>
</div>
</template>

View File

@@ -1,27 +0,0 @@
<script setup>
import { Button } from '@/src/modules/ui/components/button';
import { useToolsStore } from '../../tools/tools.store';
const localePath = useLocalePath();
</script>
<template>
<div class="w-full min-h-screen text-sm relative font-sans flex flex-col">
<div class="h-[60px] border-b">
<div class="max-w-screen-xl mx-auto py-2 px-6 w-full flex items-center gap-4 h-full">
<NuxtLink variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto px-1 rounded-none !transition-border-color-250" :as="Button" :to="localePath('/')" aria-label="Home">
<span class="font-bold text-foreground">IT</span>
<span class="text-[80%] font-extrabold border-[2px] leading-none border-current rounded-md px-1 py-0.5 ml-1.5 text-primary">TOOLS</span>
</NuxtLink>
<app-header />
</div>
</div>
<div class="flex-1 pb-6">
<slot />
</div>
<app-footer />
</div>
</template>

View File

@@ -1,36 +0,0 @@
<script setup>
import { Button } from '../../ui/components/button';
import { Sheet, SheetContent, SheetTrigger } from '../../ui/components/sheet';
</script>
<template>
<div class="w-full min-h-screen text-sm relative font-sans flex flex-row">
<div class="w-64 border-r bg-white dark:bg-background shrink-0 hidden sm:block">
<sidenav-menu />
</div>
<div class="flex-1 flex flex-col">
<div class="border-b h-[60px] flex items-center justify-between px-6 bg-white dark:bg-background">
<div class="flex items-center gap-4">
<div class="sm:hidden">
<Sheet>
<SheetTrigger>
<Button variant="ghost" size="icon">
<Icon name="i-tabler-menu-2" class="size-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" class="p-0 text-sm">
<sidenav-menu />
</SheetContent>
</Sheet>
</div>
</div>
<app-header />
</div>
<div class="flex-1">
<slot />
</div>
</div>
</div>
</template>

View File

@@ -1,34 +0,0 @@
<script setup lang="ts">
import { Button } from '@/src/modules/ui/components/button';
const localePath = useLocalePath();
</script>
<template>
<div class="flex justify-center text-center mt-24">
<div>
<h1 class="text-3xl font-light text-muted-foreground">
404
</h1>
<h2 class="font-semibold text-lg my-2">
Page not found
</h2>
<p class="text-muted-foreground">
The page you are looking for does not seem to exist.
</p>
<p class="text-muted-foreground">
Please check the URL and try again.
</p>
<Button as-child variant="secondary" class="mt-4">
<NuxtLink :to="localePath('/')">
<Icon name="i-tabler-arrow-left" class="mr-2 size-4" />
Go back home
</NuxtLink>
</Button>
</div>
</div>
</template>

View File

@@ -1,3 +0,0 @@
<template>
TODO
</template>

View File

@@ -1,74 +0,0 @@
<script setup>
import { Badge } from '@/src/modules/ui/components/badge';
import { Button } from '@/src/modules/ui/components/button';
import { useCommandPaletteStore } from '../../command-palette/command-palette.store';
import { useToolsStore } from '../../tools/tools.store';
import Card from '../../ui/components/card/Card.vue';
const { tools } = useToolsStore();
const { openCommandPalette } = useCommandPaletteStore();
</script>
<template>
<grid-background>
<div class="flex gap-24 mx-auto justify-center pb-8 mt-8 items-center px-6">
<div class="max-w-xl">
<div class="flex gap-2">
<Badge class="text-primary bg-primary/10 hover:bg-primary/10 shadow-none">
{{ $t('home.open-source') }}
</Badge>
<Badge class="text-primary bg-primary/10 hover:bg-primary/10 shadow-none">
{{ $t('home.free') }}
</Badge>
<Badge class="text-primary bg-primary/10 hover:bg-primary/10 shadow-none">
{{ $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 my-6">
<span class="font-bold ">IT</span>
<span class="text-[90%] text-primary font-extrabold border-[5px] leading-none border-current rounded-xl px-2 py-0.5 ml-3">TOOLS</span>
</h1>
<p class="text-xl text-gray-400 mb-4">
{{ $t('app.description') }}
</p>
<div class="flex gap-4">
<Button>
{{ $t('home.all-the-tools') }}
<Icon name="i-tabler-arrow-right" class="ml-2 size-4" />
</Button>
<Button variant="outline" @click="openCommandPalette">
<Icon name="i-tabler-search" class="mr-2 size-4" />
{{ $t('home.search-tools') }}
</Button>
</div>
</div>
<div class="relative hidden sm: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" />
<Icon name="i-tabler-terminal" class="text-9xl text-primary m-8" />
</div>
</div>
</grid-background>
<div class="max-w-screen-xl mx-auto px-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-12">
<NuxtLink v-for="tool in tools" :key="tool.key" :to="tool.path">
<Card class="p-6 h-full cursor-pointer hover:shadow-lg transition hover:translate-y-[-2px]">
<Icon :name="tool.icon" class="size-12 text-muted-foreground/60" />
<div class="font-semibold text-base">
{{ tool.title }}
</div>
<p class="text-muted-foreground mt-2">
{{ tool.description }}
</p>
</Card>
</NuxtLink>
</div>
</div>
</template>

View File

@@ -0,0 +1,144 @@
import type { Accessor, ParentComponent } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { createContext, createMemo, createSignal, For, onCleanup, onMount, useContext } from 'solid-js';
import { locales } from '../i18n/i18n.constants';
import { useI18n } from '../i18n/i18n.provider';
import { useToolsStore } from '../tools/tools.store';
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../ui/components/command';
import { useThemeStore } from '../ui/themes/theme.store';
import { cn } from '../ui/utils/cn';
const CommandPaletteContext = createContext<{
getIsCommandPaletteOpen: Accessor<boolean>;
openCommandPalette: () => void;
closeCommandPalette: () => void;
}>();
export function useCommandPalette() {
const context = useContext(CommandPaletteContext);
if (!context) {
throw new Error('CommandPalette context not found');
}
return context;
}
export const CommandPaletteProvider: ParentComponent = (props) => {
const [getIsCommandPaletteOpen, setIsCommandPaletteOpen] = createSignal(false);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setIsCommandPaletteOpen(true);
}
};
onMount(() => {
document.addEventListener('keydown', handleKeyDown);
});
onCleanup(() => {
document.removeEventListener('keydown', handleKeyDown);
});
const { getTools } = useToolsStore();
const navigate = useNavigate();
const { t, createLocalizedUrl, changeLocale } = useI18n();
const { setColorMode } = useThemeStore();
const getCommandData = createMemo(() => [
{
label: t('commandPalette.sections.tools'),
options: [
...getTools().map(tool => ({
label: tool.name,
icon: tool.icon,
action: () => navigate(createLocalizedUrl({ path: tool.slug })),
})),
],
},
{
label: t('commandPalette.sections.navigation'),
options: [
{
label: t('commandPalette.go-home'),
icon: 'i-tabler-home',
action: () => navigate(createLocalizedUrl({ path: '' })),
},
],
},
{
label: t('commandPalette.sections.language'),
options: [
...locales.map(locale => ({
label: locale.switchToLabel,
icon: 'i-custom-language',
action: () => changeLocale(locale.key),
keywords: [locale.name, locale.key],
})),
],
},
{
label: t('commandPalette.sections.theme'),
options: [
{
label: t('commandPalette.theme.switch-to-light'),
icon: 'i-tabler-sun',
action: () => setColorMode({ mode: 'light' }),
},
{
label: t('commandPalette.theme.switch-to-dark'),
icon: 'i-tabler-moon',
action: () => setColorMode({ mode: 'dark' }),
},
{
label: t('commandPalette.theme.switch-to-system'),
icon: 'i-tabler-device-laptop',
action: () => setColorMode({ mode: 'system' }),
},
],
},
]);
const onCommandSelect = ({ action }: { action: () => void }) => {
action();
setIsCommandPaletteOpen(false);
};
return (
<CommandPaletteContext.Provider value={{
getIsCommandPaletteOpen,
openCommandPalette: () => setIsCommandPaletteOpen(true),
closeCommandPalette: () => setIsCommandPaletteOpen(false),
}}
>
<CommandDialog
class="rounded-lg border shadow-md"
open={getIsCommandPaletteOpen()}
onOpenChange={setIsCommandPaletteOpen}
>
<CommandInput placeholder={t('commandPalette.input-placeholder')} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<For each={getCommandData()}>
{section => (
<CommandGroup heading={section.label}>
<For each={section.options}>
{item => (
<CommandItem onSelect={() => onCommandSelect(item)}>
<span class={cn('mr-2 ml-1 size-4 text-muted-foreground', item.icon)} />
<span>{item.label}</span>
</CommandItem>
)}
</For>
</CommandGroup>
)}
</For>
</CommandList>
</CommandDialog>
{props.children}
</CommandPaletteContext.Provider>
);
};

View File

@@ -1,16 +0,0 @@
export const useCommandPaletteStore = defineStore('command-palette', () => {
const isCommandPaletteOpen = ref(false);
return {
isCommandPaletteOpen,
toggleCommandPalette() {
isCommandPaletteOpen.value = !isCommandPaletteOpen.value;
},
closeCommandPalette() {
isCommandPaletteOpen.value = false;
},
openCommandPalette() {
isCommandPaletteOpen.value = true;
},
};
});

View File

@@ -1,77 +0,0 @@
<script setup lang="ts">
import { useToolsStore } from '../../tools/tools.store';
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../../ui/components/command';
import { useCommandPaletteStore } from '../command-palette.store';
const commandPaletteStore = useCommandPaletteStore();
const { tools } = useToolsStore();
onKeyStroke('k', (e) => {
e.preventDefault();
if (!e.ctrlKey && !e.metaKey) {
return;
}
commandPaletteStore.toggleCommandPalette();
});
const commandSections = computed(() => [
{
title: 'Tools',
items: [
...tools.map(tool => ({
label: tool.title,
icon: tool.icon,
action: () => navigateTo(tool.path),
})),
],
},
]);
function handleSelectCommand({ item }: { item: { label: string; action: () => void; keepOpen?: boolean } }) {
item.action();
if (!item.keepOpen) {
commandPaletteStore.closeCommandPalette();
}
}
</script>
<template>
<CommandDialog v-model:open="commandPaletteStore.isCommandPaletteOpen">
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>{{ $t('command-palette.no-result') }}</CommandEmpty>
<!-- <CommandGroup heading="Suggestions">
<CommandItem value="calendar">
Calendar
</CommandItem>
<CommandItem value="search-emoji">
Search Emoji
</CommandItem>
<CommandItem value="calculator">
Calculator
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem value="profile">
Profile
</CommandItem>
<CommandItem value="billing">
Billing
</CommandItem>
<CommandItem value="settings">
Settings
</CommandItem>
</CommandGroup> -->
<CommandGroup v-for="section in commandSections" :key="section.title" :heading="section.title">
<CommandItem v-for="item in section.items" :key="item.label" :value="item.label" @select="handleSelectCommand({ item })">
<Icon :name="item.icon" class="mr-2 size-4" />
{{ item.label }}
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
</template>

View File

@@ -0,0 +1,18 @@
import { map } from 'lodash-es';
export const locales = [
{
key: 'en',
file: 'en',
name: 'English',
switchToLabel: 'Change language to English',
},
{
key: 'fr',
file: 'fr',
name: 'Français',
switchToLabel: 'Changer la langue en Français',
},
] as const;
export const localeKeys = map(locales, 'key');

View File

@@ -0,0 +1,96 @@
import type { ParentComponent } from 'solid-js';
import type { LocaleKey } from './i18n.types';
import { joinUrlPaths } from '@corentinth/chisels';
import * as i18n from '@solid-primitives/i18n';
import { makePersisted } from '@solid-primitives/storage';
import { useNavigate } from '@solidjs/router';
import { merge } from 'lodash-es';
import { createContext, createResource, createSignal, Show, useContext } from 'solid-js';
import defaultDict from '../../locales/en.json';
import { locales } from './i18n.constants';
export {
useI18n,
};
type RawDictionary = typeof defaultDict;
type Dictionary = i18n.Flatten<RawDictionary>;
const RootI18nContext = createContext<{
t: i18n.Translator<Dictionary>;
getLocale: () => LocaleKey;
setLocale: (locale: LocaleKey) => void;
locales: typeof locales;
} | undefined>(undefined);
function useI18n() {
const context = useContext(RootI18nContext);
const navigate = useNavigate();
if (!context) {
throw new Error('I18n context not found');
}
const { t, getLocale, setLocale, locales } = context;
return {
t,
getLocale,
setLocale,
locales,
createLocalizedUrl: ({ path }: { path: string }) => {
const newPath = joinUrlPaths(getLocale(), path);
return `/${newPath}`;
},
changeLocale: (locale: LocaleKey) => {
setLocale(locale);
const pathWithoutLocale = location.pathname.split('/').slice(2).join('/');
const newPath = joinUrlPaths(locale, pathWithoutLocale);
navigate(`/${newPath}`);
},
};
}
async function fetchDictionary(locale: LocaleKey): Promise<Dictionary> {
const dict: RawDictionary = (await import(`../../locales/${locale}.json`));
const mergedDict = merge({}, defaultDict, dict);
const flattened = i18n.flatten(mergedDict);
return flattened;
}
export function getBrowserLocale(): LocaleKey {
const browserLocale = navigator.language?.split('-')[0];
if (!browserLocale) {
return 'en';
}
return locales.find(locale => locale.key === browserLocale)?.key ?? 'en';
}
export const RootI18nProvider: ParentComponent = (props) => {
const browserLocale = getBrowserLocale();
const [getLocale, setLocale] = makePersisted(createSignal<LocaleKey>(browserLocale), { name: 'it_tools_locale', storage: localStorage });
const [dict] = createResource(getLocale, fetchDictionary);
return (
<Show when={dict()}>
{dict => (
<RootI18nContext.Provider
value={{
t: i18n.translator(dict),
getLocale,
setLocale,
locales,
}}
>
{props.children}
</RootI18nContext.Provider>
)}
</Show>
);
};

View File

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

View File

@@ -0,0 +1,87 @@
import type { Component } from 'solid-js';
import { A } from '@solidjs/router';
import { useCommandPalette } from '../command-palette/command-palette.provider';
import { useI18n } from '../i18n/i18n.provider';
import { useToolsStore } from '../tools/tools.store';
import { Badge } from '../ui/components/badge';
import { Button } from '../ui/components/button';
import { Card, CardDescription, CardHeader, CardTitle } from '../ui/components/card';
import { cn } from '../ui/utils/cn';
export const HomePage: Component = () => {
const { t } = useI18n();
const { getTools } = useToolsStore();
const { openCommandPalette } = useCommandPalette();
return (
<div>
<div class="w-full bg-[linear-gradient(to_right,#80808010_1px,transparent_1px),linear-gradient(to_bottom,#80808010_1px,transparent_1px)] bg-[size:48px_48px] pt-20">
<div class="flex justify-center gap-24 items-center p-6">
<div class="max-w-xl flex flex-col gap-6 ">
<div class="flex items-center gap-2">
<Badge class="text-primary bg-primary/10 hover:bg-primary/10">
{t('home.open-source')}
</Badge>
<Badge class="text-primary bg-primary/10 hover:bg-primary/10">
{t('home.free')}
</Badge>
<Badge class="text-primary bg-primary/10 hover:bg-primary/10">
{t('home.self-hostable')}
</Badge>
</div>
<h1 class="text-5xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250">
<span class="font-bold ">IT</span>
<span class="text-90% text-primary font-extrabold border border-5px leading-none border-current rounded-xl px-2 py-0.5 ml-3">TOOLS</span>
</h1>
<p class="text-xl text-muted-foreground">
{t('app.description')}
</p>
<div class="flex items-center gap-4">
<Button variant="default" as={A} href="tools">
{t('home.all-tools')}
<div class="i-tabler-arrow-right ml-2 text-base"></div>
</Button>
<Button variant="outline" onClick={openCommandPalette}>
<div class="i-tabler-search mr-2 text-base" />
{t('home.search-tools')}
</Button>
</div>
</div>
<div class="relative hidden md:block">
<div class="absolute top-4 left-0 w-full h-full flex items-center justify-center blur-2xl rounded-full opacity-20 bg-gradient-to-br from-primary to-transparent" />
<div class="i-tabler-terminal text-9xl text-primary m-8" />
</div>
</div>
<div class="bg-gradient-to-t dark:from-background to-transparent h-24 mt-24"></div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 max-w-1200px mx-auto p-6">
{getTools().map(tool => (
<A href={tool.slug} class="h-full">
<Card class="hover:(shadow-md transform scale-101) transition-transform h-full">
<CardHeader>
<div class={cn(tool.icon, 'size-12 text-muted-foreground/60')} />
<CardTitle class="text-base font-semibold">
{tool.name}
</CardTitle>
<CardDescription>
{tool.description}
</CardDescription>
</CardHeader>
</Card>
</A>
))}
</div>
</div>
);
};

View File

@@ -1,21 +0,0 @@
// @vitest-environment nuxt
import { describe, expect, test } from 'vitest';
import { useRefreshableState } from './useRefreshableState';
describe('useRefreshableState composables', () => {
describe('useRefreshableState', () => {
test('the tuple provided by useRefreshableState contain the state that is the result of the provided function and a refresh function', () => {
let index = 0;
const [state, refresh] = useRefreshableState('key', () => ++index);
expect(state.value).to.equal(1);
expect(index).to.equal(1);
refresh();
expect(state.value).to.equal(2);
expect(index).to.equal(2);
});
});
});

View File

@@ -1,12 +0,0 @@
import { get } from '@vueuse/core';
export function useRefreshableState<T>(key: string, getState: () => T | Ref<T>) {
const state = useState(key, getState);
const refresh = () => {
const value = getState();
state.value = get(value);
};
return [state, refresh] as const;
}

View File

@@ -0,0 +1,37 @@
import type { Accessor, Component, ComponentProps } from 'solid-js';
import { Button } from '@/modules/ui/components/button';
import { omit } from 'lodash-es';
import { Show, splitProps } from 'solid-js';
import { useCopy } from './copy';
export const CopyButton: Component<{ textToCopy: Accessor<string | number>; toastMessage?: string } & ComponentProps<typeof Button>> = (props) => {
const [localProps, buttonProps] = splitProps(props, ['textToCopy', 'toastMessage']);
const { copy, getIsJustCopied } = useCopy(localProps.textToCopy, { toastMessage: localProps.toastMessage });
return (
<Button onClick={copy} {...omit(buttonProps, ['textToCopy', 'toastMessage'])}>
<Show
when={buttonProps.children}
fallback={(
getIsJustCopied()
? (
<>
<div class="i-tabler-check mr-2 text-base" />
Copied!
</>
)
: (
<>
<div class="i-tabler-copy mr-2 text-base" />
Copy to clipboard
</>
)
)}
>
{buttonProps.children}
</Show>
</Button>
);
};

View File

@@ -0,0 +1,23 @@
import type { Accessor } from 'solid-js';
import { createSignal } from 'solid-js';
import { toast } from '../../ui/components/sonner';
export { useCopy, writeTextToClipboard };
function writeTextToClipboard({ text }: { text: string }) {
return navigator.clipboard.writeText(text);
}
function useCopy(getText: Accessor<string | number>, { toastMessage = 'Copied to clipboard' }: { toastMessage?: string } = {}) {
const [getIsJustCopied, setIsJustCopied] = createSignal(false);
return {
getIsJustCopied,
copy: () => {
writeTextToClipboard({ text: String(getText()) });
setIsJustCopied(true);
setTimeout(() => setIsJustCopied(false), 2000);
toast(toastMessage);
},
};
}

View File

@@ -0,0 +1,33 @@
import { describe, expect, test } from 'vitest';
import { createRefreshableSignal } from './signals';
describe('signals', () => {
describe('createRefreshableSignal', () => {
test('the state initially has the value returned by the getter', () => {
const [getState] = createRefreshableSignal(() => 42);
expect(getState()).to.eql(42);
});
test('calling the refresh function updates the state', () => {
let value = 0;
const [getState, refresh] = createRefreshableSignal(() => value++);
expect(getState()).to.eql(0);
refresh();
expect(getState()).to.eql(1);
expect(getState()).to.eql(1);
});
test('the state can be muted using the setState function', () => {
const [getState, , { setState }] = createRefreshableSignal(() => 0);
expect(getState()).to.eql(0);
setState(42);
expect(getState()).to.eql(42);
});
});
});

View File

@@ -0,0 +1,13 @@
import { createSignal } from 'solid-js';
export { createRefreshableSignal };
function createRefreshableSignal<T>(getValue: () => T) {
const [getState, setState] = createSignal<T>(getValue());
return [
getState,
() => setState(() => getValue()),
{ setState },
] as const;
}

View File

@@ -0,0 +1,30 @@
import type { Component, ParentComponent } from 'solid-js';
import { cn } from '@/modules/ui/utils/cn';
export const ToolHeader: ParentComponent<{ name: string; description: string; icon: string }> = (props) => {
return (
<div>
<div class="w-full bg-[linear-gradient(to_right,#80808010_1px,transparent_1px),linear-gradient(to_bottom,#80808010_1px,transparent_1px)] bg-[size:48px_48px] pt-12">
<div class="flex gap-4 mb-8 max-w-1200px mx-auto px-6 items-start flex-col md:flex-row md:items-center">
<div class="bg-card p-4 rounded-lg">
<div class={cn(props.icon, 'size-8 md:size-12 text-muted-foreground')} />
</div>
<div>
<h1 class="text-xl font-semibold">
{props.name}
</h1>
<div class="text-muted-foreground text-base">
{props.description}
</div>
</div>
</div>
<div class="bg-gradient-to-t dark:from-background to-transparent h-24 mt-12 mb--24"></div>
</div>
{props.children}
</div>
);
};

View File

@@ -0,0 +1,6 @@
{
"name": "Random Port Generator",
"description": "Generate a random port number outside of the reserved ports range (0-1023).",
"refresh": "Refresh port",
"copy-toast": "Port copied to clipboard"
}

View File

@@ -0,0 +1,49 @@
import type { Component } from 'solid-js';
import { CopyButton } from '@/modules/shared/copy/copy-button';
import { createRefreshableSignal } from '@/modules/shared/signals';
import { Button } from '@/modules/ui/components/button';
import { Card, CardContent, CardHeader } from '@/modules/ui/components/card';
import { ToolHeader } from '../../components/tool-header';
import { useCurrentTool } from '../../tools.provider';
import defaultDictionary from './locales/en.json';
import { generateRandomPort } from './random-port-generator.services';
const RandomPortGenerator: Component = () => {
const [getPort, refreshPort] = createRefreshableSignal(generateRandomPort);
const { t, getTool } = useCurrentTool({ defaultDictionary });
return (
<div>
<ToolHeader {...getTool()} />
<div class="max-w-600px mx-auto px-6">
<Card>
<CardHeader class="flex justify-between items-center">
<div class="my-6 text-center">
<div class="text-base text-muted-foreground mb-2">
Random port:
</div>
<div class="text-4xl font-mono">
{getPort()}
</div>
</div>
<div class="flex gap-2 md:gap-4 mt-4 flex-col md:flex-row w-full justify-center">
<Button onClick={refreshPort} variant="outline">
<div class="i-tabler-refresh mr-2 text-base text-muted-foreground" />
{t('refresh')}
</Button>
<CopyButton textToCopy={getPort} toastMessage={t('copy-toast')} />
</div>
</CardHeader>
</Card>
</div>
</div>
);
};
export default RandomPortGenerator;

View File

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

View File

@@ -0,0 +1,9 @@
import { defineTool } from '../../tools.models';
export const randomPortGeneratorTool = defineTool({
slug: 'random-port-generator',
entryFile: () => import('./random-port-generator.page'),
icon: 'i-tabler-server',
createdAt: new Date('2024-10-03'),
dirName: 'random-port-generator',
});

View File

@@ -0,0 +1,10 @@
{
"name": "Token Generator",
"description": "Generate random string with the characters you want, uppercase, lowercase letters, numbers and/or symbols.",
"uppercase": "Uppercase letters (A-Z)",
"lowercase": "Lowercase letters (a-z)",
"numbers": "Numbers (0-9)",
"symbols": "Special characters (!@#...)",
"length": "Length",
"result-placeholder": "Your token will appear here"
}

View File

@@ -0,0 +1,10 @@
{
"name": "Générateur de token",
"description": "Génère une chaîne de caractères aléatoire, contrôlez les caractères que vous voulez, lettres majuscules, minuscules, chiffres et/ou symboles.",
"uppercase": "Lettres majuscules (A-Z)",
"lowercase": "Lettres minuscules (a-z)",
"numbers": "Chiffres (0-9)",
"symbols": "Caractères spéciaux (!@#...)",
"length": "Longueur",
"result-placeholder": "Le token apparaîtra ici"
}

View File

@@ -7,7 +7,6 @@ export function createToken({
withSymbols = false,
length = 64,
alphabet,
exclude,
sample = sampleImpl,
}: {
withUppercase?: boolean;
@@ -16,7 +15,6 @@ export function createToken({
withSymbols?: boolean;
length?: number;
alphabet?: string;
exclude?: string | string[];
sample?: (str: string) => string;
}) {
const allAlphabet = alphabet ?? [
@@ -26,8 +24,5 @@ export function createToken({
withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '',
].join('');
const charsToExclude = exclude ? (Array.isArray(exclude) ? exclude.join('') : exclude) : '';
const filteredAlphabet = allAlphabet.split('').filter(char => !charsToExclude.includes(char)).join('');
return times(length, () => sample(filteredAlphabet)).join('');
return times(length, () => sample(allAlphabet)).join('');
}

View File

@@ -0,0 +1,109 @@
import { CopyButton } from '@/modules/shared/copy/copy-button';
import { createRefreshableSignal } from '@/modules/shared/signals';
import { Button } from '@/modules/ui/components/button';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/modules/ui/components/card';
import { Switch, SwitchControl, SwitchLabel, SwitchThumb } from '@/modules/ui/components/switch';
import { TextArea } from '@/modules/ui/components/textarea';
import { TextFieldRoot } from '@/modules/ui/components/textfield';
import { type Component, createSignal } from 'solid-js';
import { ToolHeader } from '../../components/tool-header';
import { useCurrentTool } from '../../tools.provider';
import defaultDictionary from './locales/en.json';
import { createToken } from './token-generator.models';
const TokenGeneratorTool: Component = () => {
const [getUseUpperCase, setUseUpperCase] = createSignal(true);
const [getUseLowerCase, setUseLowerCase] = createSignal(true);
const [getUseNumbers, setUseNumbers] = createSignal(true);
const [getUseSpecialCharacters, setUseSpecialCharacters] = createSignal(false);
const [getLength] = createSignal(64);
const { t, getTool } = useCurrentTool({ defaultDictionary });
const [getToken, refreshToken] = createRefreshableSignal(() => createToken({
withUppercase: getUseUpperCase(),
withLowercase: getUseLowerCase(),
withNumbers: getUseNumbers(),
withSymbols: getUseSpecialCharacters(),
length: getLength(),
}));
return (
<div>
<ToolHeader {...getTool()} />
<div class="mx-auto max-w-1200px p-6 flex flex-col gap-4 md:flex-row items-start">
<Card>
<CardHeader class="border-b border-border">
<CardTitle class="text-muted-foreground">
Configuration
</CardTitle>
</CardHeader>
<CardContent class="pt-6 flex flex-col gap-2">
<Switch class="flex items-center gap-2" checked={getUseUpperCase()} onChange={setUseUpperCase}>
<SwitchControl>
<SwitchThumb />
</SwitchControl>
<SwitchLabel class="text-sm font-medium leading-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70">
{t('uppercase')}
</SwitchLabel>
</Switch>
<Switch class="flex items-center gap-2" checked={getUseLowerCase()} onChange={setUseLowerCase}>
<SwitchControl>
<SwitchThumb />
</SwitchControl>
<SwitchLabel class="text-sm font-medium leading-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70">
{t('lowercase')}
</SwitchLabel>
</Switch>
<Switch class="flex items-center gap-2" checked={getUseNumbers()} onChange={setUseNumbers}>
<SwitchControl>
<SwitchThumb />
</SwitchControl>
<SwitchLabel class="text-sm font-medium leading-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70">
{t('numbers')}
</SwitchLabel>
</Switch>
<Switch class="flex items-center gap-2" checked={getUseSpecialCharacters()} onChange={setUseSpecialCharacters}>
<SwitchControl>
<SwitchThumb />
</SwitchControl>
<SwitchLabel class="text-sm font-medium leading-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70">
{t('symbols')}
</SwitchLabel>
</Switch>
</CardContent>
</Card>
<Card class="flex-1">
<CardHeader class="border-b border-border flex justify-between flex-row py-3 items-center">
<CardTitle class="text-muted-foreground">
Your token
</CardTitle>
<div class="flex justify-center items-center gap-2">
<Button onClick={refreshToken} variant="outline">
<div class="i-tabler-refresh mr-2 text-base text-muted-foreground" />
Refresh token
</Button>
<CopyButton textToCopy={getToken} toastMessage={t('copy-toast')} />
</div>
</CardHeader>
<CardContent class="pt-6 text-center">
{getToken()}
</CardContent>
</Card>
</div>
</div>
);
};
export default TokenGeneratorTool;

View File

@@ -2,8 +2,8 @@ import { defineTool } from '../../tools.models';
export const tokenGeneratorTool = defineTool({
slug: 'token-generator',
entryFile: './token-generator.vue',
entryFile: () => import('./token-generator.page'),
icon: 'i-tabler-key',
createdAt: new Date('2024-02-13'),
currentDirUrl: import.meta.url,
dirName: 'token-generator',
});

View File

@@ -1,362 +0,0 @@
<script setup lang="ts">
import { times } from 'lodash-es';
import { useRefreshableState } from '~/src/modules/shared/composables/useRefreshableState';
import { cn } from '~/src/modules/shared/style/cn';
import { Button } from '~/src/modules/ui/components/button';
import { Checkbox } from '~/src/modules/ui/components/checkbox';
import NumberField from '~/src/modules/ui/components/number-field/NumberField.vue';
import NumberFieldContent from '~/src/modules/ui/components/number-field/NumberFieldContent.vue';
import NumberFieldDecrement from '~/src/modules/ui/components/number-field/NumberFieldDecrement.vue';
import NumberFieldIncrement from '~/src/modules/ui/components/number-field/NumberFieldIncrement.vue';
import NumberFieldInput from '~/src/modules/ui/components/number-field/NumberFieldInput.vue';
import Slider from '~/src/modules/ui/components/slider/Slider.vue';
import { Textarea } from '~/src/modules/ui/components/textarea';
import ToggleGroup from '~/src/modules/ui/components/toggle-group/ToggleGroup.vue';
import ToggleGroupItem from '~/src/modules/ui/components/toggle-group/ToggleGroupItem.vue';
import { createToken } from './token-generator.models';
definePageMeta({
layout: 'sidenav',
});
const formats = {
raw: {
label: 'Raw',
format: ({ tokens }: { tokens: string[] }) => tokens.join('\n'),
},
JSON: {
label: 'JSON',
format: ({ tokens }: { tokens: string[] }) => JSON.stringify(tokens),
},
};
const similarChars = ['I', 'l', '1', 'O', '0'];
const withUppercase = ref(true);
const withLowercase = ref(true);
const withNumbers = ref(true);
const withSymbols = ref(false);
const excludeSimilar = ref(false);
const length = ref(64);
const format = ref<keyof typeof formats>('raw');
const quantity = ref(1);
const tab = ref<'generator' | 'about'>('generator');
function reset() {
withUppercase.value = true;
withLowercase.value = true;
withNumbers.value = true;
withSymbols.value = false;
excludeSimilar.value = false;
length.value = 64;
format.value = 'raw';
quantity.value = 1;
}
function generateToken() {
return createToken({
withUppercase: withUppercase.value,
withLowercase: withLowercase.value,
withNumbers: withNumbers.value,
withSymbols: withSymbols.value,
length: length.value,
exclude: excludeSimilar.value ? similarChars : [],
});
}
const [token, refreshToken] = useRefreshableState(
'token-generator:token',
() => {
const tokens = times(
quantity.value,
generateToken,
);
return formats[format.value].format({ tokens });
},
);
watch([
withUppercase,
withLowercase,
withNumbers,
withSymbols,
length,
format,
quantity,
excludeSimilar,
], refreshToken);
// const { copy: copyToken } = useCopy({ source: token, notificationText: 'Token copied to clipboard' });
</script>
<template>
<div class="flex flex-col h-full">
<div class="p-6 pb-0 bg-white dark:bg-background border-b">
<h1 class="text-2xl">
{{ $t('tools.token-generator.title') }}
</h1>
<p class="text-muted-foreground">
{{ $t('tools.token-generator.description') }}
</p>
<div class="mt-2 flex gap-4">
<Button variant="link" :class="cn('text-muted-foreground font-sm pb-0 px-0 rounded-none hover:no-underline', { 'border-b border-b-foreground text-foreground': tab === 'generator' })" @click="tab = 'generator'">
Generator
</Button>
<Button variant="link" :class="cn('border-b border-b-transparent text-muted-foreground font-sm pb-0 px-0 rounded-none hover:no-underline transition', { 'border-b-foreground text-foreground': tab === 'about' })" @click="tab = 'about'">
About
</Button>
</div>
</div>
<div v-if="tab === 'about'" class="p-6 bg-white dark:bg-background">
<div class="prose dark:prose-invert mx-auto mb-20">
<h2 id="about-the-random-token-generator">
About The Random Token Generator
</h2>
<p>
The Random Token Generator on Crucials.tools is designed to meet the
modern developers need for quick, secure, and customizable tokens.
Whether you&#39;re working on web applications, software development, system
administration, or creating secure passwords, our tool provides a
straightforward solution to create tokens that fit your specifications
perfectly.
</p>
<h2 id="why-are-tokens-important-">
Why Are Tokens Important?
</h2>
<p>
In the realm of software development and IT, tokens play a crucial role in
maintaining security and user authentication. They are essential for:
</p>
<ul>
<li>
<p>
<strong>Session Management:</strong> Tokens are used to manage user sessions,
allowing systems to recognize and verify users over multiple requests.
</p>
</li>
<li>
<p>
<strong>API Security:</strong> Securely accessing APIs often requires tokens to
ensure that the requestor has the necessary permissions.
</p>
</li>
<li>
<p>
<strong>Randomized Data Generation:</strong> Tokens can be used in testing
environments to generate randomized data inputs, helping developers
identify potential issues.
</p>
</li>
</ul>
<h2 id="ideal-for-generating-secure-passwords">
Ideal for Generating Secure Passwords
</h2>
<p>
In addition to creating tokens, our Random Token Generator is an excellent
tool for generating strong, secure passwords. With options to include a
mix of uppercase letters, lowercase letters, numbers, and special symbols,
you can create robust passwords that enhance security for any application
or service.
</p>
<h2 id="how-to-use-the-token-generator">
How to Use the Token Generator
</h2>
<p>Using our Token Generator is simple:</p>
<ol>
<li>
<p>
<strong>Select the characters you want:</strong> Choose from uppercase letters,
lowercase letters, numbers, and special symbols based on your token or
password requirements.
</p>
</li>
<li>
<p>
<strong>Choose the length:</strong> Determine how long you want your token or
password to be, depending on the level of complexity and security needed.
</p>
</li>
<li>
<p>
<strong>Generate with a click:</strong> Once your options are set, click &#39;Generate&#39;
to receive your token or password instantly.
</p>
</li>
</ol>
<h2 id="benefits-of-using-crucials-tools-token-generator">
Benefits of Using Crucials.tools Token Generator
</h2>
<ul>
<li>
<p>
<strong>Customization:</strong> Our tool allows for extensive customization, ensuring
you get exactly what you need for your project or personal security.
</p>
</li>
<li>
<p>
<strong>Speed:</strong> Generate tokens or passwords in seconds, speeding up your
development process and enhancing your security posture.
</p>
</li>
<li>
<p>
<strong>No Installation Required:</strong> As a web-based tool, there is no need to
install any software, making it accessible from anywhere at any time.
</p>
</li>
</ul>
<h2 id="perfect-tool-for-developers-and-people-in-it">
Perfect Tool For Developers and people in IT
</h2>
<p>
Whether you are developing a new application, testing existing
functionality, needing to ensure secure access, or generating a strong
password, our Random Token Generator is the perfect tool. It is built to
be reliable and secure, providing peace of mind in various development and
personal security scenarios.
</p>
</div>
</div>
<div v-if="tab === 'generator'" class="h-full flex-1 flex flex-col md:flex-row">
<div class="md:max-w-[360px] bg-white dark:bg-background h-full border-b md:border-b-none md:border-r">
<div class="px-6 pt-4 pb-6 ">
<div class="text-muted-foreground mb-3">
Token configuration
</div>
<div class="flex gap-2 items-center mb-1">
<Checkbox id="use-uppercase" v-model:checked="withUppercase" />
<label for="use-uppercase">
{{ $t('tools.token-generator.use-uppercase') }}
<span class="text-muted-foreground">
(A-Z)
</span>
</label>
</div>
<div class="flex gap-2 items-center mb-1">
<Checkbox id="use-lowercase" v-model:checked="withLowercase" />
<label for="use-lowercase">
{{ $t('tools.token-generator.use-lowercase') }}
<span class="text-muted-foreground">
(a-z)
</span>
</label>
</div>
<div class="flex gap-2 items-center mb-1">
<Checkbox id="use-numbers" v-model:checked="withNumbers" />
<label for="use-numbers">
{{ $t('tools.token-generator.use-numbers') }}
<span class="text-muted-foreground">
(0-9)
</span>
</label>
</div>
<div class="flex gap-2 items-center mb-1">
<Checkbox id="use-symbols" v-model:checked="withSymbols" />
<label for="use-symbols">
{{ $t('tools.token-generator.use-symbols') }}
<span class="text-muted-foreground">
(!@,]*...)
</span>
</label>
</div>
<div class="flex gap-2 items-center mb-1">
<Checkbox id="exclude-similar" v-model:checked="excludeSimilar" />
<label for="exclude-similar">
{{ $t('tools.token-generator.exclude-similar') }}
<span class="text-muted-foreground">
({{ similarChars.join('') }})
</span>
</label>
</div>
<div class="flex gap-4 items-center mb-3 mt-4">
<label for="length" class="w-20 shrink-0">{{ $t('tools.token-generator.length') }}</label>
<NumberField id="length" v-model="length" :min="1" :max="1024">
<NumberFieldContent class="flex-1">
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</div>
<Slider
:model-value="[length]"
:max="512"
:min="1"
:step="1"
@update:model-value="(v) => length = v?.[0] ?? 1"
/>
</div>
<hr>
<div class="px-6 pt-4 pb-6 border-b">
<div class="text-muted-foreground mb-3">
Output configuration
</div>
<div class="flex items-center mb-2 gap-4">
<div class="w-20 shrink-0">
{{ $t('tools.token-generator.format') }}
</div>
<ToggleGroup v-model="format" type="single" variant="outline">
<ToggleGroupItem v-for="({ label }, key) in formats" :key="key" :value="key">
{{ label }}
</ToggleGroupItem>
</ToggleGroup>
</div>
<div class="flex items-center gap-4">
<div class="w-20 shrink-0">
{{ $t('tools.token-generator.quantity') }}
</div>
<NumberField v-model="quantity" :min="1" :max="100">
<NumberFieldContent class="flex-1">
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</div>
</div>
<div class="p-6">
<Button class="w-full" variant="secondary" @click="reset">
Reset configuration
</Button>
</div>
</div>
<div class="flex-1 p-6 pt-4 bg-white dark:bg-background pb-20">
<div class="text-muted-foreground mb-3">
Generated token
</div>
<Textarea v-model="token" rows="12" class="font-mono" readonly :placeholder="$t('tools.token-generator.placeholder')" />
<div class="flex md:items-center mt-2 gap-2 flex-col md:flex-row">
<Button variant="secondary" @click="refreshToken">
<Icon name="i-tabler-refresh" class="mr-2" />
{{ $t('tools.token-generator.refresh') }}
</Button>
<Button variant="secondary">
<Icon name="i-tabler-copy" class="mr-2" />
Copy to clipboard
</Button>
</div>
</div>
</div>
</div>
</template>

View File

@@ -1,22 +0,0 @@
import { defineNuxtModule, extendPages } from '@nuxt/kit';
import { toolDefinitions } from '../tools.registry';
export default defineNuxtModule({
meta: {
name: 'tools',
},
setup(options, nuxt) {
nuxt.hook('pages:extend', (pages) => {
pages.push(...toolDefinitions.map((tool) => {
return {
path: `/${tool.slug}`,
file: tool.entryFile,
name: tool.slug,
meta: {
toolKey: tool.key,
},
};
}));
});
},
});

View File

@@ -0,0 +1,43 @@
import { useI18n } from '@/modules/i18n/i18n.provider';
import { safely } from '@corentinth/chisels';
import { useParams } from '@solidjs/router';
import { type Component, createResource, lazy, Show } from 'solid-js';
import { CurrentToolProvider } from '../tools.provider';
import { getToolDefinitionBySlug } from '../tools.registry';
export const ToolPage: Component = () => {
const params = useParams();
const { getLocale, t } = useI18n();
const toolDefinition = getToolDefinitionBySlug({ slug: params.toolSlug });
const ToolComponent = lazy(toolDefinition.entryFile);
const [toolDict] = createResource(getLocale, async (locale) => {
const [dict, error] = await safely(import(`../definitions/${toolDefinition.dirName}/locales/${locale}.json`));
if (error) {
console.error(error);
}
return dict ?? { default: {} };
});
return (
<Show when={toolDict()}>
{toolLocaleDict => (
<CurrentToolProvider
toolLocaleDict={toolLocaleDict}
tool={() => ({
icon: toolDefinition.icon,
dirName: toolDefinition.dirName,
createdAt: toolDefinition.createdAt,
name: t(`tools.${toolDefinition.slug}.name` as any),
description: t(`tools.${toolDefinition.slug}.description` as any),
})}
>
<ToolComponent />
</CurrentToolProvider>
)}
</Show>
);
};

View File

@@ -1,18 +1,16 @@
export function defineTool(toolDefinition: {
import type { Component } from 'solid-js';
export { defineTool };
function defineTool(toolDefinition: {
slug: string;
entryFile: string;
currentDirUrl: string;
entryFile: () => Promise<{ default: Component }>;
dirName: string;
icon: string;
createdAt: Date;
}) {
const entryFile = new URL(toolDefinition.entryFile, toolDefinition.currentDirUrl).pathname;
const baseGithubUrlPath = entryFile.match(/(\/tools\/.*)$/)?.[1];
const entryFileGithubUrl = `https://github.com/CorentinTh/crucials-tools/blob/main${baseGithubUrlPath}`;
return {
...toolDefinition,
key: toolDefinition.slug,
entryFile,
entryFileGithubUrl,
};
}

View File

@@ -0,0 +1,35 @@
import type { Accessor, ParentComponent } from 'solid-js';
import type { ToolDefinition } from './tools.types';
import { flatten, translator } from '@solid-primitives/i18n';
import { merge } from 'lodash-es';
import { createContext, useContext } from 'solid-js';
type ToolProviderContext = {
toolLocaleDict: Accessor<Record<string, string>>;
tool: Accessor<Pick<ToolDefinition, 'icon' | 'dirName' | 'createdAt'> & { name: string; description: string }>;
};
const CurrentToolContext = createContext<ToolProviderContext>();
export function useCurrentTool<T>({ defaultDictionary }: { defaultDictionary: T }) {
const context = useContext(CurrentToolContext);
if (!context) {
throw new Error('useCurrentTool must be used within a CurrentToolProvider');
}
const { toolLocaleDict, tool } = context;
return {
t: translator(() => flatten(merge({}, defaultDictionary, toolLocaleDict()))),
getTool: tool,
};
}
export const CurrentToolProvider: ParentComponent<ToolProviderContext> = (props) => {
return (
<CurrentToolContext.Provider value={props}>
{props.children}
</CurrentToolContext.Provider>
);
};

View File

@@ -1,5 +1,17 @@
import { keyBy, map } from 'lodash-es';
import { randomPortGeneratorTool } from './definitions/random-port-generator/random-port-generator.tool';
import { tokenGeneratorTool } from './definitions/token-generator/token-generator.tool';
export const toolDefinitions = [
tokenGeneratorTool,
randomPortGeneratorTool,
];
export const toolSlugs = map(toolDefinitions, 'slug');
export const toolDefinitionBySlug = keyBy(toolDefinitions, 'slug');
export { getToolDefinitionBySlug };
function getToolDefinitionBySlug({ slug }: { slug: string }) {
return toolDefinitionBySlug[slug];
}

View File

@@ -1,36 +1,19 @@
import { joinUrlPaths } from '@corentinth/chisels';
import { createMemo } from 'solid-js';
import { useI18n } from '../i18n/i18n.provider';
import { toolDefinitions } from './tools.registry';
export const useToolsStore = defineStore('tools', () => {
const { t, locale } = useI18n();
export { useToolsStore };
const localizedTools = computed(() => {
return toolDefinitions.map((tool) => {
const { key, slug } = tool;
function useToolsStore() {
const { t } = useI18n();
return {
...tool,
title: t(`tools.${key}.title`),
description: t(`tools.${key}.description`),
path: `/${joinUrlPaths(locale.value, slug)}`,
};
});
});
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 {
tools: localizedTools,
getToolByKey({ key }: { key: unknown }) {
if (typeof key !== 'string') {
throw new TypeError('Invalid key');
}
const tool = localizedTools.value.find(tool => tool.key === key);
if (!tool) {
throw new Error('Tool not found');
}
return tool;
},
};
});
return { getTools };
}

View File

@@ -0,0 +1,6 @@
import type { Flatten, Translator } from '@solid-primitives/i18n';
import type { defineTool } from './tools.models';
export type ToolI18nFactory = <T extends Record<string, string>>(args: { defaultDictionary: T }) => { t: Translator<Flatten<T>> };
export type ToolDefinition = ReturnType<typeof defineTool>;

View File

@@ -0,0 +1,37 @@
import type { VariantProps } from 'class-variance-authority';
import { cn } from '@/modules/ui/utils/cn';
import { cva } from 'class-variance-authority';
import { type ComponentProps, splitProps } from 'solid-js';
export const badgeVariants = cva(
'inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-shadow focus-visible:(outline-none ring-1.5 ring-ring)',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: 'bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'border text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
);
export function Badge(props: ComponentProps<'div'> & VariantProps<typeof badgeVariants>) {
const [local, rest] = splitProps(props, ['class', 'variant']);
return (
<div
class={cn(
badgeVariants({
variant: local.variant,
}),
local.class,
)}
{...rest}
/>
);
}

View File

@@ -1,16 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
import { type BadgeVariants, badgeVariants } from '.';
const props = defineProps<{
variant?: BadgeVariants['variant'];
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot />
</div>
</template>

View File

@@ -1,25 +0,0 @@
import { cva, type VariantProps } from 'class-variance-authority';
export { default as Badge } from './Badge.vue';
export const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
);
export type BadgeVariants = VariantProps<typeof badgeVariants>;

View File

@@ -0,0 +1,60 @@
import type { ButtonRootProps } from '@kobalte/core/button';
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
import type { VariantProps } from 'class-variance-authority';
import type { ValidComponent } from 'solid-js';
import { cn } from '@/modules/ui/utils/cn';
import { Button as ButtonPrimitive } from '@kobalte/core/button';
import { cva } from 'class-variance-authority';
import { splitProps } from 'solid-js';
export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:(outline-none ring-1.5 ring-ring) disabled:(pointer-events-none opacity-50) bg-inherit',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:(bg-accent text-accent-foreground)',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 px-3 text-xs',
lg: 'h-10 px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
type buttonProps<T extends ValidComponent = 'button'> = ButtonRootProps<T> &
VariantProps<typeof buttonVariants> & {
class?: string;
};
export function Button<T extends ValidComponent = 'button'>(props: PolymorphicProps<T, buttonProps<T>>) {
const [local, rest] = splitProps(props as buttonProps, [
'class',
'variant',
'size',
]);
return (
<ButtonPrimitive
class={cn(
buttonVariants({
size: local.size,
variant: local.variant,
}),
local.class,
)}
{...rest}
/>
);
}

View File

@@ -1,26 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
import { Primitive, type PrimitiveProps } from 'radix-vue';
import { type ButtonVariants, buttonVariants } from '.';
type Props = {
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
class?: HTMLAttributes['class'];
} & PrimitiveProps;
const props = withDefaults(defineProps<Props>(), {
as: 'button',
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@@ -1,32 +0,0 @@
import { cva, type VariantProps } from 'class-variance-authority';
export { default as Button } from './Button.vue';
export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View File

@@ -0,0 +1,60 @@
import type { ComponentProps, ParentComponent } from 'solid-js';
import { cn } from '@/modules/ui/utils/cn';
import { splitProps } from 'solid-js';
export function Card(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ['class']);
return (
<div
class={cn(
'rounded-xl border bg-card text-card-foreground shadow',
local.class,
)}
{...rest}
/>
);
}
export function CardHeader(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ['class']);
return (
<div class={cn('flex flex-col space-y-1.5 p-6', local.class)} {...rest} />
);
}
export const CardTitle: ParentComponent<ComponentProps<'h1'>> = (props) => {
const [local, rest] = splitProps(props, ['class']);
return (
<h1
class={cn('font-semibold leading-none tracking-tight', local.class)}
{...rest}
/>
);
};
export const CardDescription: ParentComponent<ComponentProps<'h3'>> = (
props,
) => {
const [local, rest] = splitProps(props, ['class']);
return (
<h3 class={cn('text-sm text-muted-foreground', local.class)} {...rest} />
);
};
export function CardContent(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ['class']);
return <div class={cn('p-6 pt-0', local.class)} {...rest} />;
}
export function CardFooter(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ['class']);
return (
<div class={cn('flex items-center p-6 pt-0', local.class)} {...rest} />
);
}

View File

@@ -1,21 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div
:class="
cn(
'rounded-xl border bg-card text-card-foreground',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<p :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</p>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot />
</div>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot />
</div>
</template>

View File

@@ -1,18 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<h3
:class="
cn('font-semibold leading-none tracking-tight', props.class)
"
>
<slot />
</h3>
</template>

View File

@@ -1,6 +0,0 @@
export { default as Card } from './Card.vue';
export { default as CardContent } from './CardContent.vue';
export { default as CardDescription } from './CardDescription.vue';
export { default as CardFooter } from './CardFooter.vue';
export { default as CardHeader } from './CardHeader.vue';
export { default as CardTitle } from './CardTitle.vue';

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from 'radix-vue';
import { cn } from '@/src/modules/shared/style/cn';
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>();
const emits = defineEmits<CheckboxRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<CheckboxRoot
v-bind="forwarded"
:class="
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
props.class)"
>
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
<slot>
<Icon name="i-tabler-check" class="h-4 w-4" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>

View File

@@ -1 +0,0 @@
export { default as Checkbox } from './Checkbox.vue';

View File

@@ -0,0 +1,151 @@
import type {
CommandDialogProps,
CommandEmptyProps,
CommandGroupProps,
CommandInputProps,
CommandItemProps,
CommandListProps,
CommandRootProps,
} from 'cmdk-solid';
import type { ComponentProps, VoidProps } from 'solid-js';
import { cn } from '@/modules/ui/utils/cn';
import { Command as CommandPrimitive } from 'cmdk-solid';
import { splitProps } from 'solid-js';
import { Dialog, DialogContent } from './dialog';
export function Command(props: CommandRootProps) {
const [local, rest] = splitProps(props, ['class']);
return (
<CommandPrimitive
class={cn(
'flex size-full flex-col overflow-hidden bg-popover text-popover-foreground',
local.class,
)}
{...rest}
/>
);
}
export function CommandList(props: CommandListProps) {
const [local, rest] = splitProps(props, ['class']);
return (
<CommandPrimitive.List
class={cn(
'max-h-[300px] overflow-y-auto overflow-x-hidden p-1',
local.class,
)}
{...rest}
/>
);
}
export function CommandInput(props: VoidProps<CommandInputProps>) {
const [local, rest] = splitProps(props, ['class']);
return (
<div class="flex items-center border-b px-3" cmdk-input-wrapper="">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="mr-2 h-4 w-4 shrink-0 opacity-50"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0m18 11l-6-6"
/>
<title>Search</title>
</svg>
<CommandPrimitive.Input
class={cn(
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:(cursor-not-allowed opacity-50)',
local.class,
)}
{...rest}
/>
</div>
);
}
export function CommandItem(props: CommandItemProps) {
const [local, rest] = splitProps(props, ['class']);
return (
<CommandPrimitive.Item
class={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5! text-sm outline-none aria-selected:(bg-accent text-accent-foreground) aria-disabled:(pointer-events-none opacity-50)',
local.class,
)}
{...rest}
/>
);
}
export function CommandShortcut(props: ComponentProps<'span'>) {
const [local, rest] = splitProps(props, ['class']);
return (
<span
class={cn(
'ml-auto text-xs tracking-widest text-muted-foreground',
local.class,
)}
{...rest}
/>
);
}
export function CommandDialog(props: CommandDialogProps) {
const [local, rest] = splitProps(props, ['children']);
return (
<Dialog {...rest}>
<DialogContent class="overflow-hidden p-0">
<Command class="[&_[cmdk-group-heading]]:(px-2 font-medium text-muted-foreground) [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:(px-2 py-3) [&_[cmdk-item]_svg]:size-5">
{local.children}
</Command>
</DialogContent>
</Dialog>
);
}
export function CommandEmpty(props: CommandEmptyProps) {
const [local, rest] = splitProps(props, ['class']);
return (
<CommandPrimitive.Empty
class={cn('py-6 text-center text-sm', local.class)}
{...rest}
/>
);
}
export function CommandGroup(props: CommandGroupProps) {
const [local, rest] = splitProps(props, ['class']);
return (
<CommandPrimitive.Group
class={cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:(px-2 py-1.5 text-xs font-medium text-muted-foreground)',
local.class,
)}
{...rest}
/>
);
}
export function CommandSeparator(props: CommandEmptyProps) {
const [local, rest] = splitProps(props, ['class']);
return (
<CommandPrimitive.Separator
class={cn('-mx-1 h-px bg-border', local.class)}
{...rest}
/>
);
}

View File

@@ -1,30 +0,0 @@
<script setup lang="ts">
import type { ComboboxRootEmits, ComboboxRootProps } from 'radix-vue';
import { cn } from '@/src/modules/shared/style/cn';
import { ComboboxRoot, useForwardPropsEmits } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
open: true,
modelValue: '',
});
const emits = defineEmits<ComboboxRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxRoot
v-bind="forwarded"
:class="cn('flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground', props.class)"
>
<slot />
</ComboboxRoot>
</template>

View File

@@ -1,21 +0,0 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from 'radix-vue';
import { Dialog, DialogContent } from '@/src/modules/ui/components/dialog';
import { useForwardPropsEmits } from 'radix-vue';
import Command from './Command.vue';
const props = defineProps<DialogRootProps>();
const emits = defineEmits<DialogRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<Dialog v-bind="forwarded">
<DialogContent class="overflow-hidden p-0 shadow-lg">
<Command class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<slot />
</Command>
</DialogContent>
</Dialog>
</template>

View File

@@ -1,20 +0,0 @@
<script setup lang="ts">
import type { ComboboxEmptyProps } from 'radix-vue';
import { cn } from '@/src/modules/shared/style/cn';
import { ComboboxEmpty } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<ComboboxEmpty v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
<slot />
</ComboboxEmpty>
</template>

View File

@@ -1,29 +0,0 @@
<script setup lang="ts">
import type { ComboboxGroupProps } from 'radix-vue';
import { cn } from '@/src/modules/shared/style/cn';
import { ComboboxGroup, ComboboxLabel } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<ComboboxGroupProps & {
class?: HTMLAttributes['class'];
heading?: string;
}>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<ComboboxGroup
v-bind="delegatedProps"
:class="cn('overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', props.class)"
>
<ComboboxLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
{{ heading }}
</ComboboxLabel>
<slot />
</ComboboxGroup>
</template>

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
import { cn } from '@/src/modules/shared/style/cn';
import { ComboboxInput, type ComboboxInputProps, useForwardProps } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
defineOptions({
inheritAttrs: false,
});
const props = defineProps<ComboboxInputProps & {
class?: HTMLAttributes['class'];
}>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<div class="flex items-center border-b px-3" cmdk-input-wrapper>
<Icon name="i-tabler-search" class="mr-2 h-4 w-4 shrink-0 opacity-50" />
<ComboboxInput
v-bind="{ ...forwardedProps, ...$attrs }"
auto-focus
: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 disabled:opacity-50', props.class)"
/>
</div>
</template>

View File

@@ -1,26 +0,0 @@
<script setup lang="ts">
import type { ComboboxItemEmits, ComboboxItemProps } from 'radix-vue';
import { cn } from '@/src/modules/shared/style/cn';
import { ComboboxItem, useForwardPropsEmits } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>();
const emits = defineEmits<ComboboxItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxItem
v-bind="forwarded"
:class="cn('relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', props.class)"
>
<slot />
</ComboboxItem>
</template>

View File

@@ -1,27 +0,0 @@
<script setup lang="ts">
import type { ComboboxContentEmits, ComboboxContentProps } from 'radix-vue';
import { cn } from '@/src/modules/shared/style/cn';
import { ComboboxContent, useForwardPropsEmits } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = withDefaults(defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>(), {
dismissable: false,
});
const emits = defineEmits<ComboboxContentEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<ComboboxContent v-bind="forwarded" :class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)">
<div role="presentation">
<slot />
</div>
</ComboboxContent>
</template>

View File

@@ -1,23 +0,0 @@
<script setup lang="ts">
import type { ComboboxSeparatorProps } from 'radix-vue';
import { cn } from '@/src/modules/shared/style/cn';
import { ComboboxSeparator } from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
</script>
<template>
<ComboboxSeparator
v-bind="delegatedProps"
:class="cn('-mx-1 h-px bg-border', props.class)"
>
<slot />
</ComboboxSeparator>
</template>

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
const props = defineProps<{
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)">
<slot />
</span>
</template>

View File

@@ -1,9 +0,0 @@
export { default as Command } from './Command.vue';
export { default as CommandDialog } from './CommandDialog.vue';
export { default as CommandEmpty } from './CommandEmpty.vue';
export { default as CommandGroup } from './CommandGroup.vue';
export { default as CommandInput } from './CommandInput.vue';
export { default as CommandItem } from './CommandItem.vue';
export { default as CommandList } from './CommandList.vue';
export { default as CommandSeparator } from './CommandSeparator.vue';
export { default as CommandShortcut } from './CommandShortcut.vue';

View File

@@ -0,0 +1,122 @@
import type {
DialogContentProps,
DialogDescriptionProps,
DialogTitleProps,
} from '@kobalte/core/dialog';
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js';
import { cn } from '@/modules/ui/utils/cn';
import { Dialog as DialogPrimitive } from '@kobalte/core/dialog';
import { splitProps } from 'solid-js';
export const Dialog = DialogPrimitive;
export const DialogTrigger = DialogPrimitive.Trigger;
type dialogContentProps<T extends ValidComponent = 'div'> = ParentProps<
DialogContentProps<T> & {
class?: string;
}
>;
export function DialogContent<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dialogContentProps<T>>) {
const [local, rest] = splitProps(props as dialogContentProps, [
'class',
'children',
]);
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
class={cn(
'fixed inset-0 z-50 bg-background/80 data-[expanded]:(animate-in fade-in-0) data-[closed]:(animate-out fade-out-0)',
)}
{...rest}
/>
<DialogPrimitive.Content
class={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[expanded]:(animate-in fade-in-0 zoom-in-95 slide-in-from-left-1/2 slide-in-from-top-48% duration-200) data-[closed]:(animate-out fade-out-0 zoom-out-95 slide-out-to-left-1/2 slide-out-to-top-48% duration-200) md:w-full sm:rounded-lg',
local.class,
)}
{...rest}
>
{local.children}
<DialogPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:(outline-none ring-1.5 ring-ring ring-offset-2) disabled:pointer-events-none bg-inherit transition-property-[opacity,box-shadow]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-4 w-4"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M18 6L6 18M6 6l12 12"
/>
<title>Close</title>
</svg>
</DialogPrimitive.CloseButton>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
);
}
type dialogTitleProps<T extends ValidComponent = 'h2'> = DialogTitleProps<T> & {
class?: string;
};
export function DialogTitle<T extends ValidComponent = 'h2'>(props: PolymorphicProps<T, dialogTitleProps<T>>) {
const [local, rest] = splitProps(props as dialogTitleProps, ['class']);
return (
<DialogPrimitive.Title
class={cn('text-lg font-semibold text-foreground', local.class)}
{...rest}
/>
);
}
type dialogDescriptionProps<T extends ValidComponent = 'p'> =
DialogDescriptionProps<T> & {
class?: string;
};
export function DialogDescription<T extends ValidComponent = 'p'>(props: PolymorphicProps<T, dialogDescriptionProps<T>>) {
const [local, rest] = splitProps(props as dialogDescriptionProps, ['class']);
return (
<DialogPrimitive.Description
class={cn('text-sm text-muted-foreground', local.class)}
{...rest}
/>
);
}
export function DialogHeader(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ['class']);
return (
<div
class={cn(
'flex flex-col space-y-2 text-center sm:text-left',
local.class,
)}
{...rest}
/>
);
}
export function DialogFooter(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ['class']);
return (
<div
class={cn(
'flex flex-col-reverse sm:(flex-row justify-end space-x-2)',
local.class,
)}
{...rest}
/>
);
}

View File

@@ -1,14 +0,0 @@
<script setup lang="ts">
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'radix-vue';
const props = defineProps<DialogRootProps>();
const emits = defineEmits<DialogRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<DialogRoot v-bind="forwarded">
<slot />
</DialogRoot>
</template>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
import { DialogClose, type DialogCloseProps } from 'radix-vue';
const props = defineProps<DialogCloseProps>();
</script>
<template>
<DialogClose v-bind="props">
<slot />
</DialogClose>
</template>

Some files were not shown because too many files have changed in this diff Show More