chore(cd): added deploy on cloudflare pages
This commit is contained in:
		
							parent
							
								
									f8b5cbfd87
								
							
						
					
					
						commit
						161b9e6bca
					
				
							
								
								
									
										43
									
								
								.github/workflows/cd-app-prod.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								.github/workflows/cd-app-prod.yaml
									
									
									
									
										vendored
									
									
										Normal 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' | ||||
| 
 | ||||
|    | ||||
							
								
								
									
										1215
									
								
								packages/app/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1215
									
								
								packages/app/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -19,10 +19,11 @@ | ||||
|     "typecheck": "tsc --noEmit", | ||||
|     "test": "pnpm run test:unit", | ||||
|     "test:unit": "vitest run", | ||||
|     "test:unit:watch": "vitest watch" | ||||
|     "test:unit:watch": "vitest watch", | ||||
|     "create:tool": "HYGEN_TMPLS=templates hygen tools new" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@corentinth/chisels": "^1.0.4", | ||||
|     "@corentinth/chisels": "^1.1.0", | ||||
|     "@kobalte/core": "^0.13.6", | ||||
|     "@solid-primitives/i18n": "^2.1.1", | ||||
|     "@solid-primitives/storage": "^4.2.1", | ||||
| @ -30,20 +31,24 @@ | ||||
|     "@unocss/reset": "^0.62.4", | ||||
|     "class-variance-authority": "^0.7.0", | ||||
|     "clsx": "^2.1.1", | ||||
|     "cmdk-solid": "^1.1.0", | ||||
|     "lodash-es": "^4.17.21", | ||||
|     "solid-js": "^1.9.1", | ||||
|     "solid-sonner": "^0.2.8", | ||||
|     "tailwind-merge": "^2.5.2" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@antfu/eslint-config": "^3.7.3", | ||||
|     "@iconify-json/tabler": "^1.2.3", | ||||
|     "@types/lodash-es": "^4.17.12", | ||||
|     "@vitest/coverage-v8": "2.1.2", | ||||
|     "eslint": "^9.11.1", | ||||
|     "hygen": "^6.2.11", | ||||
|     "typescript": "^5.6.2", | ||||
|     "unocss": "^0.62.4", | ||||
|     "unocss-preset-animations": "^1.1.0", | ||||
|     "vite": "^5.4.8", | ||||
|     "vite-plugin-solid": "^2.10.2", | ||||
|     "vitest": "^2.1.1" | ||||
|     "vitest": "^2.1.2" | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										2
									
								
								packages/app/public/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								packages/app/public/robots.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| User-agent: * | ||||
| Disallow: | ||||
| @ -1,8 +1,7 @@ | ||||
| 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 { localeKeys } from './modules/i18n/i18n.constants'; | ||||
| import { useI18n } from './modules/i18n/i18n.provider'; | ||||
| import { HomePage } from './modules/pages/home.page'; | ||||
| import { ToolPage } from './modules/tools/pages/tool.page'; | ||||
| import { toolSlugs } from './modules/tools/tools.registry'; | ||||
|  | ||||
| @ -4,7 +4,9 @@ import { ColorModeProvider, ColorModeScript, createLocalStorageManager } from '@ | ||||
| import { Router } from '@solidjs/router'; | ||||
| import { render, Suspense } from 'solid-js/web'; | ||||
| import { routes } from './client-routes'; | ||||
| import { CommandPaletteProvider } from './modules/command-palette/command-palette.provider'; | ||||
| import { RootI18nProvider } from './modules/i18n/i18n.provider'; | ||||
| import { Toaster } from './modules/ui/components/sonner'; | ||||
| import '@unocss/reset/tailwind.css'; | ||||
| import 'virtual:uno.css'; | ||||
| import './app.css'; | ||||
| @ -26,7 +28,10 @@ render( | ||||
|                 initialColorMode={initialColorMode} | ||||
|                 storageManager={localStorageManager} | ||||
|               > | ||||
|                 <CommandPaletteProvider> | ||||
|                   <Toaster /> | ||||
|                   <div class="min-h-screen font-sans text-sm font-400">{props.children}</div> | ||||
|                 </CommandPaletteProvider> | ||||
|               </ColorModeProvider> | ||||
|             </RootI18nProvider> | ||||
|           </Suspense> | ||||
|  | ||||
| @ -16,8 +16,46 @@ | ||||
|     "support": "Support IT-Tools", | ||||
|     "report-bug": "Report a bug" | ||||
|   }, | ||||
|   "footer": { | ||||
|     "resources": { | ||||
|       "title": "Resources", | ||||
|       "all-tools": "All the tools", | ||||
|       "github": "GitHub repository", | ||||
|       "support": "Support IT-Tools", | ||||
|       "license": "License" | ||||
|     }, | ||||
|     "support": { | ||||
|       "title": "Support", | ||||
|       "report-bug": "Report a bug", | ||||
|       "request-feature": "Request a feature", | ||||
|       "contribute": "Contribute to the project", | ||||
|       "contact": "Contact me" | ||||
|     }, | ||||
|     "friends": { | ||||
|       "title": "Friends" | ||||
|     } | ||||
|   }, | ||||
|   "commandPalette": { | ||||
|     "input-placeholder": "Type to search for a tool or a command...", | ||||
|     "go-home": "Go to home", | ||||
|     "sections": { | ||||
|       "tools": "Tools", | ||||
|       "navigation": "Navigation", | ||||
|       "language": "Language", | ||||
|       "theme": "Theme" | ||||
|     }, | ||||
|     "theme": { | ||||
|       "switch-to-light": "Switch to light theme", | ||||
|       "switch-to-dark": "Switch to dark theme", | ||||
|       "switch-to-system": "Use to system theme" | ||||
|     }, | ||||
|     "trigger": { | ||||
|       "search": "Search for a tool" | ||||
|     } | ||||
|   }, | ||||
|   "home": { | ||||
|     "all-tools": "All the tools", | ||||
|     "search-tools": "Search for a tool", | ||||
|     "open-source": "Open Source", | ||||
|     "free": "Free", | ||||
|     "self-hostable": "Self-hostable" | ||||
| @ -26,6 +64,10 @@ | ||||
|     "token-generator": { | ||||
|       "name": "Token Generator", | ||||
|       "description": "Generate random string with the characters you want, uppercase, lowercase letters, numbers and/or symbols." | ||||
|     }, | ||||
|     "random-port-generator": { | ||||
|       "name": "Random Port Generator", | ||||
|       "description": "Generate a random port number outside of the reserved ports range (0-1023)." | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -2,5 +2,58 @@ | ||||
|   "app": { | ||||
|     "title": "IT-Tools", | ||||
|     "description": "La collection open-source d'outils en ligne pour aider les devs dans leur vie quotidienne." | ||||
|   }, | ||||
|   "navbar": { | ||||
|     "theme": { | ||||
|       "theme": "Thème", | ||||
|       "light-mode": "Mode clair", | ||||
|       "dark-mode": "Mode sombre", | ||||
|       "system-mode": "Système" | ||||
|     }, | ||||
|     "language": "Langue", | ||||
|     "contribute-to-i18n": "Contribuer à l'i18n", | ||||
|     "github": "GitHub", | ||||
|     "support": "Soutenir IT-Tools", | ||||
|     "report-bug": "Signaler un bug" | ||||
|   }, | ||||
|   "footer": { | ||||
|     "resources": { | ||||
|       "title": "Ressources", | ||||
|       "all-tools": "Tous les outils", | ||||
|       "github": "Dépôt GitHub", | ||||
|       "support": "Soutenir IT-Tools", | ||||
|       "license": "Licence" | ||||
|     }, | ||||
|     "support": { | ||||
|       "title": "Support", | ||||
|       "report-bug": "Signaler un bug", | ||||
|       "request-feature": "Demander une fonctionnalité", | ||||
|       "contribute": "Contribuer au projet", | ||||
|       "contact": "Me contacter" | ||||
|     }, | ||||
|     "friends": { | ||||
|       "title": "Ami·e·s" | ||||
|     } | ||||
|   }, | ||||
|   "commandPalette": { | ||||
|     "input-placeholder": "Tapez pour rechercher un outil...", | ||||
|     "go-home": "Aller à l'accueil", | ||||
|     "sections": { | ||||
|       "tools": "Outils", | ||||
|       "navigation": "Navigation", | ||||
|       "theme": "Thème" | ||||
|     } | ||||
|   }, | ||||
|   "home": { | ||||
|     "all-tools": "Tous les outils", | ||||
|     "open-source": "Open Source", | ||||
|     "free": "Gratuit", | ||||
|     "self-hostable": "Self-hostable" | ||||
|   }, | ||||
|   "tools": { | ||||
|     "token-generator": { | ||||
|       "name": "Générateur de token", | ||||
|       "description": "Générer des string aléatoires, contrôlez les caractères que vous voulez, lettres majuscules, minuscules, chiffres et/ou symboles." | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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> | ||||
|   ); | ||||
| }; | ||||
| @ -5,11 +5,13 @@ export const locales = [ | ||||
|     key: 'en', | ||||
|     file: 'en', | ||||
|     name: 'English', | ||||
|     switchToLabel: 'Change language to English', | ||||
|   }, | ||||
|   { | ||||
|     key: 'fr', | ||||
|     file: 'fr', | ||||
|     name: 'Français', | ||||
|     switchToLabel: 'Changer la langue en Français', | ||||
|   }, | ||||
| ] as const; | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,9 @@ | ||||
| import type { ParentComponent } from 'solid-js'; | ||||
| import type { LocaleKey } from './i18n.types'; | ||||
| import { joinUrlPaths } from '@corentinth/chisels'; | ||||
| import * as i18n from '@solid-primitives/i18n'; | ||||
| import { makePersisted } from '@solid-primitives/storage'; | ||||
| import { useNavigate } from '@solidjs/router'; | ||||
| import { merge } from 'lodash-es'; | ||||
| import { createContext, createResource, createSignal, Show, useContext } from 'solid-js'; | ||||
| import defaultDict from '../../locales/en.json'; | ||||
| @ -23,6 +25,7 @@ const RootI18nContext = createContext<{ | ||||
| 
 | ||||
| function useI18n() { | ||||
|   const context = useContext(RootI18nContext); | ||||
|   const navigate = useNavigate(); | ||||
| 
 | ||||
|   if (!context) { | ||||
|     throw new Error('I18n context not found'); | ||||
| @ -35,6 +38,18 @@ function useI18n() { | ||||
|     getLocale, | ||||
|     setLocale, | ||||
|     locales, | ||||
|     createLocalizedUrl: ({ path }: { path: string }) => { | ||||
|       const newPath = joinUrlPaths(getLocale(), path); | ||||
| 
 | ||||
|       return `/${newPath}`; | ||||
|     }, | ||||
|     changeLocale: (locale: LocaleKey) => { | ||||
|       setLocale(locale); | ||||
| 
 | ||||
|       const pathWithoutLocale = location.pathname.split('/').slice(2).join('/'); | ||||
|       const newPath = joinUrlPaths(locale, pathWithoutLocale); | ||||
|       navigate(`/${newPath}`); | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| import type { locales } from './i18n.constants'; | ||||
| 
 | ||||
| export type LocaleKey = typeof locales[number]['key']; | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -1,15 +1,17 @@ | ||||
| import type { Component } from 'solid-js'; | ||||
| import { A } from '@solidjs/router'; | ||||
| import { useCommandPalette } from '../command-palette/command-palette.provider'; | ||||
| import { useI18n } from '../i18n/i18n.provider'; | ||||
| import { useToolsStore } from '../tools/tools.store'; | ||||
| import { Badge } from '../ui/components/badge'; | ||||
| import { Button } from '../ui/components/button'; | ||||
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/components/card'; | ||||
| import { Card, CardDescription, CardHeader, CardTitle } from '../ui/components/card'; | ||||
| import { cn } from '../ui/utils/cn'; | ||||
| 
 | ||||
| export const HomePage: Component = () => { | ||||
|   const { t } = useI18n(); | ||||
|   const { tools } = useToolsStore(); | ||||
|   const { getTools } = useToolsStore(); | ||||
|   const { openCommandPalette } = useCommandPalette(); | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
| @ -39,11 +41,16 @@ export const HomePage: Component = () => { | ||||
|               {t('app.description')} | ||||
|             </p> | ||||
| 
 | ||||
|             <div> | ||||
|             <div class="flex items-center gap-4"> | ||||
|               <Button variant="default" as={A} href="tools"> | ||||
|                 {t('home.all-tools')} | ||||
|                 <div class="i-tabler-arrow-right ml-2 text-base"></div> | ||||
|               </Button> | ||||
| 
 | ||||
|               <Button variant="outline" onClick={openCommandPalette}> | ||||
|                 <div class="i-tabler-search mr-2 text-base" /> | ||||
|                 {t('home.search-tools')} | ||||
|               </Button> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
| @ -57,9 +64,9 @@ export const HomePage: Component = () => { | ||||
|       </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"> | ||||
|         {getTools().map(tool => ( | ||||
|           <A href={tool.slug} class="h-full"> | ||||
|             <Card class="hover:(shadow-md transform scale-101) transition-transform h-full"> | ||||
|               <CardHeader> | ||||
|                 <div class={cn(tool.icon, 'size-12 text-muted-foreground/60')} /> | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										37
									
								
								packages/app/src/modules/shared/copy/copy-button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								packages/app/src/modules/shared/copy/copy-button.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										23
									
								
								packages/app/src/modules/shared/copy/copy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/app/src/modules/shared/copy/copy.ts
									
									
									
									
									
										Normal 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); | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										33
									
								
								packages/app/src/modules/shared/signals.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								packages/app/src/modules/shared/signals.test.ts
									
									
									
									
									
										Normal 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); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										13
									
								
								packages/app/src/modules/shared/signals.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/app/src/modules/shared/signals.ts
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
| @ -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" | ||||
| } | ||||
| @ -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; | ||||
| @ -0,0 +1,5 @@ | ||||
| import { random } from 'lodash-es'; | ||||
| 
 | ||||
| export function generateRandomPort() { | ||||
|   return random(1024, 65535); | ||||
| } | ||||
| @ -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', | ||||
| }); | ||||
| @ -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)", | ||||
|   "lowercase": "Lowercase letters (a-z)", | ||||
|   "numbers": "Numbers (0-9)", | ||||
|  | ||||
| @ -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)", | ||||
|   "lowercase": "Lettres minuscules (a-z)", | ||||
|   "numbers": "Chiffres (0-9)", | ||||
|  | ||||
| @ -5,5 +5,5 @@ export const tokenGeneratorTool = defineTool({ | ||||
|   entryFile: () => import('./token-generator.page'), | ||||
|   icon: 'i-tabler-key', | ||||
|   createdAt: new Date('2024-02-13'), | ||||
|   currentDirUrl: import.meta.url, | ||||
|   dirName: 'token-generator', | ||||
| }); | ||||
|  | ||||
| @ -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 { 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 { type Component, createResource, lazy, Show } from 'solid-js'; | ||||
| import { CurrentToolProvider } from '../tools.provider'; | ||||
| import { getToolDefinitionBySlug } from '../tools.registry'; | ||||
| 
 | ||||
| @ -18,9 +13,13 @@ export const ToolPage: Component = () => { | ||||
|   const ToolComponent = lazy(toolDefinition.entryFile); | ||||
| 
 | ||||
|   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 ( | ||||
|  | ||||
| @ -5,13 +5,12 @@ export { defineTool }; | ||||
| function defineTool(toolDefinition: { | ||||
|   slug: string; | ||||
|   entryFile: () => Promise<{ default: Component }>; | ||||
|   currentDirUrl: string; | ||||
|   dirName: string; | ||||
|   icon: string; | ||||
|   createdAt: Date; | ||||
| }) { | ||||
|   return { | ||||
|     ...toolDefinition, | ||||
|     key: toolDefinition.slug, | ||||
|     dirName: toolDefinition.currentDirUrl.split('/').slice(-2)[0], | ||||
|   }; | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| 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 { flatten, translator } from '@solid-primitives/i18n'; | ||||
| import { merge } from 'lodash-es'; | ||||
| import { createContext, useContext } from 'solid-js'; | ||||
| 
 | ||||
|  | ||||
| @ -1,8 +1,10 @@ | ||||
| import { keyBy, map } from 'lodash-es'; | ||||
| import { randomPortGeneratorTool } from './definitions/random-port-generator/random-port-generator.tool'; | ||||
| import { tokenGeneratorTool } from './definitions/token-generator/token-generator.tool'; | ||||
| 
 | ||||
| export const toolDefinitions = [ | ||||
|   tokenGeneratorTool, | ||||
|   randomPortGeneratorTool, | ||||
| ]; | ||||
| 
 | ||||
| export const toolSlugs = map(toolDefinitions, 'slug'); | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { createMemo } from 'solid-js'; | ||||
| import { useI18n } from '../i18n/i18n.provider'; | ||||
| import { toolDefinitions } from './tools.registry'; | ||||
| 
 | ||||
| @ -6,13 +7,13 @@ export { useToolsStore }; | ||||
| function useToolsStore() { | ||||
|   const { t } = useI18n(); | ||||
| 
 | ||||
|   const tools = toolDefinitions.map((tool) => { | ||||
|   const getTools = createMemo(() => toolDefinitions.map((tool) => { | ||||
|     return { | ||||
|       ...tool, | ||||
|       name: t(`tools.${tool.slug}.name` as any) ?? tool.slug, | ||||
|       description: t(`tools.${tool.slug}.description` as any) ?? tool.slug, | ||||
|     }; | ||||
|   }); | ||||
|   })); | ||||
| 
 | ||||
|   return { tools }; | ||||
|   return { getTools }; | ||||
| } | ||||
|  | ||||
| @ -1,32 +1,27 @@ | ||||
| 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"; | ||||
| import type { VariantProps } from 'class-variance-authority'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { cva } from 'class-variance-authority'; | ||||
| import { type ComponentProps, splitProps } from 'solid-js'; | ||||
| 
 | ||||
| export const badgeVariants = cva( | ||||
| 	"inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-shadow focus-visible:(outline-none ring-1.5 ring-ring)", | ||||
|   '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", | ||||
|         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", | ||||
|       variant: 'default', | ||||
|     }, | ||||
|   }, | ||||
| ); | ||||
| 
 | ||||
| export const Badge = ( | ||||
| 	props: ComponentProps<"div"> & VariantProps<typeof badgeVariants>, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props, ["class", "variant"]); | ||||
| export function Badge(props: ComponentProps<'div'> & VariantProps<typeof badgeVariants>) { | ||||
|   const [local, rest] = splitProps(props, ['class', 'variant']); | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
| @ -39,4 +34,4 @@ export const Badge = ( | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
|  | ||||
| @ -1,60 +1,60 @@ | ||||
| import { cn } from "@/modules/ui/utils/cn"; | ||||
| import type { ComponentProps, ParentComponent } from "solid-js"; | ||||
| import { splitProps } from "solid-js"; | ||||
| import type { ComponentProps, ParentComponent } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| 
 | ||||
| export const Card = (props: ComponentProps<"div">) => { | ||||
| 	const [local, rest] = splitProps(props, ["class"]); | ||||
| export function Card(props: ComponentProps<'div'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       class={cn( | ||||
| 				"rounded-xl border bg-card text-card-foreground shadow", | ||||
|         'rounded-xl border bg-card text-card-foreground shadow', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| export const CardHeader = (props: ComponentProps<"div">) => { | ||||
| 	const [local, rest] = splitProps(props, ["class"]); | ||||
| 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} /> | ||||
|     <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"]); | ||||
| 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)} | ||||
|       class={cn('font-semibold leading-none tracking-tight', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const CardDescription: ParentComponent<ComponentProps<"h3">> = ( | ||||
| export const CardDescription: ParentComponent<ComponentProps<'h3'>> = ( | ||||
|   props, | ||||
| ) => { | ||||
| 	const [local, rest] = splitProps(props, ["class"]); | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   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">) => { | ||||
| 	const [local, rest] = splitProps(props, ["class"]); | ||||
| export function CardContent(props: ComponentProps<'div'>) { | ||||
|   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">) => { | ||||
| 	const [local, rest] = splitProps(props, ["class"]); | ||||
| export function CardFooter(props: ComponentProps<'div'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
| 		<div class={cn("flex items-center p-6 pt-0", local.class)} {...rest} /> | ||||
|     <div class={cn('flex items-center p-6 pt-0', local.class)} {...rest} /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
|  | ||||
							
								
								
									
										151
									
								
								packages/app/src/modules/ui/components/command.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								packages/app/src/modules/ui/components/command.tsx
									
									
									
									
									
										Normal 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} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										122
									
								
								packages/app/src/modules/ui/components/dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								packages/app/src/modules/ui/components/dialog.tsx
									
									
									
									
									
										Normal 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} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -1,4 +1,3 @@ | ||||
| import { cn } from "@/modules/ui/utils/cn"; | ||||
| import type { | ||||
|   DropdownMenuCheckboxItemProps, | ||||
|   DropdownMenuContentProps, | ||||
| @ -9,166 +8,150 @@ import type { | ||||
|   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"; | ||||
| } from '@kobalte/core/dropdown-menu'; | ||||
| import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||
| import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { DropdownMenu as DropdownMenuPrimitive } from '@kobalte/core/dropdown-menu'; | ||||
| import { mergeProps, splitProps } from 'solid-js'; | ||||
| 
 | ||||
| export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; | ||||
| export const DropdownMenuGroup = DropdownMenuPrimitive.Group; | ||||
| export const DropdownMenuSub = DropdownMenuPrimitive.Sub; | ||||
| export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; | ||||
| 
 | ||||
| export const DropdownMenu = (props: DropdownMenuRootProps) => { | ||||
| export function DropdownMenu(props: DropdownMenuRootProps) { | ||||
|   const merge = mergeProps<DropdownMenuRootProps[]>({ gutter: 4 }, props); | ||||
| 
 | ||||
|   return <DropdownMenuPrimitive {...merge} />; | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuContentProps<T extends ValidComponent = "div"> = | ||||
| type dropdownMenuContentProps<T extends ValidComponent = 'div'> = | ||||
|   DropdownMenuContentProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export const DropdownMenuContent = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuContentProps<T>>, | ||||
| ) => { | ||||
| export function DropdownMenuContent<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuContentProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuContentProps, [ | ||||
| 		"class", | ||||
|     '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", | ||||
|           '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"> = | ||||
| type dropdownMenuItemProps<T extends ValidComponent = 'div'> = | ||||
|   DropdownMenuItemProps<T> & { | ||||
|     class?: string; | ||||
|     inset?: boolean; | ||||
|   }; | ||||
| 
 | ||||
| export const DropdownMenuItem = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuItemProps<T>>, | ||||
| ) => { | ||||
| export function DropdownMenuItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuItemProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuItemProps, [ | ||||
| 		"class", | ||||
| 		"inset", | ||||
|     '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", | ||||
|         '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"> = | ||||
| type dropdownMenuGroupLabelProps<T extends ValidComponent = 'span'> = | ||||
|   DropdownMenuGroupLabelProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export const DropdownMenuGroupLabel = <T extends ValidComponent = "span">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>, | ||||
| ) => { | ||||
| export function DropdownMenuGroupLabel<T extends ValidComponent = 'span'>(props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [ | ||||
| 		"class", | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.GroupLabel | ||||
|       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} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuItemLabelProps<T extends ValidComponent = "div"> = | ||||
| type dropdownMenuItemLabelProps<T extends ValidComponent = 'div'> = | ||||
|   DropdownMenuItemLabelProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export const DropdownMenuItemLabel = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>, | ||||
| ) => { | ||||
| export function DropdownMenuItemLabel<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [ | ||||
| 		"class", | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <DropdownMenuPrimitive.ItemLabel | ||||
|       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} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuSeparatorProps<T extends ValidComponent = "hr"> = | ||||
| type dropdownMenuSeparatorProps<T extends ValidComponent = 'hr'> = | ||||
|   DropdownMenuSeparatorProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export const DropdownMenuSeparator = <T extends ValidComponent = "hr">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>, | ||||
| ) => { | ||||
| export function DropdownMenuSeparator<T extends ValidComponent = 'hr'>(props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [ | ||||
| 		"class", | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
|     <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} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| export const DropdownMenuShortcut = (props: ComponentProps<"span">) => { | ||||
| 	const [local, rest] = splitProps(props, ["class"]); | ||||
| export function DropdownMenuShortcut(props: ComponentProps<'span'>) { | ||||
|   const [local, rest] = splitProps(props, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <span | ||||
| 			class={cn("ml-auto text-xs tracking-widest opacity-60", local.class)} | ||||
|       class={cn('ml-auto text-xs tracking-widest opacity-60', local.class)} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuSubTriggerProps<T extends ValidComponent = "div"> = | ||||
| 	ParentProps< | ||||
| 		DropdownMenuSubTriggerProps<T> & { | ||||
| 			class?: string; | ||||
| 		} | ||||
| 	>; | ||||
| type dropdownMenuSubTriggerProps<T extends ValidComponent = 'div'> = ParentProps<DropdownMenuSubTriggerProps<T> & { class?: string }>; | ||||
| 
 | ||||
| export const DropdownMenuSubTrigger = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>, | ||||
| ) => { | ||||
| export function DropdownMenuSubTrigger<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [ | ||||
| 		"class", | ||||
| 		"children", | ||||
|     '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", | ||||
|         '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} | ||||
| @ -193,52 +176,43 @@ export const DropdownMenuSubTrigger = <T extends ValidComponent = "div">( | ||||
|       </svg> | ||||
|     </DropdownMenuPrimitive.SubTrigger> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuSubContentProps<T extends ValidComponent = "div"> = | ||||
| type dropdownMenuSubContentProps<T extends ValidComponent = 'div'> = | ||||
|   DropdownMenuSubTriggerProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export const DropdownMenuSubContent = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>, | ||||
| ) => { | ||||
| export function DropdownMenuSubContent<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [ | ||||
| 		"class", | ||||
|     '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)", | ||||
|           '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; | ||||
| 		} | ||||
| 	>; | ||||
| type dropdownMenuCheckboxItemProps<T extends ValidComponent = 'div'> = ParentProps<DropdownMenuCheckboxItemProps<T> & { class?: string }>; | ||||
| 
 | ||||
| export const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>, | ||||
| ) => { | ||||
| export function DropdownMenuCheckboxItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [ | ||||
| 		"class", | ||||
| 		"children", | ||||
|     '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)", | ||||
|         '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} | ||||
| @ -263,26 +237,24 @@ export const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">( | ||||
|       {props.children} | ||||
|     </DropdownMenuPrimitive.CheckboxItem> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type dropdownMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps< | ||||
| type dropdownMenuRadioItemProps<T extends ValidComponent = 'div'> = ParentProps< | ||||
|   DropdownMenuRadioItemProps<T> & { | ||||
|     class?: string; | ||||
|   } | ||||
| >; | ||||
| 
 | ||||
| export const DropdownMenuRadioItem = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>, | ||||
| ) => { | ||||
| export function DropdownMenuRadioItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [ | ||||
| 		"class", | ||||
| 		"children", | ||||
|     '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)", | ||||
|         '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} | ||||
| @ -311,4 +283,4 @@ export const DropdownMenuRadioItem = <T extends ValidComponent = "div">( | ||||
|       {props.children} | ||||
|     </DropdownMenuPrimitive.RadioItem> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
|  | ||||
							
								
								
									
										20
									
								
								packages/app/src/modules/ui/components/sonner.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								packages/app/src/modules/ui/components/sonner.tsx
									
									
									
									
									
										Normal 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} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| @ -1,28 +1,24 @@ | ||||
| import { cn } from "@/modules/ui/utils/cn"; | ||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; | ||||
| 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"; | ||||
| } from '@kobalte/core/switch'; | ||||
| import type { ParentProps, ValidComponent, VoidProps } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { Switch as SwitchPrimitive } from '@kobalte/core/switch'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| 
 | ||||
| export const SwitchLabel = SwitchPrimitive.Label; | ||||
| export const Switch = SwitchPrimitive; | ||||
| export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage; | ||||
| export const SwitchDescription = SwitchPrimitive.Description; | ||||
| 
 | ||||
| type switchControlProps<T extends ValidComponent = "input"> = ParentProps< | ||||
| 	SwitchControlProps<T> & { class?: string } | ||||
| >; | ||||
| type switchControlProps<T extends ValidComponent = 'input'> = ParentProps<SwitchControlProps<T> & { class?: string }>; | ||||
| 
 | ||||
| export const SwitchControl = <T extends ValidComponent = "input">( | ||||
| 	props: PolymorphicProps<T, switchControlProps<T>>, | ||||
| ) => { | ||||
| export function SwitchControl<T extends ValidComponent = 'input'>(props: PolymorphicProps<T, switchControlProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as switchControlProps, [ | ||||
| 		"class", | ||||
| 		"children", | ||||
|     'class', | ||||
|     'children', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
| @ -30,7 +26,7 @@ export const SwitchControl = <T extends ValidComponent = "input">( | ||||
|       <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]", | ||||
|           '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} | ||||
| @ -39,24 +35,20 @@ export const SwitchControl = <T extends ValidComponent = "input">( | ||||
|       </SwitchPrimitive.Control> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type switchThumbProps<T extends ValidComponent = "div"> = VoidProps< | ||||
| 	SwitchThumbProps<T> & { class?: string } | ||||
| >; | ||||
| 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"]); | ||||
| export function SwitchThumb<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, switchThumbProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as switchThumbProps, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <SwitchPrimitive.Thumb | ||||
|       class={cn( | ||||
| 				"pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg transition-transform data-[checked]:translate-x-4", | ||||
|         'pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg transition-transform data-[checked]:translate-x-4', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
|  | ||||
| @ -1,28 +1,26 @@ | ||||
| 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"; | ||||
| import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||
| import type { TextFieldTextAreaProps } from '@kobalte/core/text-field'; | ||||
| import type { ValidComponent, VoidProps } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { TextArea as TextFieldPrimitive } from '@kobalte/core/text-field'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| 
 | ||||
| type textAreaProps<T extends ValidComponent = "textarea"> = VoidProps< | ||||
| 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"]); | ||||
| export function TextArea<T extends ValidComponent = 'textarea'>(props: PolymorphicProps<T, textAreaProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textAreaProps, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <TextFieldPrimitive | ||||
|       class={cn( | ||||
| 				"flex min-h-[60px] w-full rounded-md border border-input bg-inherit px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow", | ||||
|         'flex min-h-[60px] w-full rounded-md border border-input bg-inherit px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
|  | ||||
| @ -1,42 +1,40 @@ | ||||
| import { cn } from "@/modules/ui/utils/cn"; | ||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; | ||||
| 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"; | ||||
| } from '@kobalte/core/text-field'; | ||||
| import type { ValidComponent, VoidProps } from 'solid-js'; | ||||
| import { cn } from '@/modules/ui/utils/cn'; | ||||
| import { TextField as TextFieldPrimitive } from '@kobalte/core/text-field'; | ||||
| import { cva } from 'class-variance-authority'; | ||||
| import { splitProps } from 'solid-js'; | ||||
| 
 | ||||
| type textFieldProps<T extends ValidComponent = "div"> = | ||||
| 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"]); | ||||
| export function TextFieldRoot<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textFieldProps, ['class']); | ||||
| 
 | ||||
| 	return <TextFieldPrimitive class={cn("space-y-1", local.class)} {...rest} />; | ||||
| }; | ||||
|   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", | ||||
|   'text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium', | ||||
|   { | ||||
|     variants: { | ||||
|       label: { | ||||
| 				true: "data-[invalid]:text-destructive", | ||||
|         true: 'data-[invalid]:text-destructive', | ||||
|       }, | ||||
|       error: { | ||||
| 				true: "text-destructive text-xs", | ||||
|         true: 'text-destructive text-xs', | ||||
|       }, | ||||
|       description: { | ||||
| 				true: "font-normal text-muted-foreground", | ||||
|         true: 'font-normal text-muted-foreground', | ||||
|       }, | ||||
|     }, | ||||
|     defaultVariants: { | ||||
| @ -45,15 +43,13 @@ export const textfieldLabel = cva( | ||||
|   }, | ||||
| ); | ||||
| 
 | ||||
| type textFieldLabelProps<T extends ValidComponent = "label"> = | ||||
| 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"]); | ||||
| export function TextFieldLabel<T extends ValidComponent = 'label'>(props: PolymorphicProps<T, textFieldLabelProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textFieldLabelProps, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <TextFieldPrimitive.Label | ||||
| @ -61,18 +57,16 @@ export const TextFieldLabel = <T extends ValidComponent = "label">( | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type textFieldErrorMessageProps<T extends ValidComponent = "div"> = | ||||
| type textFieldErrorMessageProps<T extends ValidComponent = 'div'> = | ||||
|   TextFieldErrorMessageProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export const TextFieldErrorMessage = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, textFieldErrorMessageProps<T>>, | ||||
| ) => { | ||||
| export function TextFieldErrorMessage<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldErrorMessageProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textFieldErrorMessageProps, [ | ||||
| 		"class", | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
| @ -81,18 +75,16 @@ export const TextFieldErrorMessage = <T extends ValidComponent = "div">( | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type textFieldDescriptionProps<T extends ValidComponent = "div"> = | ||||
| type textFieldDescriptionProps<T extends ValidComponent = 'div'> = | ||||
|   TextFieldDescriptionProps<T> & { | ||||
|     class?: string; | ||||
|   }; | ||||
| 
 | ||||
| export const TextFieldDescription = <T extends ValidComponent = "div">( | ||||
| 	props: PolymorphicProps<T, textFieldDescriptionProps<T>>, | ||||
| ) => { | ||||
| export function TextFieldDescription<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldDescriptionProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textFieldDescriptionProps, [ | ||||
| 		"class", | ||||
|     'class', | ||||
|   ]); | ||||
| 
 | ||||
|   return ( | ||||
| @ -101,26 +93,24 @@ export const TextFieldDescription = <T extends ValidComponent = "div">( | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| type textFieldInputProps<T extends ValidComponent = "input"> = VoidProps< | ||||
| 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"]); | ||||
| export function TextField<T extends ValidComponent = 'input'>(props: PolymorphicProps<T, textFieldInputProps<T>>) { | ||||
|   const [local, rest] = splitProps(props as textFieldInputProps, ['class']); | ||||
| 
 | ||||
|   return ( | ||||
|     <TextFieldPrimitive.Input | ||||
|       class={cn( | ||||
| 				"flex h-9 w-full rounded-md border border-input bg-inherit px-3 py-1 text-sm shadow-sm file:(border-0 bg-transparent text-sm font-medium) placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow", | ||||
|         'flex h-9 w-full rounded-md border border-input bg-inherit px-3 py-1 text-sm shadow-sm file:(border-0 bg-transparent text-sm font-medium) placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow', | ||||
|         local.class, | ||||
|       )} | ||||
|       {...rest} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| } | ||||
|  | ||||
| @ -1,11 +1,13 @@ | ||||
| import type { LocaleKey } from '@/modules/i18n/i18n.types'; | ||||
| import type { Component, ParentComponent } from 'solid-js'; | ||||
| import { useCommandPalette } from '@/modules/command-palette/command-palette.provider'; | ||||
| import { useI18n } from '@/modules/i18n/i18n.provider'; | ||||
| import { Button } from '@/modules/ui/components/button'; | ||||
| import { A, 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 { useThemeStore } from '../themes/theme.store'; | ||||
| import { cn } from '../utils/cn'; | ||||
| import { socialLinks } from './app.layouts.constants'; | ||||
| 
 | ||||
| const ThemeSwitcher: Component = () => { | ||||
|   const themeStore = useThemeStore(); | ||||
| @ -30,17 +32,7 @@ const ThemeSwitcher: Component = () => { | ||||
| }; | ||||
| 
 | ||||
| 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}`); | ||||
|   }; | ||||
|   const { t, getLocale, changeLocale, locales } = useI18n(); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
| @ -62,14 +54,24 @@ const LanguageSwitcher: Component = () => { | ||||
| export const Navbar: Component = () => { | ||||
|   const themeStore = useThemeStore(); | ||||
|   const { t } = useI18n(); | ||||
|   const { openCommandPalette } = useCommandPalette(); | ||||
|   const getIsMacOs = () => navigator?.userAgent?.match(/Macintosh;/); | ||||
| 
 | ||||
|   return ( | ||||
|     <div class="border-b border-border bg-surface"> | ||||
|       <div class="flex items-center justify-between px-6 py-3 mx-auto max-w-1200px"> | ||||
|         <div class="flex items-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"> | ||||
|             <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> | ||||
|         </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> | ||||
|             © | ||||
|             {new Date().getFullYear()} | ||||
|             {' '} | ||||
|             Corentin Thomasset | ||||
|           </span> | ||||
|         </div> | ||||
|         <div class="text-xs text-foreground opacity-80%"> | ||||
|         </div> | ||||
|       </div> | ||||
|     </footer> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const AppLayout: ParentComponent = (props) => { | ||||
|   return ( | ||||
|     <div class="flex flex-col h-screen min-h-0"> | ||||
| @ -163,6 +273,7 @@ export const AppLayout: ParentComponent = (props) => { | ||||
| 
 | ||||
|       <div class="flex-1 pb-20 ">{props.children}</div> | ||||
| 
 | ||||
|       <Footer /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
							
								
								
									
										17
									
								
								packages/app/src/modules/ui/layouts/app.layouts.constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								packages/app/src/modules/ui/layouts/app.layouts.constants.ts
									
									
									
									
									
										Normal 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', | ||||
|   }, | ||||
| ]; | ||||
							
								
								
									
										12
									
								
								packages/app/templates/tools/new/tool.definition.ejs.t
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/app/templates/tools/new/tool.definition.ejs.t
									
									
									
									
									
										Normal 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) %>', | ||||
| }) | ||||
							
								
								
									
										4
									
								
								packages/app/templates/tools/new/tool.en.locale.ejs.t
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								packages/app/templates/tools/new/tool.en.locale.ejs.t
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| --- | ||||
| to: src/modules/tools/definitions/<%= h.changeCase.param(name) %>/locales/en.json | ||||
| --- | ||||
| {} | ||||
| @ -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'; | ||||
| @ -0,0 +1,6 @@ | ||||
| --- | ||||
| inject: true | ||||
| to: src/modules/tools/tools.registry.ts | ||||
| before: "^]" | ||||
| --- | ||||
|   <%= h.changeCase.camel(name) %>Tool, | ||||
							
								
								
									
										14
									
								
								packages/app/templates/tools/new/tool.page.ejs.t
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								packages/app/templates/tools/new/tool.page.ejs.t
									
									
									
									
									
										Normal 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) %>; | ||||
| @ -8,6 +8,7 @@ import { | ||||
| } from 'unocss'; | ||||
| import presetAnimations from 'unocss-preset-animations'; | ||||
| import { toolDefinitions } from './src/modules/tools/tools.registry'; | ||||
| import { socialLinks } from './src/modules/ui/layouts/app.layouts.constants'; | ||||
| 
 | ||||
| export default defineConfig({ | ||||
|   presets: [ | ||||
| @ -111,5 +112,9 @@ export default defineConfig({ | ||||
|   }, | ||||
|   safelist: [ | ||||
|     ...toolDefinitions.map(tool => tool.icon), | ||||
|     ...socialLinks.map(({ icon }) => icon), | ||||
|   ], | ||||
|   shortcuts: { | ||||
|     'i-logo': 'i-tabler-terminal', | ||||
|   }, | ||||
| }); | ||||
|  | ||||
							
								
								
									
										1596
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1596
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user