chore(cd): added deploy on cloudflare pages

This commit is contained in:
Corentin Thomasset 2024-10-02 22:30:45 +02:00
parent f8b5cbfd87
commit 161b9e6bca
No known key found for this signature in database
GPG key ID: DBD997E935996158
48 changed files with 4066 additions and 813 deletions

43
.github/workflows/cd-app-prod.yaml vendored Normal file
View file

@ -0,0 +1,43 @@
name: CD - Production
on:
push:
branches:
- next
jobs:
publish-app-prod:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
name: Publish app to production
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 22
corepack: true
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Build the app
run: pnpm -F @it-tools/app build
- name: Publish to Cloudflare Pages
uses: AdrianGonz97/refined-cf-pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
githubToken: ${{ secrets.GITHUB_TOKEN }}
projectName: it-tools
workingDirectory: packages/app
directory: dist
deploymentName: Production App
branch: next
wranglerVersion: '3'

1
README.md Normal file
View file

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

File diff suppressed because it is too large Load diff

View file

@ -19,10 +19,11 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test": "pnpm run test:unit", "test": "pnpm run test:unit",
"test:unit": "vitest run", "test:unit": "vitest run",
"test:unit:watch": "vitest watch" "test:unit:watch": "vitest watch",
"create:tool": "HYGEN_TMPLS=templates hygen tools new"
}, },
"dependencies": { "dependencies": {
"@corentinth/chisels": "^1.0.4", "@corentinth/chisels": "^1.1.0",
"@kobalte/core": "^0.13.6", "@kobalte/core": "^0.13.6",
"@solid-primitives/i18n": "^2.1.1", "@solid-primitives/i18n": "^2.1.1",
"@solid-primitives/storage": "^4.2.1", "@solid-primitives/storage": "^4.2.1",
@ -30,20 +31,24 @@
"@unocss/reset": "^0.62.4", "@unocss/reset": "^0.62.4",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk-solid": "^1.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"solid-js": "^1.9.1", "solid-js": "^1.9.1",
"solid-sonner": "^0.2.8",
"tailwind-merge": "^2.5.2" "tailwind-merge": "^2.5.2"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^3.7.3", "@antfu/eslint-config": "^3.7.3",
"@iconify-json/tabler": "^1.2.3", "@iconify-json/tabler": "^1.2.3",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@vitest/coverage-v8": "2.1.2",
"eslint": "^9.11.1", "eslint": "^9.11.1",
"hygen": "^6.2.11",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"unocss": "^0.62.4", "unocss": "^0.62.4",
"unocss-preset-animations": "^1.1.0", "unocss-preset-animations": "^1.1.0",
"vite": "^5.4.8", "vite": "^5.4.8",
"vite-plugin-solid": "^2.10.2", "vite-plugin-solid": "^2.10.2",
"vitest": "^2.1.1" "vitest": "^2.1.2"
} }
} }

View file

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

View file

@ -1,8 +1,7 @@
import type { LocaleKey } from './modules/i18n/i18n.types'; import type { LocaleKey } from './modules/i18n/i18n.types';
import { A, Navigate, type RouteDefinition, useParams } from '@solidjs/router'; import { A, Navigate, type RouteDefinition, useParams } from '@solidjs/router';
import { map } from 'lodash-es'; import { localeKeys } from './modules/i18n/i18n.constants';
import { localeKeys, locales } from './modules/i18n/i18n.constants'; import { useI18n } from './modules/i18n/i18n.provider';
import { getBrowserLocale, useI18n } from './modules/i18n/i18n.provider';
import { HomePage } from './modules/pages/home.page'; import { HomePage } from './modules/pages/home.page';
import { ToolPage } from './modules/tools/pages/tool.page'; import { ToolPage } from './modules/tools/pages/tool.page';
import { toolSlugs } from './modules/tools/tools.registry'; import { toolSlugs } from './modules/tools/tools.registry';

View file

@ -4,7 +4,9 @@ import { ColorModeProvider, ColorModeScript, createLocalStorageManager } from '@
import { Router } from '@solidjs/router'; import { Router } from '@solidjs/router';
import { render, Suspense } from 'solid-js/web'; import { render, Suspense } from 'solid-js/web';
import { routes } from './client-routes'; import { routes } from './client-routes';
import { CommandPaletteProvider } from './modules/command-palette/command-palette.provider';
import { RootI18nProvider } from './modules/i18n/i18n.provider'; import { RootI18nProvider } from './modules/i18n/i18n.provider';
import { Toaster } from './modules/ui/components/sonner';
import '@unocss/reset/tailwind.css'; import '@unocss/reset/tailwind.css';
import 'virtual:uno.css'; import 'virtual:uno.css';
import './app.css'; import './app.css';
@ -26,7 +28,10 @@ render(
initialColorMode={initialColorMode} initialColorMode={initialColorMode}
storageManager={localStorageManager} storageManager={localStorageManager}
> >
<div class="min-h-screen font-sans text-sm font-400">{props.children}</div> <CommandPaletteProvider>
<Toaster />
<div class="min-h-screen font-sans text-sm font-400">{props.children}</div>
</CommandPaletteProvider>
</ColorModeProvider> </ColorModeProvider>
</RootI18nProvider> </RootI18nProvider>
</Suspense> </Suspense>

View file

@ -16,8 +16,46 @@
"support": "Support IT-Tools", "support": "Support IT-Tools",
"report-bug": "Report a bug" "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": { "home": {
"all-tools": "All the tools", "all-tools": "All the tools",
"search-tools": "Search for a tool",
"open-source": "Open Source", "open-source": "Open Source",
"free": "Free", "free": "Free",
"self-hostable": "Self-hostable" "self-hostable": "Self-hostable"
@ -26,6 +64,10 @@
"token-generator": { "token-generator": {
"name": "Token Generator", "name": "Token Generator",
"description": "Generate random string with the characters you want, uppercase, lowercase letters, numbers and/or symbols." "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

@ -2,5 +2,58 @@
"app": { "app": {
"title": "IT-Tools", "title": "IT-Tools",
"description": "La collection open-source d'outils en ligne pour aider les devs dans leur vie quotidienne." "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

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

@ -5,11 +5,13 @@ export const locales = [
key: 'en', key: 'en',
file: 'en', file: 'en',
name: 'English', name: 'English',
switchToLabel: 'Change language to English',
}, },
{ {
key: 'fr', key: 'fr',
file: 'fr', file: 'fr',
name: 'Français', name: 'Français',
switchToLabel: 'Changer la langue en Français',
}, },
] as const; ] as const;

View file

@ -1,7 +1,9 @@
import type { ParentComponent } from 'solid-js'; import type { ParentComponent } from 'solid-js';
import type { LocaleKey } from './i18n.types'; import type { LocaleKey } from './i18n.types';
import { joinUrlPaths } from '@corentinth/chisels';
import * as i18n from '@solid-primitives/i18n'; import * as i18n from '@solid-primitives/i18n';
import { makePersisted } from '@solid-primitives/storage'; import { makePersisted } from '@solid-primitives/storage';
import { useNavigate } from '@solidjs/router';
import { merge } from 'lodash-es'; import { merge } from 'lodash-es';
import { createContext, createResource, createSignal, Show, useContext } from 'solid-js'; import { createContext, createResource, createSignal, Show, useContext } from 'solid-js';
import defaultDict from '../../locales/en.json'; import defaultDict from '../../locales/en.json';
@ -23,6 +25,7 @@ const RootI18nContext = createContext<{
function useI18n() { function useI18n() {
const context = useContext(RootI18nContext); const context = useContext(RootI18nContext);
const navigate = useNavigate();
if (!context) { if (!context) {
throw new Error('I18n context not found'); throw new Error('I18n context not found');
@ -35,6 +38,18 @@ function useI18n() {
getLocale, getLocale,
setLocale, setLocale,
locales, 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}`);
},
}; };
} }

View file

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

View file

@ -1,15 +1,17 @@
import type { Component } from 'solid-js'; import type { Component } from 'solid-js';
import { A } from '@solidjs/router'; import { A } from '@solidjs/router';
import { useCommandPalette } from '../command-palette/command-palette.provider';
import { useI18n } from '../i18n/i18n.provider'; import { useI18n } from '../i18n/i18n.provider';
import { useToolsStore } from '../tools/tools.store'; import { useToolsStore } from '../tools/tools.store';
import { Badge } from '../ui/components/badge'; import { Badge } from '../ui/components/badge';
import { Button } from '../ui/components/button'; import { Button } from '../ui/components/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/components/card'; import { Card, CardDescription, CardHeader, CardTitle } from '../ui/components/card';
import { cn } from '../ui/utils/cn'; import { cn } from '../ui/utils/cn';
export const HomePage: Component = () => { export const HomePage: Component = () => {
const { t } = useI18n(); const { t } = useI18n();
const { tools } = useToolsStore(); const { getTools } = useToolsStore();
const { openCommandPalette } = useCommandPalette();
return ( return (
<div> <div>
@ -39,11 +41,16 @@ export const HomePage: Component = () => {
{t('app.description')} {t('app.description')}
</p> </p>
<div> <div class="flex items-center gap-4">
<Button variant="default" as={A} href="tools"> <Button variant="default" as={A} href="tools">
{t('home.all-tools')} {t('home.all-tools')}
<div class="i-tabler-arrow-right ml-2 text-base"></div> <div class="i-tabler-arrow-right ml-2 text-base"></div>
</Button> </Button>
<Button variant="outline" onClick={openCommandPalette}>
<div class="i-tabler-search mr-2 text-base" />
{t('home.search-tools')}
</Button>
</div> </div>
</div> </div>
@ -57,9 +64,9 @@ export const HomePage: Component = () => {
</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"> <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 => ( {getTools().map(tool => (
<A href={tool.slug}> <A href={tool.slug} class="h-full">
<Card class="hover:(shadow-md transform scale-101) transition-transform"> <Card class="hover:(shadow-md transform scale-101) transition-transform h-full">
<CardHeader> <CardHeader>
<div class={cn(tool.icon, 'size-12 text-muted-foreground/60')} /> <div class={cn(tool.icon, 'size-12 text-muted-foreground/60')} />

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,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,31 @@
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 { 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 } = useCurrentTool({ defaultDictionary });
return (
<div class="mx-auto max-w-1200px p-6">
<div>
{getPort()}
</div>
<div class="flex gap-4 mt-4">
<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>
</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

@ -1,4 +1,6 @@
{ {
"name": "Token Generator",
"description": "Generate random string with the characters you want, uppercase, lowercase letters, numbers and/or symbols.",
"uppercase": "Uppercase letters (A-Z)", "uppercase": "Uppercase letters (A-Z)",
"lowercase": "Lowercase letters (a-z)", "lowercase": "Lowercase letters (a-z)",
"numbers": "Numbers (0-9)", "numbers": "Numbers (0-9)",

View file

@ -1,4 +1,6 @@
{ {
"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)", "uppercase": "Lettres majuscules (A-Z)",
"lowercase": "Lettres minuscules (a-z)", "lowercase": "Lettres minuscules (a-z)",
"numbers": "Chiffres (0-9)", "numbers": "Chiffres (0-9)",

View file

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

View file

@ -1,12 +1,7 @@
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 { useI18n } from '@/modules/i18n/i18n.provider';
import { safely } from '@corentinth/chisels'; import { safely } from '@corentinth/chisels';
import { flatten, translator } from '@solid-primitives/i18n';
import { useParams } from '@solidjs/router'; import { useParams } from '@solidjs/router';
import { merge } from 'lodash-es'; import { type Component, createResource, lazy, Show } from 'solid-js';
import { type Component, createContext, createResource, lazy, Show } from 'solid-js';
import { CurrentToolProvider } from '../tools.provider'; import { CurrentToolProvider } from '../tools.provider';
import { getToolDefinitionBySlug } from '../tools.registry'; import { getToolDefinitionBySlug } from '../tools.registry';
@ -18,9 +13,13 @@ export const ToolPage: Component = () => {
const ToolComponent = lazy(toolDefinition.entryFile); const ToolComponent = lazy(toolDefinition.entryFile);
const [toolDict] = createResource(getLocale, async (locale) => { const [toolDict] = createResource(getLocale, async (locale) => {
const [dict = { default: {} }] = await safely(import(`../definitions/${toolDefinition.dirName}/locales/${locale}.json`)); const [dict, error] = await safely(import(`../definitions/${toolDefinition.dirName}/locales/${locale}.json`));
return dict; if (error) {
console.error(error);
}
return dict ?? { default: {} };
}); });
return ( return (

View file

@ -5,13 +5,12 @@ export { defineTool };
function defineTool(toolDefinition: { function defineTool(toolDefinition: {
slug: string; slug: string;
entryFile: () => Promise<{ default: Component }>; entryFile: () => Promise<{ default: Component }>;
currentDirUrl: string; dirName: string;
icon: string; icon: string;
createdAt: Date; createdAt: Date;
}) { }) {
return { return {
...toolDefinition, ...toolDefinition,
key: toolDefinition.slug, key: toolDefinition.slug,
dirName: toolDefinition.currentDirUrl.split('/').slice(-2)[0],
}; };
} }

View file

@ -1,6 +1,5 @@
import type { Accessor, ParentComponent } from 'solid-js'; import type { Accessor, ParentComponent } from 'solid-js';
import type { ToolI18nFactory } from './tools.types'; import { flatten, translator } from '@solid-primitives/i18n';
import { flatten, type Flatten, translator, type Translator } from '@solid-primitives/i18n';
import { merge } from 'lodash-es'; import { merge } from 'lodash-es';
import { createContext, useContext } from 'solid-js'; import { createContext, useContext } from 'solid-js';

View file

@ -1,8 +1,10 @@
import { keyBy, map } from 'lodash-es'; 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'; import { tokenGeneratorTool } from './definitions/token-generator/token-generator.tool';
export const toolDefinitions = [ export const toolDefinitions = [
tokenGeneratorTool, tokenGeneratorTool,
randomPortGeneratorTool,
]; ];
export const toolSlugs = map(toolDefinitions, 'slug'); export const toolSlugs = map(toolDefinitions, 'slug');

View file

@ -1,3 +1,4 @@
import { createMemo } from 'solid-js';
import { useI18n } from '../i18n/i18n.provider'; import { useI18n } from '../i18n/i18n.provider';
import { toolDefinitions } from './tools.registry'; import { toolDefinitions } from './tools.registry';
@ -6,13 +7,13 @@ export { useToolsStore };
function useToolsStore() { function useToolsStore() {
const { t } = useI18n(); const { t } = useI18n();
const tools = toolDefinitions.map((tool) => { const getTools = createMemo(() => toolDefinitions.map((tool) => {
return { return {
...tool, ...tool,
name: t(`tools.${tool.slug}.name` as any) ?? tool.slug, name: t(`tools.${tool.slug}.name` as any) ?? tool.slug,
description: t(`tools.${tool.slug}.description` as any) ?? tool.slug, description: t(`tools.${tool.slug}.description` as any) ?? tool.slug,
}; };
}); }));
return { tools }; return { getTools };
} }

View file

@ -1,42 +1,37 @@
import { cn } from "@/modules/ui/utils/cn"; import type { VariantProps } from 'class-variance-authority';
import type { VariantProps } from "class-variance-authority"; import { cn } from '@/modules/ui/utils/cn';
import { cva } from "class-variance-authority"; import { cva } from 'class-variance-authority';
import { type ComponentProps, splitProps } from "solid-js"; import { type ComponentProps, splitProps } from 'solid-js';
export const badgeVariants = cva( 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)", '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: { variants: {
variant: { variant: {
default: default: 'bg-primary text-primary-foreground shadow hover:bg-primary/80',
"bg-primary text-primary-foreground shadow hover:bg-primary/80", secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
secondary: destructive: 'bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
"bg-secondary text-secondary-foreground hover:bg-secondary/80", outline: 'border text-foreground',
destructive: },
"bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", },
outline: "border text-foreground", defaultVariants: {
}, variant: 'default',
}, },
defaultVariants: { },
variant: "default",
},
},
); );
export const Badge = ( export function Badge(props: ComponentProps<'div'> & VariantProps<typeof badgeVariants>) {
props: ComponentProps<"div"> & VariantProps<typeof badgeVariants>, const [local, rest] = splitProps(props, ['class', 'variant']);
) => {
const [local, rest] = splitProps(props, ["class", "variant"]);
return ( return (
<div <div
class={cn( class={cn(
badgeVariants({ badgeVariants({
variant: local.variant, variant: local.variant,
}), }),
local.class, local.class,
)} )}
{...rest} {...rest}
/> />
); );
}; }

View file

@ -1,60 +1,60 @@
import { cn } from "@/modules/ui/utils/cn"; import type { ComponentProps, ParentComponent } from 'solid-js';
import type { ComponentProps, ParentComponent } from "solid-js"; import { cn } from '@/modules/ui/utils/cn';
import { splitProps } from "solid-js"; import { splitProps } from 'solid-js';
export const Card = (props: ComponentProps<"div">) => { export function Card(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ["class"]); const [local, rest] = splitProps(props, ['class']);
return ( return (
<div <div
class={cn( class={cn(
"rounded-xl border bg-card text-card-foreground shadow", 'rounded-xl border bg-card text-card-foreground shadow',
local.class, local.class,
)} )}
{...rest} {...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 CardHeader = (props: ComponentProps<"div">) => { export const CardDescription: ParentComponent<ComponentProps<'h3'>> = (
const [local, rest] = splitProps(props, ["class"]); props,
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"]); const [local, rest] = splitProps(props, ['class']);
return ( return (
<h3 class={cn("text-sm text-muted-foreground", local.class)} {...rest} /> <h3 class={cn('text-sm text-muted-foreground', local.class)} {...rest} />
); );
}; };
export const CardContent = (props: ComponentProps<"div">) => { export function CardContent(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ["class"]); const [local, rest] = splitProps(props, ['class']);
return <div class={cn("p-6 pt-0", local.class)} {...rest} />; return <div class={cn('p-6 pt-0', local.class)} {...rest} />;
}; }
export const CardFooter = (props: ComponentProps<"div">) => { export function CardFooter(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ["class"]); const [local, rest] = splitProps(props, ['class']);
return ( return (
<div class={cn("flex items-center p-6 pt-0", local.class)} {...rest} /> <div class={cn('flex items-center p-6 pt-0', local.class)} {...rest} />
); );
}; }

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

@ -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,314 +1,286 @@
import { cn } from "@/modules/ui/utils/cn";
import type { import type {
DropdownMenuCheckboxItemProps, DropdownMenuCheckboxItemProps,
DropdownMenuContentProps, DropdownMenuContentProps,
DropdownMenuGroupLabelProps, DropdownMenuGroupLabelProps,
DropdownMenuItemLabelProps, DropdownMenuItemLabelProps,
DropdownMenuItemProps, DropdownMenuItemProps,
DropdownMenuRadioItemProps, DropdownMenuRadioItemProps,
DropdownMenuRootProps, DropdownMenuRootProps,
DropdownMenuSeparatorProps, DropdownMenuSeparatorProps,
DropdownMenuSubTriggerProps, DropdownMenuSubTriggerProps,
} from "@kobalte/core/dropdown-menu"; } from '@kobalte/core/dropdown-menu';
import { DropdownMenu as DropdownMenuPrimitive } from "@kobalte/core/dropdown-menu"; import type { PolymorphicProps } from '@kobalte/core/polymorphic';
import type { PolymorphicProps } from "@kobalte/core/polymorphic"; import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js';
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js"; import { cn } from '@/modules/ui/utils/cn';
import { mergeProps, splitProps } from "solid-js"; import { DropdownMenu as DropdownMenuPrimitive } from '@kobalte/core/dropdown-menu';
import { mergeProps, splitProps } from 'solid-js';
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
export const DropdownMenuGroup = DropdownMenuPrimitive.Group; export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
export const DropdownMenuSub = DropdownMenuPrimitive.Sub; export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
export const DropdownMenu = (props: DropdownMenuRootProps) => { export function DropdownMenu(props: DropdownMenuRootProps) {
const merge = mergeProps<DropdownMenuRootProps[]>({ gutter: 4 }, props); const merge = mergeProps<DropdownMenuRootProps[]>({ gutter: 4 }, props);
return <DropdownMenuPrimitive {...merge} />; return <DropdownMenuPrimitive {...merge} />;
}; }
type dropdownMenuContentProps<T extends ValidComponent = "div"> = type dropdownMenuContentProps<T extends ValidComponent = 'div'> =
DropdownMenuContentProps<T> & { DropdownMenuContentProps<T> & {
class?: string; class?: string;
}; };
export const DropdownMenuContent = <T extends ValidComponent = "div">( export function DropdownMenuContent<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuContentProps<T>>) {
props: PolymorphicProps<T, dropdownMenuContentProps<T>>, const [local, rest] = splitProps(props as dropdownMenuContentProps, [
) => { 'class',
const [local, rest] = splitProps(props as dropdownMenuContentProps, [ ]);
"class",
]);
return ( return (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
class={cn( 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", '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, local.class,
)} )}
{...rest} {...rest}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
); );
}; }
type dropdownMenuItemProps<T extends ValidComponent = "div"> = type dropdownMenuItemProps<T extends ValidComponent = 'div'> =
DropdownMenuItemProps<T> & { DropdownMenuItemProps<T> & {
class?: string; class?: string;
inset?: boolean; inset?: boolean;
}; };
export const DropdownMenuItem = <T extends ValidComponent = "div">( export function DropdownMenuItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuItemProps<T>>) {
props: PolymorphicProps<T, dropdownMenuItemProps<T>>, const [local, rest] = splitProps(props as dropdownMenuItemProps, [
) => { 'class',
const [local, rest] = splitProps(props as dropdownMenuItemProps, [ 'inset',
"class", ]);
"inset",
]);
return ( return (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
class={cn( 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)", '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.inset && 'pl-8',
local.class, local.class,
)} )}
{...rest} {...rest}
/> />
); );
}; }
type dropdownMenuGroupLabelProps<T extends ValidComponent = "span"> = type dropdownMenuGroupLabelProps<T extends ValidComponent = 'span'> =
DropdownMenuGroupLabelProps<T> & { DropdownMenuGroupLabelProps<T> & {
class?: string; class?: string;
}; };
export const DropdownMenuGroupLabel = <T extends ValidComponent = "span">( export function DropdownMenuGroupLabel<T extends ValidComponent = 'span'>(props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>) {
props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>, const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [
) => { 'class',
const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [ ]);
"class",
]);
return ( return (
<DropdownMenuPrimitive.GroupLabel <DropdownMenuPrimitive.GroupLabel
as="div" as="div"
class={cn("px-2 py-1.5 text-sm font-semibold", local.class)} class={cn('px-2 py-1.5 text-sm font-semibold', local.class)}
{...rest} {...rest}
/> />
); );
}; }
type dropdownMenuItemLabelProps<T extends ValidComponent = "div"> = type dropdownMenuItemLabelProps<T extends ValidComponent = 'div'> =
DropdownMenuItemLabelProps<T> & { DropdownMenuItemLabelProps<T> & {
class?: string; class?: string;
}; };
export const DropdownMenuItemLabel = <T extends ValidComponent = "div">( export function DropdownMenuItemLabel<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>) {
props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>, const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [
) => { 'class',
const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [ ]);
"class",
]);
return ( return (
<DropdownMenuPrimitive.ItemLabel <DropdownMenuPrimitive.ItemLabel
as="div" as="div"
class={cn("px-2 py-1.5 text-sm font-semibold", local.class)} class={cn('px-2 py-1.5 text-sm font-semibold', local.class)}
{...rest} {...rest}
/> />
); );
}; }
type dropdownMenuSeparatorProps<T extends ValidComponent = "hr"> = type dropdownMenuSeparatorProps<T extends ValidComponent = 'hr'> =
DropdownMenuSeparatorProps<T> & { DropdownMenuSeparatorProps<T> & {
class?: string; class?: string;
}; };
export const DropdownMenuSeparator = <T extends ValidComponent = "hr">( export function DropdownMenuSeparator<T extends ValidComponent = 'hr'>(props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>) {
props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>, const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [
) => { 'class',
const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [ ]);
"class",
]);
return ( return (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
class={cn("-mx-1 my-1 h-px bg-muted", local.class)} class={cn('-mx-1 my-1 h-px bg-muted', local.class)}
{...rest} {...rest}
/> />
); );
}; }
export const DropdownMenuShortcut = (props: ComponentProps<"span">) => { export function DropdownMenuShortcut(props: ComponentProps<'span'>) {
const [local, rest] = splitProps(props, ["class"]); const [local, rest] = splitProps(props, ['class']);
return ( return (
<span <span
class={cn("ml-auto text-xs tracking-widest opacity-60", local.class)} class={cn('ml-auto text-xs tracking-widest opacity-60', local.class)}
{...rest} {...rest}
/> />
); );
}; }
type dropdownMenuSubTriggerProps<T extends ValidComponent = "div"> = type dropdownMenuSubTriggerProps<T extends ValidComponent = 'div'> = ParentProps<DropdownMenuSubTriggerProps<T> & { class?: string }>;
ParentProps<
DropdownMenuSubTriggerProps<T> & {
class?: string;
}
>;
export const DropdownMenuSubTrigger = <T extends ValidComponent = "div">( export function DropdownMenuSubTrigger<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>) {
props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>, const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [
) => { 'class',
const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [ 'children',
"class", ]);
"children",
]);
return ( return (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
class={cn( 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", '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, local.class,
)} )}
{...rest} {...rest}
> >
{local.children} {local.children}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="1em" width="1em"
height="1em" height="1em"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="ml-auto h-4 w-4" class="ml-auto h-4 w-4"
> >
<path <path
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="m9 6l6 6l-6 6" d="m9 6l6 6l-6 6"
/> />
<title>Arrow</title> <title>Arrow</title>
</svg> </svg>
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
); );
}; }
type dropdownMenuSubContentProps<T extends ValidComponent = "div"> = type dropdownMenuSubContentProps<T extends ValidComponent = 'div'> =
DropdownMenuSubTriggerProps<T> & { DropdownMenuSubTriggerProps<T> & {
class?: string; class?: string;
}; };
export const DropdownMenuSubContent = <T extends ValidComponent = "div">( export function DropdownMenuSubContent<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>) {
props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>, const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [
) => { 'class',
const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [ ]);
"class",
]);
return ( return (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
class={cn( 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)", '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, local.class,
)} )}
{...rest} {...rest}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
); );
}; }
type dropdownMenuCheckboxItemProps<T extends ValidComponent = "div"> = type dropdownMenuCheckboxItemProps<T extends ValidComponent = 'div'> = ParentProps<DropdownMenuCheckboxItemProps<T> & { class?: string }>;
ParentProps<
DropdownMenuCheckboxItemProps<T> & {
class?: string;
}
>;
export const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">( export function DropdownMenuCheckboxItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>) {
props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>, const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [
) => { 'class',
const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [ 'children',
"class", ]);
"children",
]);
return ( return (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
class={cn( 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)", '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, local.class,
)} )}
{...rest} {...rest}
> >
<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center"> <DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="h-4 w-4" class="h-4 w-4"
> >
<path <path
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="m5 12l5 5L20 7" d="m5 12l5 5L20 7"
/> />
<title>Checkbox</title> <title>Checkbox</title>
</svg> </svg>
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
{props.children} {props.children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
); );
}; }
type dropdownMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps< type dropdownMenuRadioItemProps<T extends ValidComponent = 'div'> = ParentProps<
DropdownMenuRadioItemProps<T> & { DropdownMenuRadioItemProps<T> & {
class?: string; class?: string;
} }
>; >;
export const DropdownMenuRadioItem = <T extends ValidComponent = "div">( export function DropdownMenuRadioItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>) {
props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>, const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [
) => { 'class',
const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [ 'children',
"class", ]);
"children",
]);
return ( return (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
class={cn( 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)", '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, local.class,
)} )}
{...rest} {...rest}
> >
<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center"> <DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="h-2 w-2" class="h-2 w-2"
> >
<g <g
fill="none" fill="none"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
> >
<path d="M0 0h24v24H0z" /> <path d="M0 0h24v24H0z" />
<path <path
fill="currentColor" fill="currentColor"
d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34" d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34"
/> />
</g> </g>
<title>Radio</title> <title>Radio</title>
</svg> </svg>
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
{props.children} {props.children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
); );
}; }

View file

@ -0,0 +1,20 @@
import { Toaster as Sonner, toast } from 'solid-sonner';
export { toast };
export function Toaster(props: Parameters<typeof Sonner>[0]) {
return (
<Sonner
class="toaster group"
toastOptions={{
classes: {
toast: 'group toast group-[.toaster]:(bg-background text-foreground border-border shadow-lg)',
description: 'group-[.toast]:text-muted-foreground',
actionButton: 'group-[.toast]:(bg-primary text-primary-foreground)',
cancelButton: 'group-[.toast]:(bg-muted text-muted-foreground)',
},
}}
{...props}
/>
);
}

View file

@ -1,62 +1,54 @@
import { cn } from "@/modules/ui/utils/cn"; import type { PolymorphicProps } from '@kobalte/core/polymorphic';
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { import type {
SwitchControlProps, SwitchControlProps,
SwitchThumbProps, SwitchThumbProps,
} from "@kobalte/core/switch"; } from '@kobalte/core/switch';
import { Switch as SwitchPrimitive } from "@kobalte/core/switch"; import type { ParentProps, ValidComponent, VoidProps } from 'solid-js';
import type { ParentProps, ValidComponent, VoidProps } from "solid-js"; import { cn } from '@/modules/ui/utils/cn';
import { splitProps } from "solid-js"; import { Switch as SwitchPrimitive } from '@kobalte/core/switch';
import { splitProps } from 'solid-js';
export const SwitchLabel = SwitchPrimitive.Label; export const SwitchLabel = SwitchPrimitive.Label;
export const Switch = SwitchPrimitive; export const Switch = SwitchPrimitive;
export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage; export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage;
export const SwitchDescription = SwitchPrimitive.Description; export const SwitchDescription = SwitchPrimitive.Description;
type switchControlProps<T extends ValidComponent = "input"> = ParentProps< type switchControlProps<T extends ValidComponent = 'input'> = ParentProps<SwitchControlProps<T> & { class?: string }>;
SwitchControlProps<T> & { class?: string }
>;
export const SwitchControl = <T extends ValidComponent = "input">( export function SwitchControl<T extends ValidComponent = 'input'>(props: PolymorphicProps<T, switchControlProps<T>>) {
props: PolymorphicProps<T, switchControlProps<T>>, const [local, rest] = splitProps(props as switchControlProps, [
) => { 'class',
const [local, rest] = splitProps(props as switchControlProps, [ 'children',
"class", ]);
"children",
]);
return ( return (
<> <>
<SwitchPrimitive.Input class="[&:focus-visible+div]:(outline-none ring-1.5 ring-ring ring-offset-2 ring-offset-background)" /> <SwitchPrimitive.Input class="[&:focus-visible+div]:(outline-none ring-1.5 ring-ring ring-offset-2 ring-offset-background)" />
<SwitchPrimitive.Control <SwitchPrimitive.Control
class={cn( 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]", '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, local.class,
)} )}
{...rest} {...rest}
> >
{local.children} {local.children}
</SwitchPrimitive.Control> </SwitchPrimitive.Control>
</> </>
); );
}; }
type switchThumbProps<T extends ValidComponent = "div"> = VoidProps< type switchThumbProps<T extends ValidComponent = 'div'> = VoidProps<SwitchThumbProps<T> & { class?: string }>;
SwitchThumbProps<T> & { class?: string }
>;
export const SwitchThumb = <T extends ValidComponent = "div">( export function SwitchThumb<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, switchThumbProps<T>>) {
props: PolymorphicProps<T, switchThumbProps<T>>, const [local, rest] = splitProps(props as switchThumbProps, ['class']);
) => {
const [local, rest] = splitProps(props as switchThumbProps, ["class"]);
return ( return (
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
class={cn( 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", '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, local.class,
)} )}
{...rest} {...rest}
/> />
); );
}; }

View file

@ -1,28 +1,26 @@
import { cn } from "@/modules/ui/utils/cn"; import type { PolymorphicProps } from '@kobalte/core/polymorphic';
import type { PolymorphicProps } from "@kobalte/core/polymorphic"; import type { TextFieldTextAreaProps } from '@kobalte/core/text-field';
import type { TextFieldTextAreaProps } from "@kobalte/core/text-field"; import type { ValidComponent, VoidProps } from 'solid-js';
import { TextArea as TextFieldPrimitive } from "@kobalte/core/text-field"; import { cn } from '@/modules/ui/utils/cn';
import type { ValidComponent, VoidProps } from "solid-js"; import { TextArea as TextFieldPrimitive } from '@kobalte/core/text-field';
import { splitProps } from "solid-js"; import { splitProps } from 'solid-js';
type textAreaProps<T extends ValidComponent = "textarea"> = VoidProps< type textAreaProps<T extends ValidComponent = 'textarea'> = VoidProps<
TextFieldTextAreaProps<T> & { TextFieldTextAreaProps<T> & {
class?: string; class?: string;
} }
>; >;
export const TextArea = <T extends ValidComponent = "textarea">( export function TextArea<T extends ValidComponent = 'textarea'>(props: PolymorphicProps<T, textAreaProps<T>>) {
props: PolymorphicProps<T, textAreaProps<T>>, const [local, rest] = splitProps(props as textAreaProps, ['class']);
) => {
const [local, rest] = splitProps(props as textAreaProps, ["class"]);
return ( return (
<TextFieldPrimitive <TextFieldPrimitive
class={cn( 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", '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, local.class,
)} )}
{...rest} {...rest}
/> />
); );
}; }

View file

@ -1,126 +1,116 @@
import { cn } from "@/modules/ui/utils/cn"; import type { PolymorphicProps } from '@kobalte/core/polymorphic';
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { import type {
TextFieldDescriptionProps, TextFieldDescriptionProps,
TextFieldErrorMessageProps, TextFieldErrorMessageProps,
TextFieldInputProps, TextFieldInputProps,
TextFieldLabelProps, TextFieldLabelProps,
TextFieldRootProps, TextFieldRootProps,
} from "@kobalte/core/text-field"; } from '@kobalte/core/text-field';
import { TextField as TextFieldPrimitive } from "@kobalte/core/text-field"; import type { ValidComponent, VoidProps } from 'solid-js';
import { cva } from "class-variance-authority"; import { cn } from '@/modules/ui/utils/cn';
import type { ValidComponent, VoidProps } from "solid-js"; import { TextField as TextFieldPrimitive } from '@kobalte/core/text-field';
import { splitProps } from "solid-js"; import { cva } from 'class-variance-authority';
import { splitProps } from 'solid-js';
type textFieldProps<T extends ValidComponent = "div"> = type textFieldProps<T extends ValidComponent = 'div'> =
TextFieldRootProps<T> & { TextFieldRootProps<T> & {
class?: string; class?: string;
}; };
export const TextFieldRoot = <T extends ValidComponent = "div">( export function TextFieldRoot<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldProps<T>>) {
props: PolymorphicProps<T, textFieldProps<T>>, const [local, rest] = splitProps(props as textFieldProps, ['class']);
) => {
const [local, rest] = splitProps(props as textFieldProps, ["class"]);
return <TextFieldPrimitive class={cn("space-y-1", local.class)} {...rest} />; return <TextFieldPrimitive class={cn('space-y-1', local.class)} {...rest} />;
}; }
export const textfieldLabel = cva( export const textfieldLabel = cva(
"text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium", 'text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium',
{ {
variants: { variants: {
label: { label: {
true: "data-[invalid]:text-destructive", true: 'data-[invalid]:text-destructive',
}, },
error: { error: {
true: "text-destructive text-xs", true: 'text-destructive text-xs',
}, },
description: { description: {
true: "font-normal text-muted-foreground", true: 'font-normal text-muted-foreground',
}, },
}, },
defaultVariants: { defaultVariants: {
label: true, label: true,
}, },
}, },
); );
type textFieldLabelProps<T extends ValidComponent = "label"> = type textFieldLabelProps<T extends ValidComponent = 'label'> =
TextFieldLabelProps<T> & { TextFieldLabelProps<T> & {
class?: string; class?: string;
}; };
export const TextFieldLabel = <T extends ValidComponent = "label">( export function TextFieldLabel<T extends ValidComponent = 'label'>(props: PolymorphicProps<T, textFieldLabelProps<T>>) {
props: PolymorphicProps<T, textFieldLabelProps<T>>, const [local, rest] = splitProps(props as textFieldLabelProps, ['class']);
) => {
const [local, rest] = splitProps(props as textFieldLabelProps, ["class"]);
return ( return (
<TextFieldPrimitive.Label <TextFieldPrimitive.Label
class={cn(textfieldLabel(), local.class)} class={cn(textfieldLabel(), local.class)}
{...rest} {...rest}
/> />
); );
}; }
type textFieldErrorMessageProps<T extends ValidComponent = "div"> = type textFieldErrorMessageProps<T extends ValidComponent = 'div'> =
TextFieldErrorMessageProps<T> & { TextFieldErrorMessageProps<T> & {
class?: string; class?: string;
}; };
export const TextFieldErrorMessage = <T extends ValidComponent = "div">( export function TextFieldErrorMessage<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldErrorMessageProps<T>>) {
props: PolymorphicProps<T, textFieldErrorMessageProps<T>>, const [local, rest] = splitProps(props as textFieldErrorMessageProps, [
) => { 'class',
const [local, rest] = splitProps(props as textFieldErrorMessageProps, [ ]);
"class",
]);
return ( return (
<TextFieldPrimitive.ErrorMessage <TextFieldPrimitive.ErrorMessage
class={cn(textfieldLabel({ error: true }), local.class)} class={cn(textfieldLabel({ error: true }), local.class)}
{...rest} {...rest}
/> />
); );
}; }
type textFieldDescriptionProps<T extends ValidComponent = "div"> = type textFieldDescriptionProps<T extends ValidComponent = 'div'> =
TextFieldDescriptionProps<T> & { TextFieldDescriptionProps<T> & {
class?: string; class?: string;
}; };
export const TextFieldDescription = <T extends ValidComponent = "div">( export function TextFieldDescription<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldDescriptionProps<T>>) {
props: PolymorphicProps<T, textFieldDescriptionProps<T>>, const [local, rest] = splitProps(props as textFieldDescriptionProps, [
) => { 'class',
const [local, rest] = splitProps(props as textFieldDescriptionProps, [ ]);
"class",
]);
return ( return (
<TextFieldPrimitive.Description <TextFieldPrimitive.Description
class={cn(textfieldLabel({ description: true }), local.class)} class={cn(textfieldLabel({ description: true }), local.class)}
{...rest} {...rest}
/> />
); );
}; }
type textFieldInputProps<T extends ValidComponent = "input"> = VoidProps< type textFieldInputProps<T extends ValidComponent = 'input'> = VoidProps<
TextFieldInputProps<T> & { TextFieldInputProps<T> & {
class?: string; class?: string;
} }
>; >;
export const TextField = <T extends ValidComponent = "input">( export function TextField<T extends ValidComponent = 'input'>(props: PolymorphicProps<T, textFieldInputProps<T>>) {
props: PolymorphicProps<T, textFieldInputProps<T>>, const [local, rest] = splitProps(props as textFieldInputProps, ['class']);
) => {
const [local, rest] = splitProps(props as textFieldInputProps, ["class"]);
return ( return (
<TextFieldPrimitive.Input <TextFieldPrimitive.Input
class={cn( 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", '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, local.class,
)} )}
{...rest} {...rest}
/> />
); );
}; }

View file

@ -1,11 +1,13 @@
import type { LocaleKey } from '@/modules/i18n/i18n.types';
import type { Component, ParentComponent } from 'solid-js'; import type { Component, ParentComponent } from 'solid-js';
import { useCommandPalette } from '@/modules/command-palette/command-palette.provider';
import { useI18n } from '@/modules/i18n/i18n.provider'; import { useI18n } from '@/modules/i18n/i18n.provider';
import { Button } from '@/modules/ui/components/button'; import { Button } from '@/modules/ui/components/button';
import { A, useLocation, useNavigate } from '@solidjs/router'; import { A } from '@solidjs/router';
import { Badge } from '../components/badge';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../components/dropdown-menu'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../components/dropdown-menu';
import { useThemeStore } from '../themes/theme.store'; import { useThemeStore } from '../themes/theme.store';
import { cn } from '../utils/cn'; import { cn } from '../utils/cn';
import { socialLinks } from './app.layouts.constants';
const ThemeSwitcher: Component = () => { const ThemeSwitcher: Component = () => {
const themeStore = useThemeStore(); const themeStore = useThemeStore();
@ -30,17 +32,7 @@ const ThemeSwitcher: Component = () => {
}; };
const LanguageSwitcher: Component = () => { const LanguageSwitcher: Component = () => {
const { t, getLocale, setLocale, locales } = useI18n(); const { t, getLocale, changeLocale, 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 ( return (
<> <>
@ -62,14 +54,24 @@ const LanguageSwitcher: Component = () => {
export const Navbar: Component = () => { export const Navbar: Component = () => {
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const { t } = useI18n(); const { t } = useI18n();
const { openCommandPalette } = useCommandPalette();
const getIsMacOs = () => navigator?.userAgent?.match(/Macintosh;/);
return ( return (
<div class="border-b border-border bg-surface"> <div class="border-b border-border bg-surface">
<div class="flex items-center justify-between px-6 py-3 mx-auto max-w-1200px"> <div class="flex items-center justify-between px-6 py-3 mx-auto max-w-1200px">
<div class="flex items-baseline gap-4"> <div class="flex items-center gap-4">
<Button variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250" as={A} href="/" aria-label="Home"> <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="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> <span class="text-80% font-extrabold border border-2px leading-none border-current rounded-md px-1 py-0.5 ml-1 text-primary">TOOLS</span>
</Button>
<Button size="sm" variant="outline" class="bg-card transition flex items-center gap-2 text-muted-foreground" onClick={openCommandPalette}>
<div class="i-tabler-search text-base"></div>
{t('commandPalette.trigger.search')}
<Badge variant="secondary" class="text-muted-foreground text-10px!">
{getIsMacOs() ? '⌘ + K' : 'Ctrl + K'}
</Badge>
</Button> </Button>
</div> </div>
@ -155,6 +157,114 @@ export const Navbar: Component = () => {
); );
}; };
const Footer: Component = () => {
const { t, createLocalizedUrl } = useI18n();
const getFooterSections = () => [
{
title: t('footer.resources.title'),
items: [
{ label: t('footer.resources.all-tools'), to: createLocalizedUrl({ path: '/tools' }) },
{ label: t('footer.resources.github'), href: 'https://github.com/CorentinTh/it-tools' },
{ label: t('footer.resources.support'), href: 'https://buymeacoffee.com/cthmsst' },
{ label: 'Humans.txt', href: '/humans.txt' },
{ label: t('footer.resources.license'), href: 'https://github.com/CorentinTh/it-tools/blob/main/LICENSE' },
],
},
{
title: t('footer.support.title'),
items: [
{ label: t('footer.support.report-bug'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
{ label: t('footer.support.request-feature'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
{ label: t('footer.support.contribute'), href: 'https://github.com/CorentinTh/it-tools/blob/main/CONTRIBUTING.md' },
{ label: t('footer.support.contact'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
],
},
{
title: t('footer.friends.title'),
items: [
{ label: 'Jugly.io', href: 'https://jugly.io' },
{ label: 'Enclosed.cc', href: 'https://enclosed.cc' },
],
},
];
return (
<footer class="bg-card border-t border-border">
<div class="py-12 px-6 mx-auto max-w-1200px">
<div class="flex items-start justify-between flex-col md:flex-row gap-12">
<div>
<div class="flex items-center gap-2">
<A href="/" class="text-2xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250 group text-muted-foreground flex items-center gap-1">
<span class="font-bold group-hover:text-foreground transition">IT</span>
<span class="text-80% font-extrabold border border-2px leading-none border-current rounded-md px-1 py-0.5 ml-1 group-hover:text-primary transition">TOOLS</span>
</A>
</div>
<div class="flex items-center gap-2 mt-4">
{socialLinks.map(({ icon, href, label }) => (
<a href={href} target="_blank" rel="noopener noreferrer" class="text-2xl text-muted-foreground hover:text-primary transition" aria-label={label}>
<div class={icon}></div>
</a>
))}
</div>
<div class="text-muted-foreground mt-2">
Crafted with
{' '}
<span class="i-tabler-heart inline-block text-base mb--0.5"></span>
{' '}
by
{' '}
<a href="https://corentin.tech" target="_blank" rel="noopener" class="hover:text-primary transition">
Corentin Thomasset
</a>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-12">
{getFooterSections().map(({ title, items }) => (
<div>
<h4 class="font-semibold text-foreground">{title}</h4>
<ul class="mt-4">
{items.map(({ label, to, href }) => (
<li class="mt-1">
{to
? (
<A href={to} class="text-muted-foreground hover:text-primary transition">
{label}
</A>
)
: (
<a href={href} target="_blank" rel="noopener" class="text-muted-foreground hover:text-primary transition">
{label}
</a>
)}
</li>
))}
</ul>
</div>
))}
</div>
</div>
<div class="text-xs text-muted-foreground border-t border-border pt-4 mt-12">
<span>
&copy;
{new Date().getFullYear()}
{' '}
Corentin Thomasset
</span>
</div>
<div class="text-xs text-foreground opacity-80%">
</div>
</div>
</footer>
);
};
export const AppLayout: ParentComponent = (props) => { export const AppLayout: ParentComponent = (props) => {
return ( return (
<div class="flex flex-col h-screen min-h-0"> <div class="flex flex-col h-screen min-h-0">
@ -163,6 +273,7 @@ export const AppLayout: ParentComponent = (props) => {
<div class="flex-1 pb-20 ">{props.children}</div> <div class="flex-1 pb-20 ">{props.children}</div>
<Footer />
</div> </div>
); );
}; };

View file

@ -0,0 +1,17 @@
export const socialLinks = [
{
icon: 'i-tabler-brand-github',
href: 'https://github.com/CorentinTh/it-tools',
label: 'GitHub',
},
{
icon: 'i-tabler-brand-x',
href: 'https://x.com/ittoolsdottech',
label: 'X',
},
{
icon: 'i-tabler-coffee',
href: 'https://buymeacoffee.com/cthmsst',
label: 'Support the project',
},
];

View file

@ -0,0 +1,12 @@
---
to: src/modules/tools/definitions/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.tool.ts
---
import { defineTool } from '../../tools.models'
export const <%= h.changeCase.camel(name) %>Tool = defineTool({
slug: '<%= h.changeCase.param(name) %>',
entryFile: () => import('./<%= h.changeCase.param(name) %>.page'),
icon: 'i-tabler-question-mark',
createdAt: new Date('<%= new Date().toISOString().split('T')[0] %>'),
dirName: '<%= h.changeCase.param(name) %>',
})

View file

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

View file

@ -0,0 +1,6 @@
---
inject: true
to: src/modules/tools/tools.registry.ts
at_line: 0
---
import { <%= h.changeCase.camel(name) %>Tool } from './definitions/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.tool';

View file

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

View file

@ -0,0 +1,14 @@
---
to: src/modules/tools/definitions/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.page.tsx
---
import type { Component } from 'solid-js';
const <%= h.changeCase.pascal(name) %>: Component = () => {
return (
<div class="mx-auto max-w-1200px p-6">
<h1><%= h.changeCase.title(name) %></h1>
</div>
);
}
export default <%= h.changeCase.pascal(name) %>;

View file

@ -8,6 +8,7 @@ import {
} from 'unocss'; } from 'unocss';
import presetAnimations from 'unocss-preset-animations'; import presetAnimations from 'unocss-preset-animations';
import { toolDefinitions } from './src/modules/tools/tools.registry'; import { toolDefinitions } from './src/modules/tools/tools.registry';
import { socialLinks } from './src/modules/ui/layouts/app.layouts.constants';
export default defineConfig({ export default defineConfig({
presets: [ presets: [
@ -111,5 +112,9 @@ export default defineConfig({
}, },
safelist: [ safelist: [
...toolDefinitions.map(tool => tool.icon), ...toolDefinitions.map(tool => tool.icon),
...socialLinks.map(({ icon }) => icon),
], ],
shortcuts: {
'i-logo': 'i-tabler-terminal',
},
}); });

1596
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff