mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-25 01:06:15 -04:00
feat: it-tools v3 base
This commit is contained in:
parent
1c35ac3704
commit
f8b5cbfd87
530 changed files with 7529 additions and 33524 deletions
17
packages/app/components.json
Normal file
17
packages/app/components.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"$schema": "https://shadcn-solid.com/schema.json",
|
||||
"uno": {
|
||||
"config": "uno.config.ts",
|
||||
"css": {
|
||||
"path": "src/client/app.css",
|
||||
"variable": true
|
||||
},
|
||||
"color": "neutral",
|
||||
"prefix": ""
|
||||
},
|
||||
"alias": {
|
||||
"component": "@/modules/ui/components",
|
||||
"ui": "@/modules/ui/components",
|
||||
"cn": "@/modules/ui/utils/cn"
|
||||
}
|
||||
}
|
21
packages/app/eslint.config.js
Normal file
21
packages/app/eslint.config.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import antfu from '@antfu/eslint-config';
|
||||
|
||||
export default antfu({
|
||||
stylistic: {
|
||||
semi: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
// To allow export on top of files
|
||||
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||
'curly': ['error', 'all'],
|
||||
'vitest/consistent-test-it': ['error', { fn: 'test' }],
|
||||
'ts/consistent-type-definitions': ['error', 'type'],
|
||||
'style/brace-style': ['error', '1tbs', { allowSingleLine: false }],
|
||||
'unused-imports/no-unused-vars': ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
}],
|
||||
},
|
||||
});
|
21
packages/app/index.html
Normal file
21
packages/app/index.html
Normal 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>
|
1174
packages/app/package-lock.json
generated
Normal file
1174
packages/app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
49
packages/app/package.json
Normal file
49
packages/app/package.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "@it-tools/app",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"packageManager": "pnpm@9.11.0",
|
||||
"description": "Collection of handy online tools for developers, with great UX.",
|
||||
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CorentinTh/it-tools"
|
||||
},
|
||||
"keywords": [],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "pnpm run test:unit",
|
||||
"test:unit": "vitest run",
|
||||
"test:unit:watch": "vitest watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@corentinth/chisels": "^1.0.4",
|
||||
"@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",
|
||||
"lodash-es": "^4.17.21",
|
||||
"solid-js": "^1.9.1",
|
||||
"tailwind-merge": "^2.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^3.7.3",
|
||||
"@iconify-json/tabler": "^1.2.3",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"eslint": "^9.11.1",
|
||||
"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.1"
|
||||
}
|
||||
}
|
4
packages/app/public/humans.txt
Normal file
4
packages/app/public/humans.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
/* TEAM */
|
||||
Developer: Corentin Thomasset
|
||||
Site: https://corentin.tech
|
||||
Twitter: @cthmsst
|
75
packages/app/src/app.css
Normal file
75
packages/app/src/app.css
Normal file
|
@ -0,0 +1,75 @@
|
|||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--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;
|
||||
}
|
70
packages/app/src/client-routes.tsx
Normal file
70
packages/app/src/client-routes.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import type { LocaleKey } from './modules/i18n/i18n.types';
|
||||
import { A, Navigate, type RouteDefinition, useParams } from '@solidjs/router';
|
||||
import { map } from 'lodash-es';
|
||||
import { localeKeys, locales } from './modules/i18n/i18n.constants';
|
||||
import { getBrowserLocale, 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>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
];
|
38
packages/app/src/client.tsx
Normal file
38
packages/app/src/client.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
/* @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 { RootI18nProvider } from './modules/i18n/i18n.provider';
|
||||
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}
|
||||
>
|
||||
<div class="min-h-screen font-sans text-sm font-400">{props.children}</div>
|
||||
</ColorModeProvider>
|
||||
</RootI18nProvider>
|
||||
</Suspense>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
document.getElementById('root')!,
|
||||
);
|
31
packages/app/src/locales/en.json
Normal file
31
packages/app/src/locales/en.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"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"
|
||||
},
|
||||
"home": {
|
||||
"all-tools": "All the tools",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
6
packages/app/src/locales/fr.json
Normal file
6
packages/app/src/locales/fr.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"app": {
|
||||
"title": "IT-Tools",
|
||||
"description": "La collection open-source d'outils en ligne pour aider les devs dans leur vie quotidienne."
|
||||
}
|
||||
}
|
16
packages/app/src/modules/i18n/i18n.constants.ts
Normal file
16
packages/app/src/modules/i18n/i18n.constants.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { map } from 'lodash-es';
|
||||
|
||||
export const locales = [
|
||||
{
|
||||
key: 'en',
|
||||
file: 'en',
|
||||
name: 'English',
|
||||
},
|
||||
{
|
||||
key: 'fr',
|
||||
file: 'fr',
|
||||
name: 'Français',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const localeKeys = map(locales, 'key');
|
81
packages/app/src/modules/i18n/i18n.provider.tsx
Normal file
81
packages/app/src/modules/i18n/i18n.provider.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import type { ParentComponent } from 'solid-js';
|
||||
import type { LocaleKey } from './i18n.types';
|
||||
import * as i18n from '@solid-primitives/i18n';
|
||||
import { makePersisted } from '@solid-primitives/storage';
|
||||
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);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('I18n context not found');
|
||||
}
|
||||
|
||||
const { t, getLocale, setLocale, locales } = context;
|
||||
|
||||
return {
|
||||
t,
|
||||
getLocale,
|
||||
setLocale,
|
||||
locales,
|
||||
};
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
5
packages/app/src/modules/i18n/i18n.types.ts
Normal file
5
packages/app/src/modules/i18n/i18n.types.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { locales } from './i18n.constants';
|
||||
|
||||
export type LocaleKey = typeof locales[number]['key'];
|
||||
|
||||
|
80
packages/app/src/modules/pages/home.page.tsx
Normal file
80
packages/app/src/modules/pages/home.page.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import type { Component } from 'solid-js';
|
||||
import { A } from '@solidjs/router';
|
||||
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, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/components/card';
|
||||
import { cn } from '../ui/utils/cn';
|
||||
|
||||
export const HomePage: Component = () => {
|
||||
const { t } = useI18n();
|
||||
const { tools } = useToolsStore();
|
||||
|
||||
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>
|
||||
<Button variant="default" as={A} href="tools">
|
||||
{t('home.all-tools')}
|
||||
<div class="i-tabler-arrow-right ml-2 text-base"></div>
|
||||
</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">
|
||||
{tools.map(tool => (
|
||||
<A href={tool.slug}>
|
||||
<Card class="hover:(shadow-md transform scale-101) transition-transform">
|
||||
<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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"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"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"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"
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { sample as sampleImpl, times } from 'lodash-es';
|
||||
|
||||
export function createToken({
|
||||
withUppercase = true,
|
||||
withLowercase = true,
|
||||
withNumbers = true,
|
||||
withSymbols = false,
|
||||
length = 64,
|
||||
alphabet,
|
||||
sample = sampleImpl,
|
||||
}: {
|
||||
withUppercase?: boolean;
|
||||
withLowercase?: boolean;
|
||||
withNumbers?: boolean;
|
||||
withSymbols?: boolean;
|
||||
length?: number;
|
||||
alphabet?: string;
|
||||
sample?: (str: string) => string;
|
||||
}) {
|
||||
const allAlphabet = alphabet ?? [
|
||||
withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : '',
|
||||
withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '',
|
||||
withNumbers ? '0123456789' : '',
|
||||
withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '',
|
||||
].join('');
|
||||
|
||||
return times(length, () => sample(allAlphabet)).join('');
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
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 { 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 } = useCurrentTool({ defaultDictionary });
|
||||
|
||||
const getToken = () => createToken({
|
||||
withUppercase: getUseUpperCase(),
|
||||
withLowercase: getUseLowerCase(),
|
||||
withNumbers: getUseNumbers(),
|
||||
withSymbols: getUseSpecialCharacters(),
|
||||
length: getLength(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="space-y-2 mx-auto max-w-1200px p-6">
|
||||
<Switch class="flex items-center space-x-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 space-x-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 space-x-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 space-x-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>
|
||||
|
||||
<TextFieldRoot>
|
||||
<TextArea placeholder="Your token will appear here" value={getToken()} readonly />
|
||||
</TextFieldRoot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TokenGeneratorTool;
|
|
@ -0,0 +1,9 @@
|
|||
import { defineTool } from '../../tools.models';
|
||||
|
||||
export const tokenGeneratorTool = defineTool({
|
||||
slug: 'token-generator',
|
||||
entryFile: () => import('./token-generator.page'),
|
||||
icon: 'i-tabler-key',
|
||||
createdAt: new Date('2024-02-13'),
|
||||
currentDirUrl: import.meta.url,
|
||||
});
|
35
packages/app/src/modules/tools/pages/tool.page.tsx
Normal file
35
packages/app/src/modules/tools/pages/tool.page.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import type { LocaleKey } from '@/modules/i18n/i18n.types';
|
||||
import type { Flatten } from '@solid-primitives/i18n';
|
||||
import type { ToolI18nFactory } from '../tools.types';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { safely } from '@corentinth/chisels';
|
||||
import { flatten, translator } from '@solid-primitives/i18n';
|
||||
import { useParams } from '@solidjs/router';
|
||||
import { merge } from 'lodash-es';
|
||||
import { type Component, createContext, 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 } = useI18n();
|
||||
|
||||
const toolDefinition = getToolDefinitionBySlug({ slug: params.toolSlug });
|
||||
const ToolComponent = lazy(toolDefinition.entryFile);
|
||||
|
||||
const [toolDict] = createResource(getLocale, async (locale) => {
|
||||
const [dict = { default: {} }] = await safely(import(`../definitions/${toolDefinition.dirName}/locales/${locale}.json`));
|
||||
|
||||
return dict;
|
||||
});
|
||||
|
||||
return (
|
||||
<Show when={toolDict()}>
|
||||
{toolLocaleDict => (
|
||||
<CurrentToolProvider toolLocaleDict={toolLocaleDict}>
|
||||
<ToolComponent />
|
||||
</CurrentToolProvider>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
};
|
17
packages/app/src/modules/tools/tools.models.ts
Normal file
17
packages/app/src/modules/tools/tools.models.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { Component } from 'solid-js';
|
||||
|
||||
export { defineTool };
|
||||
|
||||
function defineTool(toolDefinition: {
|
||||
slug: string;
|
||||
entryFile: () => Promise<{ default: Component }>;
|
||||
currentDirUrl: string;
|
||||
icon: string;
|
||||
createdAt: Date;
|
||||
}) {
|
||||
return {
|
||||
...toolDefinition,
|
||||
key: toolDefinition.slug,
|
||||
dirName: toolDefinition.currentDirUrl.split('/').slice(-2)[0],
|
||||
};
|
||||
}
|
31
packages/app/src/modules/tools/tools.provider.tsx
Normal file
31
packages/app/src/modules/tools/tools.provider.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import type { Accessor, ParentComponent } from 'solid-js';
|
||||
import type { ToolI18nFactory } from './tools.types';
|
||||
import { flatten, type Flatten, translator, type Translator } from '@solid-primitives/i18n';
|
||||
import { merge } from 'lodash-es';
|
||||
import { createContext, useContext } from 'solid-js';
|
||||
|
||||
type ToolProviderContext = {
|
||||
toolLocaleDict: Accessor<Record<string, 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');
|
||||
}
|
||||
|
||||
return {
|
||||
t: translator(() => flatten(merge({}, defaultDictionary, context.toolLocaleDict()))),
|
||||
};
|
||||
}
|
||||
|
||||
export const CurrentToolProvider: ParentComponent<ToolProviderContext> = (props) => {
|
||||
return (
|
||||
<CurrentToolContext.Provider value={props}>
|
||||
{props.children}
|
||||
</CurrentToolContext.Provider>
|
||||
);
|
||||
};
|
15
packages/app/src/modules/tools/tools.registry.ts
Normal file
15
packages/app/src/modules/tools/tools.registry.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { keyBy, map } from 'lodash-es';
|
||||
import { tokenGeneratorTool } from './definitions/token-generator/token-generator.tool';
|
||||
|
||||
export const toolDefinitions = [
|
||||
tokenGeneratorTool,
|
||||
];
|
||||
|
||||
export const toolSlugs = map(toolDefinitions, 'slug');
|
||||
export const toolDefinitionBySlug = keyBy(toolDefinitions, 'slug');
|
||||
|
||||
export { getToolDefinitionBySlug };
|
||||
|
||||
function getToolDefinitionBySlug({ slug }: { slug: string }) {
|
||||
return toolDefinitionBySlug[slug];
|
||||
}
|
18
packages/app/src/modules/tools/tools.store.ts
Normal file
18
packages/app/src/modules/tools/tools.store.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { useI18n } from '../i18n/i18n.provider';
|
||||
import { toolDefinitions } from './tools.registry';
|
||||
|
||||
export { useToolsStore };
|
||||
|
||||
function useToolsStore() {
|
||||
const { t } = useI18n();
|
||||
|
||||
const tools = 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 };
|
||||
}
|
3
packages/app/src/modules/tools/tools.types.ts
Normal file
3
packages/app/src/modules/tools/tools.types.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import type { Flatten, Translator } from '@solid-primitives/i18n';
|
||||
|
||||
export type ToolI18nFactory = <T extends Record<string, string>>(args: { defaultDictionary: T }) => { t: Translator<Flatten<T>> };
|
42
packages/app/src/modules/ui/components/badge.tsx
Normal file
42
packages/app/src/modules/ui/components/badge.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { cn } from "@/modules/ui/utils/cn";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
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 const 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}
|
||||
/>
|
||||
);
|
||||
};
|
60
packages/app/src/modules/ui/components/button.tsx
Normal file
60
packages/app/src/modules/ui/components/button.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
60
packages/app/src/modules/ui/components/card.tsx
Normal file
60
packages/app/src/modules/ui/components/card.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { cn } from "@/modules/ui/utils/cn";
|
||||
import type { ComponentProps, ParentComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const 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 const 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 const CardContent = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return <div class={cn("p-6 pt-0", local.class)} {...rest} />;
|
||||
};
|
||||
|
||||
export const CardFooter = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div class={cn("flex items-center p-6 pt-0", local.class)} {...rest} />
|
||||
);
|
||||
};
|
314
packages/app/src/modules/ui/components/dropdown-menu.tsx
Normal file
314
packages/app/src/modules/ui/components/dropdown-menu.tsx
Normal file
|
@ -0,0 +1,314 @@
|
|||
import { cn } from "@/modules/ui/utils/cn";
|
||||
import type {
|
||||
DropdownMenuCheckboxItemProps,
|
||||
DropdownMenuContentProps,
|
||||
DropdownMenuGroupLabelProps,
|
||||
DropdownMenuItemLabelProps,
|
||||
DropdownMenuItemProps,
|
||||
DropdownMenuRadioItemProps,
|
||||
DropdownMenuRootProps,
|
||||
DropdownMenuSeparatorProps,
|
||||
DropdownMenuSubTriggerProps,
|
||||
} from "@kobalte/core/dropdown-menu";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "@kobalte/core/dropdown-menu";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
|
||||
import { mergeProps, splitProps } from "solid-js";
|
||||
|
||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
export const DropdownMenu = (props: DropdownMenuRootProps) => {
|
||||
const merge = mergeProps<DropdownMenuRootProps[]>({ gutter: 4 }, props);
|
||||
|
||||
return <DropdownMenuPrimitive {...merge} />;
|
||||
};
|
||||
|
||||
type dropdownMenuContentProps<T extends ValidComponent = "div"> =
|
||||
DropdownMenuContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenuContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuContentProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
class={cn(
|
||||
"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95) focus-visible:(outline-none ring-1.5 ring-ring) transition-shadow",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuItemProps<T extends ValidComponent = "div"> =
|
||||
DropdownMenuItemProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
};
|
||||
|
||||
export const DropdownMenuItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuItemProps, [
|
||||
"class",
|
||||
"inset",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)",
|
||||
local.inset && "pl-8",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuGroupLabelProps<T extends ValidComponent = "span"> =
|
||||
DropdownMenuGroupLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenuGroupLabel = <T extends ValidComponent = "span">(
|
||||
props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.GroupLabel
|
||||
as="div"
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuItemLabelProps<T extends ValidComponent = "div"> =
|
||||
DropdownMenuItemLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenuItemLabel = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.ItemLabel
|
||||
as="div"
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuSeparatorProps<T extends ValidComponent = "hr"> =
|
||||
DropdownMenuSeparatorProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenuSeparator = <T extends ValidComponent = "hr">(
|
||||
props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
class={cn("-mx-1 my-1 h-px bg-muted", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DropdownMenuShortcut = (props: ComponentProps<"span">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<span
|
||||
class={cn("ml-auto text-xs tracking-widest opacity-60", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuSubTriggerProps<T extends ValidComponent = "div"> =
|
||||
ParentProps<
|
||||
DropdownMenuSubTriggerProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const DropdownMenuSubTrigger = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[expanded]:bg-accent",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
class="ml-auto h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m9 6l6 6l-6 6"
|
||||
/>
|
||||
<title>Arrow</title>
|
||||
</svg>
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuSubContentProps<T extends ValidComponent = "div"> =
|
||||
DropdownMenuSubTriggerProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenuSubContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
class={cn(
|
||||
"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95)",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuCheckboxItemProps<T extends ValidComponent = "div"> =
|
||||
ParentProps<
|
||||
DropdownMenuCheckboxItemProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m5 12l5 5L20 7"
|
||||
/>
|
||||
<title>Checkbox</title>
|
||||
</svg>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
{props.children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
DropdownMenuRadioItemProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const DropdownMenuRadioItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-2 w-2"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34"
|
||||
/>
|
||||
</g>
|
||||
<title>Radio</title>
|
||||
</svg>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
{props.children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
};
|
62
packages/app/src/modules/ui/components/switch.tsx
Normal file
62
packages/app/src/modules/ui/components/switch.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { cn } from "@/modules/ui/utils/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
SwitchControlProps,
|
||||
SwitchThumbProps,
|
||||
} from "@kobalte/core/switch";
|
||||
import { Switch as SwitchPrimitive } from "@kobalte/core/switch";
|
||||
import type { ParentProps, ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const SwitchLabel = SwitchPrimitive.Label;
|
||||
export const Switch = SwitchPrimitive;
|
||||
export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage;
|
||||
export const SwitchDescription = SwitchPrimitive.Description;
|
||||
|
||||
type switchControlProps<T extends ValidComponent = "input"> = ParentProps<
|
||||
SwitchControlProps<T> & { class?: string }
|
||||
>;
|
||||
|
||||
export const SwitchControl = <T extends ValidComponent = "input">(
|
||||
props: PolymorphicProps<T, switchControlProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as switchControlProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SwitchPrimitive.Input class="[&:focus-visible+div]:(outline-none ring-1.5 ring-ring ring-offset-2 ring-offset-background)" />
|
||||
<SwitchPrimitive.Control
|
||||
class={cn(
|
||||
"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input shadow-sm transition-shadow data-[disabled]:(cursor-not-allowed opacity-50) data-[checked]:bg-primary transition-property-[box-shadow,color,background-color]",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
</SwitchPrimitive.Control>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type switchThumbProps<T extends ValidComponent = "div"> = VoidProps<
|
||||
SwitchThumbProps<T> & { class?: string }
|
||||
>;
|
||||
|
||||
export const SwitchThumb = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, switchThumbProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as switchThumbProps, ["class"]);
|
||||
|
||||
return (
|
||||
<SwitchPrimitive.Thumb
|
||||
class={cn(
|
||||
"pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg transition-transform data-[checked]:translate-x-4",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
28
packages/app/src/modules/ui/components/textarea.tsx
Normal file
28
packages/app/src/modules/ui/components/textarea.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { cn } from "@/modules/ui/utils/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { TextFieldTextAreaProps } from "@kobalte/core/text-field";
|
||||
import { TextArea as TextFieldPrimitive } from "@kobalte/core/text-field";
|
||||
import type { ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
type textAreaProps<T extends ValidComponent = "textarea"> = VoidProps<
|
||||
TextFieldTextAreaProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const TextArea = <T extends ValidComponent = "textarea">(
|
||||
props: PolymorphicProps<T, textAreaProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textAreaProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TextFieldPrimitive
|
||||
class={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-inherit px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
126
packages/app/src/modules/ui/components/textfield.tsx
Normal file
126
packages/app/src/modules/ui/components/textfield.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
import { cn } from "@/modules/ui/utils/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
TextFieldDescriptionProps,
|
||||
TextFieldErrorMessageProps,
|
||||
TextFieldInputProps,
|
||||
TextFieldLabelProps,
|
||||
TextFieldRootProps,
|
||||
} from "@kobalte/core/text-field";
|
||||
import { TextField as TextFieldPrimitive } from "@kobalte/core/text-field";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
type textFieldProps<T extends ValidComponent = "div"> =
|
||||
TextFieldRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TextFieldRoot = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, textFieldProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textFieldProps, ["class"]);
|
||||
|
||||
return <TextFieldPrimitive class={cn("space-y-1", local.class)} {...rest} />;
|
||||
};
|
||||
|
||||
export const textfieldLabel = cva(
|
||||
"text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium",
|
||||
{
|
||||
variants: {
|
||||
label: {
|
||||
true: "data-[invalid]:text-destructive",
|
||||
},
|
||||
error: {
|
||||
true: "text-destructive text-xs",
|
||||
},
|
||||
description: {
|
||||
true: "font-normal text-muted-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
label: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type textFieldLabelProps<T extends ValidComponent = "label"> =
|
||||
TextFieldLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TextFieldLabel = <T extends ValidComponent = "label">(
|
||||
props: PolymorphicProps<T, textFieldLabelProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textFieldLabelProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TextFieldPrimitive.Label
|
||||
class={cn(textfieldLabel(), local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type textFieldErrorMessageProps<T extends ValidComponent = "div"> =
|
||||
TextFieldErrorMessageProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TextFieldErrorMessage = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, textFieldErrorMessageProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textFieldErrorMessageProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<TextFieldPrimitive.ErrorMessage
|
||||
class={cn(textfieldLabel({ error: true }), local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type textFieldDescriptionProps<T extends ValidComponent = "div"> =
|
||||
TextFieldDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TextFieldDescription = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, textFieldDescriptionProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textFieldDescriptionProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<TextFieldPrimitive.Description
|
||||
class={cn(textfieldLabel({ description: true }), local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type textFieldInputProps<T extends ValidComponent = "input"> = VoidProps<
|
||||
TextFieldInputProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const TextField = <T extends ValidComponent = "input">(
|
||||
props: PolymorphicProps<T, textFieldInputProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textFieldInputProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TextFieldPrimitive.Input
|
||||
class={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-inherit px-3 py-1 text-sm shadow-sm file:(border-0 bg-transparent text-sm font-medium) placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
168
packages/app/src/modules/ui/layouts/app.layout.tsx
Normal file
168
packages/app/src/modules/ui/layouts/app.layout.tsx
Normal file
|
@ -0,0 +1,168 @@
|
|||
import type { LocaleKey } from '@/modules/i18n/i18n.types';
|
||||
import type { Component, ParentComponent } from 'solid-js';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { A, useLocation, useNavigate } from '@solidjs/router';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../components/dropdown-menu';
|
||||
import { useThemeStore } from '../themes/theme.store';
|
||||
import { cn } from '../utils/cn';
|
||||
|
||||
const ThemeSwitcher: Component = () => {
|
||||
const themeStore = useThemeStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem onClick={() => themeStore.setColorMode({ mode: 'light' })} class="flex items-center gap-2 cursor-pointer">
|
||||
<div class="i-tabler-sun text-lg"></div>
|
||||
{t('navbar.theme.light-mode')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => themeStore.setColorMode({ mode: 'dark' })} class="flex items-center gap-2 cursor-pointer">
|
||||
<div class="i-tabler-moon text-lg"></div>
|
||||
{t('navbar.theme.dark-mode')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => themeStore.setColorMode({ mode: 'system' })} class="flex items-center gap-2 cursor-pointer">
|
||||
<div class="i-tabler-device-laptop text-lg"></div>
|
||||
{t('navbar.theme.system-mode')}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LanguageSwitcher: Component = () => {
|
||||
const { t, getLocale, setLocale, locales } = useI18n();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const changeLocale = (locale: LocaleKey) => {
|
||||
setLocale(locale);
|
||||
|
||||
const pathWithoutLocale = location.pathname.split('/').slice(2).join('/');
|
||||
const newPath = [locale, pathWithoutLocale].filter(Boolean).join('/');
|
||||
navigate(`/${newPath}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{locales.map(locale => (
|
||||
<DropdownMenuItem onClick={() => changeLocale(locale.key)} class={cn('flex items-center gap-2 cursor-pointer', { 'font-semibold': getLocale() === locale.key })}>
|
||||
{locale.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem as="a" class="flex items-center gap-2 cursor-pointer" target="_blank" rel="noopener noreferrer" href="https://github.com/CorentinTh/it-tools">
|
||||
{t('navbar.contribute-to-i18n')}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Navbar: Component = () => {
|
||||
const themeStore = useThemeStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div class="border-b border-border bg-surface">
|
||||
<div class="flex items-center justify-between px-6 py-3 mx-auto max-w-1200px">
|
||||
<div class="flex items-baseline gap-4">
|
||||
<Button variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250" as={A} href="/" aria-label="Home">
|
||||
<span class="font-bold text-foreground">IT</span>
|
||||
<span class="text-80% font-extrabold border border-2px leading-none border-current rounded-md px-1 py-0.5 ml-1 text-lime">TOOLS</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<Button variant="ghost" class="text-lg px-0 size-9 hidden md:inline-flex" as={A} href="https://github.com/CorentinTh/enclosed" target="_blank" rel="noopener noreferrer" aria-label="GitHub repository">
|
||||
<div class="i-tabler-brand-github"></div>
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as={Button} class="text-lg px-0 size-9 hidden md:inline-flex" variant="ghost" aria-label="Change theme">
|
||||
<div classList={{ 'i-tabler-moon': themeStore.getColorMode() === 'dark', 'i-tabler-sun': themeStore.getColorMode() === 'light' }}></div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-42">
|
||||
<ThemeSwitcher />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as={Button} class="text-lg px-0 size-9 hidden md:inline-flex" variant="ghost" aria-label="Language">
|
||||
<div class="i-custom-language size-4"></div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<LanguageSwitcher />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
|
||||
<DropdownMenuTrigger as={Button} class="text-lg px-0 size-9" variant="ghost" aria-label="Menu icon">
|
||||
<div class="i-tabler-dots-vertical hidden md:block"></div>
|
||||
<div class="i-tabler-menu-2 block md:hidden"></div>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent class="w-46">
|
||||
|
||||
{/* Mobile only items */}
|
||||
<DropdownMenuItem as="a" class="flex items-center gap-2 cursor-pointer md:hidden" target="_blank" href="https://github.com/CorentinTh/enclosed" rel="noopener noreferrer">
|
||||
<div class="i-tabler-brand-github text-lg"></div>
|
||||
{t('navbar.github')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger as="a" class="flex items-center gap-2 md:hidden" aria-label="Change theme">
|
||||
<div class="text-lg" classList={{ 'i-tabler-moon': themeStore.getColorMode() === 'dark', 'i-tabler-sun': themeStore.getColorMode() === 'light' }}></div>
|
||||
{t('navbar.theme.theme')}
|
||||
</DropdownMenuSubTrigger>
|
||||
|
||||
<DropdownMenuSubContent>
|
||||
<ThemeSwitcher />
|
||||
</DropdownMenuSubContent>
|
||||
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger as="a" class="flex items-center text-medium gap-2 md:hidden" aria-label="Change language">
|
||||
<div class="i-custom-language size-4"></div>
|
||||
{t('navbar.language')}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<LanguageSwitcher />
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
||||
{/* Default items */}
|
||||
|
||||
<DropdownMenuItem as="a" class="flex items-center gap-2 cursor-pointer" target="_blank" href="https://github.com/CorentinTh/it-tools/issues/new/choose" rel="noopener noreferrer">
|
||||
<div class="i-tabler-bug text-lg"></div>
|
||||
{t('navbar.report-bug')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem as="a" class="flex items-center gap-2 cursor-pointer" target="_blank" href="https://buymeacoffee.com/cthmsst" rel="noopener noreferrer">
|
||||
<div class="i-tabler-pig-money text-lg"></div>
|
||||
{t('navbar.support')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
</DropdownMenuContent>
|
||||
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppLayout: ParentComponent = (props) => {
|
||||
return (
|
||||
<div class="flex flex-col h-screen min-h-0">
|
||||
|
||||
<Navbar />
|
||||
|
||||
<div class="flex-1 pb-20 ">{props.children}</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
11
packages/app/src/modules/ui/themes/theme.store.ts
Normal file
11
packages/app/src/modules/ui/themes/theme.store.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import type { ConfigColorMode } from '@kobalte/core/color-mode';
|
||||
import { useColorMode } from '@kobalte/core/color-mode';
|
||||
|
||||
export function useThemeStore() {
|
||||
const { setColorMode, colorMode: getColorMode } = useColorMode();
|
||||
|
||||
return {
|
||||
setColorMode: ({ mode }: { mode: ConfigColorMode }) => setColorMode(mode),
|
||||
getColorMode,
|
||||
};
|
||||
}
|
5
packages/app/src/modules/ui/utils/cn.ts
Normal file
5
packages/app/src/modules/ui/utils/cn.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { ClassValue } from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));
|
22
packages/app/tsconfig.json
Normal file
22
packages/app/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"baseUrl": "./",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": ["vite/client"],
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
115
packages/app/uno.config.ts
Normal file
115
packages/app/uno.config.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
import {
|
||||
defineConfig,
|
||||
presetIcons,
|
||||
presetUno,
|
||||
presetWebFonts,
|
||||
transformerDirectives,
|
||||
transformerVariantGroup,
|
||||
} from 'unocss';
|
||||
import presetAnimations from 'unocss-preset-animations';
|
||||
import { toolDefinitions } from './src/modules/tools/tools.registry';
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetUno({
|
||||
dark: {
|
||||
dark: '[data-kb-theme="dark"]',
|
||||
light: '[data-kb-theme="light"]',
|
||||
},
|
||||
prefix: '',
|
||||
}),
|
||||
presetAnimations(),
|
||||
presetWebFonts({
|
||||
fonts: {
|
||||
sans: 'Inter:400,500,600,700,800,900',
|
||||
},
|
||||
}),
|
||||
presetIcons({
|
||||
collections: {
|
||||
custom: {
|
||||
language: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><path fill="currentColor" d="m478.33 433.6l-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4ZM334.83 362L368 281.65L401.17 362Zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73c39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36c-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93c.92 1.19 1.83 2.35 2.74 3.51c-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59c22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9Z" /></svg>',
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
transformers: [transformerVariantGroup(), transformerDirectives()],
|
||||
theme: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: 'hsl(var(--warning))',
|
||||
foreground: 'hsl(var(--warning-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
animation: {
|
||||
keyframes: {
|
||||
'accordion-down':
|
||||
'{ from { height: 0 } to { height: var(--kb-accordion-content-height) } }',
|
||||
'accordion-up':
|
||||
'{ from { height: var(--kb-accordion-content-height) } to { height: 0 } }',
|
||||
'collapsible-down':
|
||||
'{ from { height: 0 } to { height: var(--kb-collapsible-content-height) } }',
|
||||
'collapsible-up':
|
||||
'{ from { height: var(--kb-collapsible-content-height) } to { height: 0 } }',
|
||||
'caret-blink': '{ 0%,70%,100% { opacity: 1 } 20%,50% { opacity: 0 } }',
|
||||
},
|
||||
timingFns: {
|
||||
'accordion-down': 'ease-out',
|
||||
'accordion-up': 'ease-out',
|
||||
'collapsible-down': 'ease-out',
|
||||
'collapsible-up': 'ease-out',
|
||||
'caret-blink': 'ease-out',
|
||||
},
|
||||
durations: {
|
||||
'accordion-down': '0.2s',
|
||||
'accordion-up': '0.2s',
|
||||
'collapsible-down': '0.2s',
|
||||
'collapsible-up': '0.2s',
|
||||
'caret-blink': '1.25s',
|
||||
},
|
||||
counts: {
|
||||
'caret-blink': 'infinite',
|
||||
},
|
||||
},
|
||||
},
|
||||
safelist: [
|
||||
...toolDefinitions.map(tool => tool.icon),
|
||||
],
|
||||
});
|
33
packages/app/vite.config.ts
Normal file
33
packages/app/vite.config.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import path from 'node:path';
|
||||
import unoCssPlugin from 'unocss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import solidPlugin from 'vite-plugin-solid';
|
||||
import { configDefaults } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
unoCssPlugin(),
|
||||
solidPlugin(),
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8787',
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
'**/*.e2e.test.ts',
|
||||
],
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue