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", |     "typecheck": "tsc --noEmit", | ||||||
|     "test": "pnpm run test:unit", |     "test": "pnpm run test:unit", | ||||||
|     "test:unit": "vitest run", |     "test:unit": "vitest run", | ||||||
|     "test:unit:watch": "vitest watch" |     "test:unit:watch": "vitest watch", | ||||||
|  |     "create:tool": "HYGEN_TMPLS=templates hygen tools new" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@corentinth/chisels": "^1.0.4", |     "@corentinth/chisels": "^1.1.0", | ||||||
|     "@kobalte/core": "^0.13.6", |     "@kobalte/core": "^0.13.6", | ||||||
|     "@solid-primitives/i18n": "^2.1.1", |     "@solid-primitives/i18n": "^2.1.1", | ||||||
|     "@solid-primitives/storage": "^4.2.1", |     "@solid-primitives/storage": "^4.2.1", | ||||||
| @ -30,20 +31,24 @@ | |||||||
|     "@unocss/reset": "^0.62.4", |     "@unocss/reset": "^0.62.4", | ||||||
|     "class-variance-authority": "^0.7.0", |     "class-variance-authority": "^0.7.0", | ||||||
|     "clsx": "^2.1.1", |     "clsx": "^2.1.1", | ||||||
|  |     "cmdk-solid": "^1.1.0", | ||||||
|     "lodash-es": "^4.17.21", |     "lodash-es": "^4.17.21", | ||||||
|     "solid-js": "^1.9.1", |     "solid-js": "^1.9.1", | ||||||
|  |     "solid-sonner": "^0.2.8", | ||||||
|     "tailwind-merge": "^2.5.2" |     "tailwind-merge": "^2.5.2" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@antfu/eslint-config": "^3.7.3", |     "@antfu/eslint-config": "^3.7.3", | ||||||
|     "@iconify-json/tabler": "^1.2.3", |     "@iconify-json/tabler": "^1.2.3", | ||||||
|     "@types/lodash-es": "^4.17.12", |     "@types/lodash-es": "^4.17.12", | ||||||
|  |     "@vitest/coverage-v8": "2.1.2", | ||||||
|     "eslint": "^9.11.1", |     "eslint": "^9.11.1", | ||||||
|  |     "hygen": "^6.2.11", | ||||||
|     "typescript": "^5.6.2", |     "typescript": "^5.6.2", | ||||||
|     "unocss": "^0.62.4", |     "unocss": "^0.62.4", | ||||||
|     "unocss-preset-animations": "^1.1.0", |     "unocss-preset-animations": "^1.1.0", | ||||||
|     "vite": "^5.4.8", |     "vite": "^5.4.8", | ||||||
|     "vite-plugin-solid": "^2.10.2", |     "vite-plugin-solid": "^2.10.2", | ||||||
|     "vitest": "^2.1.1" |     "vitest": "^2.1.2" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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 type { LocaleKey } from './modules/i18n/i18n.types'; | ||||||
| import { A, Navigate, type RouteDefinition, useParams } from '@solidjs/router'; | import { A, Navigate, type RouteDefinition, useParams } from '@solidjs/router'; | ||||||
| import { map } from 'lodash-es'; | import { localeKeys } from './modules/i18n/i18n.constants'; | ||||||
| import { localeKeys, locales } from './modules/i18n/i18n.constants'; | import { useI18n } from './modules/i18n/i18n.provider'; | ||||||
| import { getBrowserLocale, useI18n } from './modules/i18n/i18n.provider'; |  | ||||||
| import { HomePage } from './modules/pages/home.page'; | import { HomePage } from './modules/pages/home.page'; | ||||||
| import { ToolPage } from './modules/tools/pages/tool.page'; | import { ToolPage } from './modules/tools/pages/tool.page'; | ||||||
| import { toolSlugs } from './modules/tools/tools.registry'; | import { toolSlugs } from './modules/tools/tools.registry'; | ||||||
|  | |||||||
| @ -4,7 +4,9 @@ import { ColorModeProvider, ColorModeScript, createLocalStorageManager } from '@ | |||||||
| import { Router } from '@solidjs/router'; | import { Router } from '@solidjs/router'; | ||||||
| import { render, Suspense } from 'solid-js/web'; | import { render, Suspense } from 'solid-js/web'; | ||||||
| import { routes } from './client-routes'; | import { routes } from './client-routes'; | ||||||
|  | import { CommandPaletteProvider } from './modules/command-palette/command-palette.provider'; | ||||||
| import { RootI18nProvider } from './modules/i18n/i18n.provider'; | import { RootI18nProvider } from './modules/i18n/i18n.provider'; | ||||||
|  | import { Toaster } from './modules/ui/components/sonner'; | ||||||
| import '@unocss/reset/tailwind.css'; | import '@unocss/reset/tailwind.css'; | ||||||
| import 'virtual:uno.css'; | import 'virtual:uno.css'; | ||||||
| import './app.css'; | import './app.css'; | ||||||
| @ -26,7 +28,10 @@ render( | |||||||
|                 initialColorMode={initialColorMode} |                 initialColorMode={initialColorMode} | ||||||
|                 storageManager={localStorageManager} |                 storageManager={localStorageManager} | ||||||
|               > |               > | ||||||
|                 <div class="min-h-screen font-sans text-sm font-400">{props.children}</div> |                 <CommandPaletteProvider> | ||||||
|  |                   <Toaster /> | ||||||
|  |                   <div class="min-h-screen font-sans text-sm font-400">{props.children}</div> | ||||||
|  |                 </CommandPaletteProvider> | ||||||
|               </ColorModeProvider> |               </ColorModeProvider> | ||||||
|             </RootI18nProvider> |             </RootI18nProvider> | ||||||
|           </Suspense> |           </Suspense> | ||||||
|  | |||||||
| @ -16,8 +16,46 @@ | |||||||
|     "support": "Support IT-Tools", |     "support": "Support IT-Tools", | ||||||
|     "report-bug": "Report a bug" |     "report-bug": "Report a bug" | ||||||
|   }, |   }, | ||||||
|  |   "footer": { | ||||||
|  |     "resources": { | ||||||
|  |       "title": "Resources", | ||||||
|  |       "all-tools": "All the tools", | ||||||
|  |       "github": "GitHub repository", | ||||||
|  |       "support": "Support IT-Tools", | ||||||
|  |       "license": "License" | ||||||
|  |     }, | ||||||
|  |     "support": { | ||||||
|  |       "title": "Support", | ||||||
|  |       "report-bug": "Report a bug", | ||||||
|  |       "request-feature": "Request a feature", | ||||||
|  |       "contribute": "Contribute to the project", | ||||||
|  |       "contact": "Contact me" | ||||||
|  |     }, | ||||||
|  |     "friends": { | ||||||
|  |       "title": "Friends" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "commandPalette": { | ||||||
|  |     "input-placeholder": "Type to search for a tool or a command...", | ||||||
|  |     "go-home": "Go to home", | ||||||
|  |     "sections": { | ||||||
|  |       "tools": "Tools", | ||||||
|  |       "navigation": "Navigation", | ||||||
|  |       "language": "Language", | ||||||
|  |       "theme": "Theme" | ||||||
|  |     }, | ||||||
|  |     "theme": { | ||||||
|  |       "switch-to-light": "Switch to light theme", | ||||||
|  |       "switch-to-dark": "Switch to dark theme", | ||||||
|  |       "switch-to-system": "Use to system theme" | ||||||
|  |     }, | ||||||
|  |     "trigger": { | ||||||
|  |       "search": "Search for a tool" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|   "home": { |   "home": { | ||||||
|     "all-tools": "All the tools", |     "all-tools": "All the tools", | ||||||
|  |     "search-tools": "Search for a tool", | ||||||
|     "open-source": "Open Source", |     "open-source": "Open Source", | ||||||
|     "free": "Free", |     "free": "Free", | ||||||
|     "self-hostable": "Self-hostable" |     "self-hostable": "Self-hostable" | ||||||
| @ -26,6 +64,10 @@ | |||||||
|     "token-generator": { |     "token-generator": { | ||||||
|       "name": "Token Generator", |       "name": "Token Generator", | ||||||
|       "description": "Generate random string with the characters you want, uppercase, lowercase letters, numbers and/or symbols." |       "description": "Generate random string with the characters you want, uppercase, lowercase letters, numbers and/or symbols." | ||||||
|  |     }, | ||||||
|  |     "random-port-generator": { | ||||||
|  |       "name": "Random Port Generator", | ||||||
|  |       "description": "Generate a random port number outside of the reserved ports range (0-1023)." | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -2,5 +2,58 @@ | |||||||
|   "app": { |   "app": { | ||||||
|     "title": "IT-Tools", |     "title": "IT-Tools", | ||||||
|     "description": "La collection open-source d'outils en ligne pour aider les devs dans leur vie quotidienne." |     "description": "La collection open-source d'outils en ligne pour aider les devs dans leur vie quotidienne." | ||||||
|  |   }, | ||||||
|  |   "navbar": { | ||||||
|  |     "theme": { | ||||||
|  |       "theme": "Thème", | ||||||
|  |       "light-mode": "Mode clair", | ||||||
|  |       "dark-mode": "Mode sombre", | ||||||
|  |       "system-mode": "Système" | ||||||
|  |     }, | ||||||
|  |     "language": "Langue", | ||||||
|  |     "contribute-to-i18n": "Contribuer à l'i18n", | ||||||
|  |     "github": "GitHub", | ||||||
|  |     "support": "Soutenir IT-Tools", | ||||||
|  |     "report-bug": "Signaler un bug" | ||||||
|  |   }, | ||||||
|  |   "footer": { | ||||||
|  |     "resources": { | ||||||
|  |       "title": "Ressources", | ||||||
|  |       "all-tools": "Tous les outils", | ||||||
|  |       "github": "Dépôt GitHub", | ||||||
|  |       "support": "Soutenir IT-Tools", | ||||||
|  |       "license": "Licence" | ||||||
|  |     }, | ||||||
|  |     "support": { | ||||||
|  |       "title": "Support", | ||||||
|  |       "report-bug": "Signaler un bug", | ||||||
|  |       "request-feature": "Demander une fonctionnalité", | ||||||
|  |       "contribute": "Contribuer au projet", | ||||||
|  |       "contact": "Me contacter" | ||||||
|  |     }, | ||||||
|  |     "friends": { | ||||||
|  |       "title": "Ami·e·s" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "commandPalette": { | ||||||
|  |     "input-placeholder": "Tapez pour rechercher un outil...", | ||||||
|  |     "go-home": "Aller à l'accueil", | ||||||
|  |     "sections": { | ||||||
|  |       "tools": "Outils", | ||||||
|  |       "navigation": "Navigation", | ||||||
|  |       "theme": "Thème" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "home": { | ||||||
|  |     "all-tools": "Tous les outils", | ||||||
|  |     "open-source": "Open Source", | ||||||
|  |     "free": "Gratuit", | ||||||
|  |     "self-hostable": "Self-hostable" | ||||||
|  |   }, | ||||||
|  |   "tools": { | ||||||
|  |     "token-generator": { | ||||||
|  |       "name": "Générateur de token", | ||||||
|  |       "description": "Générer des string aléatoires, contrôlez les caractères que vous voulez, lettres majuscules, minuscules, chiffres et/ou symboles." | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -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', |     key: 'en', | ||||||
|     file: 'en', |     file: 'en', | ||||||
|     name: 'English', |     name: 'English', | ||||||
|  |     switchToLabel: 'Change language to English', | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     key: 'fr', |     key: 'fr', | ||||||
|     file: 'fr', |     file: 'fr', | ||||||
|     name: 'Français', |     name: 'Français', | ||||||
|  |     switchToLabel: 'Changer la langue en Français', | ||||||
|   }, |   }, | ||||||
| ] as const; | ] as const; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,7 +1,9 @@ | |||||||
| import type { ParentComponent } from 'solid-js'; | import type { ParentComponent } from 'solid-js'; | ||||||
| import type { LocaleKey } from './i18n.types'; | import type { LocaleKey } from './i18n.types'; | ||||||
|  | import { joinUrlPaths } from '@corentinth/chisels'; | ||||||
| import * as i18n from '@solid-primitives/i18n'; | import * as i18n from '@solid-primitives/i18n'; | ||||||
| import { makePersisted } from '@solid-primitives/storage'; | import { makePersisted } from '@solid-primitives/storage'; | ||||||
|  | import { useNavigate } from '@solidjs/router'; | ||||||
| import { merge } from 'lodash-es'; | import { merge } from 'lodash-es'; | ||||||
| import { createContext, createResource, createSignal, Show, useContext } from 'solid-js'; | import { createContext, createResource, createSignal, Show, useContext } from 'solid-js'; | ||||||
| import defaultDict from '../../locales/en.json'; | import defaultDict from '../../locales/en.json'; | ||||||
| @ -23,6 +25,7 @@ const RootI18nContext = createContext<{ | |||||||
| 
 | 
 | ||||||
| function useI18n() { | function useI18n() { | ||||||
|   const context = useContext(RootI18nContext); |   const context = useContext(RootI18nContext); | ||||||
|  |   const navigate = useNavigate(); | ||||||
| 
 | 
 | ||||||
|   if (!context) { |   if (!context) { | ||||||
|     throw new Error('I18n context not found'); |     throw new Error('I18n context not found'); | ||||||
| @ -35,6 +38,18 @@ function useI18n() { | |||||||
|     getLocale, |     getLocale, | ||||||
|     setLocale, |     setLocale, | ||||||
|     locales, |     locales, | ||||||
|  |     createLocalizedUrl: ({ path }: { path: string }) => { | ||||||
|  |       const newPath = joinUrlPaths(getLocale(), path); | ||||||
|  | 
 | ||||||
|  |       return `/${newPath}`; | ||||||
|  |     }, | ||||||
|  |     changeLocale: (locale: LocaleKey) => { | ||||||
|  |       setLocale(locale); | ||||||
|  | 
 | ||||||
|  |       const pathWithoutLocale = location.pathname.split('/').slice(2).join('/'); | ||||||
|  |       const newPath = joinUrlPaths(locale, pathWithoutLocale); | ||||||
|  |       navigate(`/${newPath}`); | ||||||
|  |     }, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,3 @@ | |||||||
| import type { locales } from './i18n.constants'; | import type { locales } from './i18n.constants'; | ||||||
| 
 | 
 | ||||||
| export type LocaleKey = typeof locales[number]['key']; | export type LocaleKey = typeof locales[number]['key']; | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -1,15 +1,17 @@ | |||||||
| import type { Component } from 'solid-js'; | import type { Component } from 'solid-js'; | ||||||
| import { A } from '@solidjs/router'; | import { A } from '@solidjs/router'; | ||||||
|  | import { useCommandPalette } from '../command-palette/command-palette.provider'; | ||||||
| import { useI18n } from '../i18n/i18n.provider'; | import { useI18n } from '../i18n/i18n.provider'; | ||||||
| import { useToolsStore } from '../tools/tools.store'; | import { useToolsStore } from '../tools/tools.store'; | ||||||
| import { Badge } from '../ui/components/badge'; | import { Badge } from '../ui/components/badge'; | ||||||
| import { Button } from '../ui/components/button'; | import { Button } from '../ui/components/button'; | ||||||
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/components/card'; | import { Card, CardDescription, CardHeader, CardTitle } from '../ui/components/card'; | ||||||
| import { cn } from '../ui/utils/cn'; | import { cn } from '../ui/utils/cn'; | ||||||
| 
 | 
 | ||||||
| export const HomePage: Component = () => { | export const HomePage: Component = () => { | ||||||
|   const { t } = useI18n(); |   const { t } = useI18n(); | ||||||
|   const { tools } = useToolsStore(); |   const { getTools } = useToolsStore(); | ||||||
|  |   const { openCommandPalette } = useCommandPalette(); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div> |     <div> | ||||||
| @ -39,11 +41,16 @@ export const HomePage: Component = () => { | |||||||
|               {t('app.description')} |               {t('app.description')} | ||||||
|             </p> |             </p> | ||||||
| 
 | 
 | ||||||
|             <div> |             <div class="flex items-center gap-4"> | ||||||
|               <Button variant="default" as={A} href="tools"> |               <Button variant="default" as={A} href="tools"> | ||||||
|                 {t('home.all-tools')} |                 {t('home.all-tools')} | ||||||
|                 <div class="i-tabler-arrow-right ml-2 text-base"></div> |                 <div class="i-tabler-arrow-right ml-2 text-base"></div> | ||||||
|               </Button> |               </Button> | ||||||
|  | 
 | ||||||
|  |               <Button variant="outline" onClick={openCommandPalette}> | ||||||
|  |                 <div class="i-tabler-search mr-2 text-base" /> | ||||||
|  |                 {t('home.search-tools')} | ||||||
|  |               </Button> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
| @ -57,9 +64,9 @@ export const HomePage: Component = () => { | |||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 max-w-1200px mx-auto p-6"> |       <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 max-w-1200px mx-auto p-6"> | ||||||
|         {tools.map(tool => ( |         {getTools().map(tool => ( | ||||||
|           <A href={tool.slug}> |           <A href={tool.slug} class="h-full"> | ||||||
|             <Card class="hover:(shadow-md transform scale-101) transition-transform"> |             <Card class="hover:(shadow-md transform scale-101) transition-transform h-full"> | ||||||
|               <CardHeader> |               <CardHeader> | ||||||
|                 <div class={cn(tool.icon, 'size-12 text-muted-foreground/60')} /> |                 <div class={cn(tool.icon, 'size-12 text-muted-foreground/60')} /> | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										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)", |   "uppercase": "Uppercase letters (A-Z)", | ||||||
|   "lowercase": "Lowercase letters (a-z)", |   "lowercase": "Lowercase letters (a-z)", | ||||||
|   "numbers": "Numbers (0-9)", |   "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)", |   "uppercase": "Lettres majuscules (A-Z)", | ||||||
|   "lowercase": "Lettres minuscules (a-z)", |   "lowercase": "Lettres minuscules (a-z)", | ||||||
|   "numbers": "Chiffres (0-9)", |   "numbers": "Chiffres (0-9)", | ||||||
|  | |||||||
| @ -5,5 +5,5 @@ export const tokenGeneratorTool = defineTool({ | |||||||
|   entryFile: () => import('./token-generator.page'), |   entryFile: () => import('./token-generator.page'), | ||||||
|   icon: 'i-tabler-key', |   icon: 'i-tabler-key', | ||||||
|   createdAt: new Date('2024-02-13'), |   createdAt: new Date('2024-02-13'), | ||||||
|   currentDirUrl: import.meta.url, |   dirName: 'token-generator', | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -1,12 +1,7 @@ | |||||||
| import type { LocaleKey } from '@/modules/i18n/i18n.types'; |  | ||||||
| import type { Flatten } from '@solid-primitives/i18n'; |  | ||||||
| import type { ToolI18nFactory } from '../tools.types'; |  | ||||||
| import { useI18n } from '@/modules/i18n/i18n.provider'; | import { useI18n } from '@/modules/i18n/i18n.provider'; | ||||||
| import { safely } from '@corentinth/chisels'; | import { safely } from '@corentinth/chisels'; | ||||||
| import { flatten, translator } from '@solid-primitives/i18n'; |  | ||||||
| import { useParams } from '@solidjs/router'; | import { useParams } from '@solidjs/router'; | ||||||
| import { merge } from 'lodash-es'; | import { type Component, createResource, lazy, Show } from 'solid-js'; | ||||||
| import { type Component, createContext, createResource, lazy, Show } from 'solid-js'; |  | ||||||
| import { CurrentToolProvider } from '../tools.provider'; | import { CurrentToolProvider } from '../tools.provider'; | ||||||
| import { getToolDefinitionBySlug } from '../tools.registry'; | import { getToolDefinitionBySlug } from '../tools.registry'; | ||||||
| 
 | 
 | ||||||
| @ -18,9 +13,13 @@ export const ToolPage: Component = () => { | |||||||
|   const ToolComponent = lazy(toolDefinition.entryFile); |   const ToolComponent = lazy(toolDefinition.entryFile); | ||||||
| 
 | 
 | ||||||
|   const [toolDict] = createResource(getLocale, async (locale) => { |   const [toolDict] = createResource(getLocale, async (locale) => { | ||||||
|     const [dict = { default: {} }] = await safely(import(`../definitions/${toolDefinition.dirName}/locales/${locale}.json`)); |     const [dict, error] = await safely(import(`../definitions/${toolDefinition.dirName}/locales/${locale}.json`)); | ||||||
| 
 | 
 | ||||||
|     return dict; |     if (error) { | ||||||
|  |       console.error(error); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return dict ?? { default: {} }; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  | |||||||
| @ -5,13 +5,12 @@ export { defineTool }; | |||||||
| function defineTool(toolDefinition: { | function defineTool(toolDefinition: { | ||||||
|   slug: string; |   slug: string; | ||||||
|   entryFile: () => Promise<{ default: Component }>; |   entryFile: () => Promise<{ default: Component }>; | ||||||
|   currentDirUrl: string; |   dirName: string; | ||||||
|   icon: string; |   icon: string; | ||||||
|   createdAt: Date; |   createdAt: Date; | ||||||
| }) { | }) { | ||||||
|   return { |   return { | ||||||
|     ...toolDefinition, |     ...toolDefinition, | ||||||
|     key: toolDefinition.slug, |     key: toolDefinition.slug, | ||||||
|     dirName: toolDefinition.currentDirUrl.split('/').slice(-2)[0], |  | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| import type { Accessor, ParentComponent } from 'solid-js'; | import type { Accessor, ParentComponent } from 'solid-js'; | ||||||
| import type { ToolI18nFactory } from './tools.types'; | import { flatten, translator } from '@solid-primitives/i18n'; | ||||||
| import { flatten, type Flatten, translator, type Translator } from '@solid-primitives/i18n'; |  | ||||||
| import { merge } from 'lodash-es'; | import { merge } from 'lodash-es'; | ||||||
| import { createContext, useContext } from 'solid-js'; | import { createContext, useContext } from 'solid-js'; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,8 +1,10 @@ | |||||||
| import { keyBy, map } from 'lodash-es'; | import { keyBy, map } from 'lodash-es'; | ||||||
|  | import { randomPortGeneratorTool } from './definitions/random-port-generator/random-port-generator.tool'; | ||||||
| import { tokenGeneratorTool } from './definitions/token-generator/token-generator.tool'; | import { tokenGeneratorTool } from './definitions/token-generator/token-generator.tool'; | ||||||
| 
 | 
 | ||||||
| export const toolDefinitions = [ | export const toolDefinitions = [ | ||||||
|   tokenGeneratorTool, |   tokenGeneratorTool, | ||||||
|  |   randomPortGeneratorTool, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| export const toolSlugs = map(toolDefinitions, 'slug'); | export const toolSlugs = map(toolDefinitions, 'slug'); | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import { createMemo } from 'solid-js'; | ||||||
| import { useI18n } from '../i18n/i18n.provider'; | import { useI18n } from '../i18n/i18n.provider'; | ||||||
| import { toolDefinitions } from './tools.registry'; | import { toolDefinitions } from './tools.registry'; | ||||||
| 
 | 
 | ||||||
| @ -6,13 +7,13 @@ export { useToolsStore }; | |||||||
| function useToolsStore() { | function useToolsStore() { | ||||||
|   const { t } = useI18n(); |   const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|   const tools = toolDefinitions.map((tool) => { |   const getTools = createMemo(() => toolDefinitions.map((tool) => { | ||||||
|     return { |     return { | ||||||
|       ...tool, |       ...tool, | ||||||
|       name: t(`tools.${tool.slug}.name` as any) ?? tool.slug, |       name: t(`tools.${tool.slug}.name` as any) ?? tool.slug, | ||||||
|       description: t(`tools.${tool.slug}.description` as any) ?? tool.slug, |       description: t(`tools.${tool.slug}.description` as any) ?? tool.slug, | ||||||
|     }; |     }; | ||||||
|   }); |   })); | ||||||
| 
 | 
 | ||||||
|   return { tools }; |   return { getTools }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,42 +1,37 @@ | |||||||
| import { cn } from "@/modules/ui/utils/cn"; | import type { VariantProps } from 'class-variance-authority'; | ||||||
| import type { VariantProps } from "class-variance-authority"; | import { cn } from '@/modules/ui/utils/cn'; | ||||||
| import { cva } from "class-variance-authority"; | import { cva } from 'class-variance-authority'; | ||||||
| import { type ComponentProps, splitProps } from "solid-js"; | import { type ComponentProps, splitProps } from 'solid-js'; | ||||||
| 
 | 
 | ||||||
| export const badgeVariants = cva( | export const badgeVariants = cva( | ||||||
| 	"inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-shadow focus-visible:(outline-none ring-1.5 ring-ring)", |   'inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-shadow focus-visible:(outline-none ring-1.5 ring-ring)', | ||||||
| 	{ |   { | ||||||
| 		variants: { |     variants: { | ||||||
| 			variant: { |       variant: { | ||||||
| 				default: |         default: 'bg-primary text-primary-foreground shadow hover:bg-primary/80', | ||||||
| 					"bg-primary text-primary-foreground shadow hover:bg-primary/80", |         secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', | ||||||
| 				secondary: |         destructive: 'bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', | ||||||
| 					"bg-secondary text-secondary-foreground hover:bg-secondary/80", |         outline: 'border text-foreground', | ||||||
| 				destructive: |       }, | ||||||
| 					"bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", |     }, | ||||||
| 				outline: "border text-foreground", |     defaultVariants: { | ||||||
| 			}, |       variant: 'default', | ||||||
| 		}, |     }, | ||||||
| 		defaultVariants: { |   }, | ||||||
| 			variant: "default", |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const Badge = ( | export function Badge(props: ComponentProps<'div'> & VariantProps<typeof badgeVariants>) { | ||||||
| 	props: ComponentProps<"div"> & VariantProps<typeof badgeVariants>, |   const [local, rest] = splitProps(props, ['class', 'variant']); | ||||||
| ) => { |  | ||||||
| 	const [local, rest] = splitProps(props, ["class", "variant"]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<div |     <div | ||||||
| 			class={cn( |       class={cn( | ||||||
| 				badgeVariants({ |         badgeVariants({ | ||||||
| 					variant: local.variant, |           variant: local.variant, | ||||||
| 				}), |         }), | ||||||
| 				local.class, |         local.class, | ||||||
| 			)} |       )} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
|  | |||||||
| @ -1,60 +1,60 @@ | |||||||
| import { cn } from "@/modules/ui/utils/cn"; | import type { ComponentProps, ParentComponent } from 'solid-js'; | ||||||
| import type { ComponentProps, ParentComponent } from "solid-js"; | import { cn } from '@/modules/ui/utils/cn'; | ||||||
| import { splitProps } from "solid-js"; | import { splitProps } from 'solid-js'; | ||||||
| 
 | 
 | ||||||
| export const Card = (props: ComponentProps<"div">) => { | export function Card(props: ComponentProps<'div'>) { | ||||||
| 	const [local, rest] = splitProps(props, ["class"]); |   const [local, rest] = splitProps(props, ['class']); | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<div |     <div | ||||||
| 			class={cn( |       class={cn( | ||||||
| 				"rounded-xl border bg-card text-card-foreground shadow", |         'rounded-xl border bg-card text-card-foreground shadow', | ||||||
| 				local.class, |         local.class, | ||||||
| 			)} |       )} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function CardHeader(props: ComponentProps<'div'>) { | ||||||
|  |   const [local, rest] = splitProps(props, ['class']); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div class={cn('flex flex-col space-y-1.5 p-6', local.class)} {...rest} /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const CardTitle: ParentComponent<ComponentProps<'h1'>> = (props) => { | ||||||
|  |   const [local, rest] = splitProps(props, ['class']); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <h1 | ||||||
|  |       class={cn('font-semibold leading-none tracking-tight', local.class)} | ||||||
|  |       {...rest} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const CardHeader = (props: ComponentProps<"div">) => { | export const CardDescription: ParentComponent<ComponentProps<'h3'>> = ( | ||||||
| 	const [local, rest] = splitProps(props, ["class"]); |   props, | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<div class={cn("flex flex-col space-y-1.5 p-6", local.class)} {...rest} /> |  | ||||||
| 	); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const CardTitle: ParentComponent<ComponentProps<"h1">> = (props) => { |  | ||||||
| 	const [local, rest] = splitProps(props, ["class"]); |  | ||||||
| 
 |  | ||||||
| 	return ( |  | ||||||
| 		<h1 |  | ||||||
| 			class={cn("font-semibold leading-none tracking-tight", local.class)} |  | ||||||
| 			{...rest} |  | ||||||
| 		/> |  | ||||||
| 	); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const CardDescription: ParentComponent<ComponentProps<"h3">> = ( |  | ||||||
| 	props, |  | ||||||
| ) => { | ) => { | ||||||
| 	const [local, rest] = splitProps(props, ["class"]); |   const [local, rest] = splitProps(props, ['class']); | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<h3 class={cn("text-sm text-muted-foreground", local.class)} {...rest} /> |     <h3 class={cn('text-sm text-muted-foreground', local.class)} {...rest} /> | ||||||
| 	); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const CardContent = (props: ComponentProps<"div">) => { | export function CardContent(props: ComponentProps<'div'>) { | ||||||
| 	const [local, rest] = splitProps(props, ["class"]); |   const [local, rest] = splitProps(props, ['class']); | ||||||
| 
 | 
 | ||||||
| 	return <div class={cn("p-6 pt-0", local.class)} {...rest} />; |   return <div class={cn('p-6 pt-0', local.class)} {...rest} />; | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export const CardFooter = (props: ComponentProps<"div">) => { | export function CardFooter(props: ComponentProps<'div'>) { | ||||||
| 	const [local, rest] = splitProps(props, ["class"]); |   const [local, rest] = splitProps(props, ['class']); | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<div class={cn("flex items-center p-6 pt-0", local.class)} {...rest} /> |     <div class={cn('flex items-center p-6 pt-0', local.class)} {...rest} /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
|  | |||||||
							
								
								
									
										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,314 +1,286 @@ | |||||||
| import { cn } from "@/modules/ui/utils/cn"; |  | ||||||
| import type { | import type { | ||||||
| 	DropdownMenuCheckboxItemProps, |   DropdownMenuCheckboxItemProps, | ||||||
| 	DropdownMenuContentProps, |   DropdownMenuContentProps, | ||||||
| 	DropdownMenuGroupLabelProps, |   DropdownMenuGroupLabelProps, | ||||||
| 	DropdownMenuItemLabelProps, |   DropdownMenuItemLabelProps, | ||||||
| 	DropdownMenuItemProps, |   DropdownMenuItemProps, | ||||||
| 	DropdownMenuRadioItemProps, |   DropdownMenuRadioItemProps, | ||||||
| 	DropdownMenuRootProps, |   DropdownMenuRootProps, | ||||||
| 	DropdownMenuSeparatorProps, |   DropdownMenuSeparatorProps, | ||||||
| 	DropdownMenuSubTriggerProps, |   DropdownMenuSubTriggerProps, | ||||||
| } from "@kobalte/core/dropdown-menu"; | } from '@kobalte/core/dropdown-menu'; | ||||||
| import { DropdownMenu as DropdownMenuPrimitive } from "@kobalte/core/dropdown-menu"; | import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; | import type { ComponentProps, ParentProps, ValidComponent } from 'solid-js'; | ||||||
| import type { ComponentProps, ParentProps, ValidComponent } from "solid-js"; | import { cn } from '@/modules/ui/utils/cn'; | ||||||
| import { mergeProps, splitProps } from "solid-js"; | import { DropdownMenu as DropdownMenuPrimitive } from '@kobalte/core/dropdown-menu'; | ||||||
|  | import { mergeProps, splitProps } from 'solid-js'; | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; | export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; | ||||||
| export const DropdownMenuGroup = DropdownMenuPrimitive.Group; | export const DropdownMenuGroup = DropdownMenuPrimitive.Group; | ||||||
| export const DropdownMenuSub = DropdownMenuPrimitive.Sub; | export const DropdownMenuSub = DropdownMenuPrimitive.Sub; | ||||||
| export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; | export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; | ||||||
| 
 | 
 | ||||||
| export const DropdownMenu = (props: DropdownMenuRootProps) => { | export function DropdownMenu(props: DropdownMenuRootProps) { | ||||||
| 	const merge = mergeProps<DropdownMenuRootProps[]>({ gutter: 4 }, props); |   const merge = mergeProps<DropdownMenuRootProps[]>({ gutter: 4 }, props); | ||||||
| 
 | 
 | ||||||
| 	return <DropdownMenuPrimitive {...merge} />; |   return <DropdownMenuPrimitive {...merge} />; | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type dropdownMenuContentProps<T extends ValidComponent = "div"> = | type dropdownMenuContentProps<T extends ValidComponent = 'div'> = | ||||||
| 	DropdownMenuContentProps<T> & { |   DropdownMenuContentProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuContent = <T extends ValidComponent = "div">( | export function DropdownMenuContent<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuContentProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, dropdownMenuContentProps<T>>, |   const [local, rest] = splitProps(props as dropdownMenuContentProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as dropdownMenuContentProps, [ |   ]); | ||||||
| 		"class", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<DropdownMenuPrimitive.Portal> |     <DropdownMenuPrimitive.Portal> | ||||||
| 			<DropdownMenuPrimitive.Content |       <DropdownMenuPrimitive.Content | ||||||
| 				class={cn( |         class={cn( | ||||||
| 					"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95) focus-visible:(outline-none ring-1.5 ring-ring) transition-shadow", |           'min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95) focus-visible:(outline-none ring-1.5 ring-ring) transition-shadow', | ||||||
| 					local.class, |           local.class, | ||||||
| 				)} |         )} | ||||||
| 				{...rest} |         {...rest} | ||||||
| 			/> |       /> | ||||||
| 		</DropdownMenuPrimitive.Portal> |     </DropdownMenuPrimitive.Portal> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type dropdownMenuItemProps<T extends ValidComponent = "div"> = | type dropdownMenuItemProps<T extends ValidComponent = 'div'> = | ||||||
| 	DropdownMenuItemProps<T> & { |   DropdownMenuItemProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 		inset?: boolean; |     inset?: boolean; | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuItem = <T extends ValidComponent = "div">( | export function DropdownMenuItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuItemProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, dropdownMenuItemProps<T>>, |   const [local, rest] = splitProps(props as dropdownMenuItemProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as dropdownMenuItemProps, [ |     'inset', | ||||||
| 		"class", |   ]); | ||||||
| 		"inset", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<DropdownMenuPrimitive.Item |     <DropdownMenuPrimitive.Item | ||||||
| 			class={cn( |       class={cn( | ||||||
| 				"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)", |         'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)', | ||||||
| 				local.inset && "pl-8", |         local.inset && 'pl-8', | ||||||
| 				local.class, |         local.class, | ||||||
| 			)} |       )} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type dropdownMenuGroupLabelProps<T extends ValidComponent = "span"> = | type dropdownMenuGroupLabelProps<T extends ValidComponent = 'span'> = | ||||||
| 	DropdownMenuGroupLabelProps<T> & { |   DropdownMenuGroupLabelProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuGroupLabel = <T extends ValidComponent = "span">( | export function DropdownMenuGroupLabel<T extends ValidComponent = 'span'>(props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>, |   const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [ |   ]); | ||||||
| 		"class", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<DropdownMenuPrimitive.GroupLabel |     <DropdownMenuPrimitive.GroupLabel | ||||||
| 			as="div" |       as="div" | ||||||
| 			class={cn("px-2 py-1.5 text-sm font-semibold", local.class)} |       class={cn('px-2 py-1.5 text-sm font-semibold', local.class)} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type dropdownMenuItemLabelProps<T extends ValidComponent = "div"> = | type dropdownMenuItemLabelProps<T extends ValidComponent = 'div'> = | ||||||
| 	DropdownMenuItemLabelProps<T> & { |   DropdownMenuItemLabelProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuItemLabel = <T extends ValidComponent = "div">( | export function DropdownMenuItemLabel<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>, |   const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [ |   ]); | ||||||
| 		"class", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<DropdownMenuPrimitive.ItemLabel |     <DropdownMenuPrimitive.ItemLabel | ||||||
| 			as="div" |       as="div" | ||||||
| 			class={cn("px-2 py-1.5 text-sm font-semibold", local.class)} |       class={cn('px-2 py-1.5 text-sm font-semibold', local.class)} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type dropdownMenuSeparatorProps<T extends ValidComponent = "hr"> = | type dropdownMenuSeparatorProps<T extends ValidComponent = 'hr'> = | ||||||
| 	DropdownMenuSeparatorProps<T> & { |   DropdownMenuSeparatorProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuSeparator = <T extends ValidComponent = "hr">( | export function DropdownMenuSeparator<T extends ValidComponent = 'hr'>(props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>, |   const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [ |   ]); | ||||||
| 		"class", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<DropdownMenuPrimitive.Separator |     <DropdownMenuPrimitive.Separator | ||||||
| 			class={cn("-mx-1 my-1 h-px bg-muted", local.class)} |       class={cn('-mx-1 my-1 h-px bg-muted', local.class)} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuShortcut = (props: ComponentProps<"span">) => { | export function DropdownMenuShortcut(props: ComponentProps<'span'>) { | ||||||
| 	const [local, rest] = splitProps(props, ["class"]); |   const [local, rest] = splitProps(props, ['class']); | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<span |     <span | ||||||
| 			class={cn("ml-auto text-xs tracking-widest opacity-60", local.class)} |       class={cn('ml-auto text-xs tracking-widest opacity-60', local.class)} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type dropdownMenuSubTriggerProps<T extends ValidComponent = "div"> = | type dropdownMenuSubTriggerProps<T extends ValidComponent = 'div'> = ParentProps<DropdownMenuSubTriggerProps<T> & { class?: string }>; | ||||||
| 	ParentProps< |  | ||||||
| 		DropdownMenuSubTriggerProps<T> & { |  | ||||||
| 			class?: string; |  | ||||||
| 		} |  | ||||||
| 	>; |  | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuSubTrigger = <T extends ValidComponent = "div">( | export function DropdownMenuSubTrigger<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>, |   const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [ |     'children', | ||||||
| 		"class", |   ]); | ||||||
| 		"children", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<DropdownMenuPrimitive.SubTrigger |     <DropdownMenuPrimitive.SubTrigger | ||||||
| 			class={cn( |       class={cn( | ||||||
| 				"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[expanded]:bg-accent", |         'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[expanded]:bg-accent', | ||||||
| 				local.class, |         local.class, | ||||||
| 			)} |       )} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		> |     > | ||||||
| 			{local.children} |       {local.children} | ||||||
| 			<svg |       <svg | ||||||
| 				xmlns="http://www.w3.org/2000/svg" |         xmlns="http://www.w3.org/2000/svg" | ||||||
| 				width="1em" |         width="1em" | ||||||
| 				height="1em" |         height="1em" | ||||||
| 				viewBox="0 0 24 24" |         viewBox="0 0 24 24" | ||||||
| 				class="ml-auto h-4 w-4" |         class="ml-auto h-4 w-4" | ||||||
| 			> |       > | ||||||
| 				<path |         <path | ||||||
| 					fill="none" |           fill="none" | ||||||
| 					stroke="currentColor" |           stroke="currentColor" | ||||||
| 					stroke-linecap="round" |           stroke-linecap="round" | ||||||
| 					stroke-linejoin="round" |           stroke-linejoin="round" | ||||||
| 					stroke-width="2" |           stroke-width="2" | ||||||
| 					d="m9 6l6 6l-6 6" |           d="m9 6l6 6l-6 6" | ||||||
| 				/> |         /> | ||||||
| 				<title>Arrow</title> |         <title>Arrow</title> | ||||||
| 			</svg> |       </svg> | ||||||
| 		</DropdownMenuPrimitive.SubTrigger> |     </DropdownMenuPrimitive.SubTrigger> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type dropdownMenuSubContentProps<T extends ValidComponent = "div"> = | type dropdownMenuSubContentProps<T extends ValidComponent = 'div'> = | ||||||
| 	DropdownMenuSubTriggerProps<T> & { |   DropdownMenuSubTriggerProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuSubContent = <T extends ValidComponent = "div">( | export function DropdownMenuSubContent<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>, |   const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [ |   ]); | ||||||
| 		"class", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<DropdownMenuPrimitive.Portal> |     <DropdownMenuPrimitive.Portal> | ||||||
| 			<DropdownMenuPrimitive.SubContent |       <DropdownMenuPrimitive.SubContent | ||||||
| 				class={cn( |         class={cn( | ||||||
| 					"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95)", |           'min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:(animate-in fade-in-0 zoom-in-95) data-[closed]:(animate-out fade-out-0 zoom-out-95)', | ||||||
| 					local.class, |           local.class, | ||||||
| 				)} |         )} | ||||||
| 				{...rest} |         {...rest} | ||||||
| 			/> |       /> | ||||||
| 		</DropdownMenuPrimitive.Portal> |     </DropdownMenuPrimitive.Portal> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type dropdownMenuCheckboxItemProps<T extends ValidComponent = "div"> = | type dropdownMenuCheckboxItemProps<T extends ValidComponent = 'div'> = ParentProps<DropdownMenuCheckboxItemProps<T> & { class?: string }>; | ||||||
| 	ParentProps< |  | ||||||
| 		DropdownMenuCheckboxItemProps<T> & { |  | ||||||
| 			class?: string; |  | ||||||
| 		} |  | ||||||
| 	>; |  | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">( | export function DropdownMenuCheckboxItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>, |   const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [ |     'children', | ||||||
| 		"class", |   ]); | ||||||
| 		"children", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<DropdownMenuPrimitive.CheckboxItem |     <DropdownMenuPrimitive.CheckboxItem | ||||||
| 			class={cn( |       class={cn( | ||||||
| 				"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)", |         'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)', | ||||||
| 				local.class, |         local.class, | ||||||
| 			)} |       )} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		> |     > | ||||||
| 			<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center"> |       <DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center"> | ||||||
| 				<svg |         <svg | ||||||
| 					xmlns="http://www.w3.org/2000/svg" |           xmlns="http://www.w3.org/2000/svg" | ||||||
| 					viewBox="0 0 24 24" |           viewBox="0 0 24 24" | ||||||
| 					class="h-4 w-4" |           class="h-4 w-4" | ||||||
| 				> |         > | ||||||
| 					<path |           <path | ||||||
| 						fill="none" |             fill="none" | ||||||
| 						stroke="currentColor" |             stroke="currentColor" | ||||||
| 						stroke-linecap="round" |             stroke-linecap="round" | ||||||
| 						stroke-linejoin="round" |             stroke-linejoin="round" | ||||||
| 						stroke-width="2" |             stroke-width="2" | ||||||
| 						d="m5 12l5 5L20 7" |             d="m5 12l5 5L20 7" | ||||||
| 					/> |           /> | ||||||
| 					<title>Checkbox</title> |           <title>Checkbox</title> | ||||||
| 				</svg> |         </svg> | ||||||
| 			</DropdownMenuPrimitive.ItemIndicator> |       </DropdownMenuPrimitive.ItemIndicator> | ||||||
| 			{props.children} |       {props.children} | ||||||
| 		</DropdownMenuPrimitive.CheckboxItem> |     </DropdownMenuPrimitive.CheckboxItem> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type dropdownMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps< | type dropdownMenuRadioItemProps<T extends ValidComponent = 'div'> = ParentProps< | ||||||
| 	DropdownMenuRadioItemProps<T> & { |   DropdownMenuRadioItemProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	} |   } | ||||||
| >; | >; | ||||||
| 
 | 
 | ||||||
| export const DropdownMenuRadioItem = <T extends ValidComponent = "div">( | export function DropdownMenuRadioItem<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>, |   const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [ |     'children', | ||||||
| 		"class", |   ]); | ||||||
| 		"children", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<DropdownMenuPrimitive.RadioItem |     <DropdownMenuPrimitive.RadioItem | ||||||
| 			class={cn( |       class={cn( | ||||||
| 				"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)", |         'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:(bg-accent text-accent-foreground) data-[disabled]:(pointer-events-none opacity-50)', | ||||||
| 				local.class, |         local.class, | ||||||
| 			)} |       )} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		> |     > | ||||||
| 			<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center"> |       <DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center"> | ||||||
| 				<svg |         <svg | ||||||
| 					xmlns="http://www.w3.org/2000/svg" |           xmlns="http://www.w3.org/2000/svg" | ||||||
| 					viewBox="0 0 24 24" |           viewBox="0 0 24 24" | ||||||
| 					class="h-2 w-2" |           class="h-2 w-2" | ||||||
| 				> |         > | ||||||
| 					<g |           <g | ||||||
| 						fill="none" |             fill="none" | ||||||
| 						stroke-linecap="round" |             stroke-linecap="round" | ||||||
| 						stroke-linejoin="round" |             stroke-linejoin="round" | ||||||
| 						stroke-width="2" |             stroke-width="2" | ||||||
| 					> |           > | ||||||
| 						<path d="M0 0h24v24H0z" /> |             <path d="M0 0h24v24H0z" /> | ||||||
| 						<path |             <path | ||||||
| 							fill="currentColor" |               fill="currentColor" | ||||||
| 							d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34" |               d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34" | ||||||
| 						/> |             /> | ||||||
| 					</g> |           </g> | ||||||
| 					<title>Radio</title> |           <title>Radio</title> | ||||||
| 				</svg> |         </svg> | ||||||
| 			</DropdownMenuPrimitive.ItemIndicator> |       </DropdownMenuPrimitive.ItemIndicator> | ||||||
| 			{props.children} |       {props.children} | ||||||
| 		</DropdownMenuPrimitive.RadioItem> |     </DropdownMenuPrimitive.RadioItem> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
|  | |||||||
							
								
								
									
										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,62 +1,54 @@ | |||||||
| import { cn } from "@/modules/ui/utils/cn"; | import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; |  | ||||||
| import type { | import type { | ||||||
| 	SwitchControlProps, |   SwitchControlProps, | ||||||
| 	SwitchThumbProps, |   SwitchThumbProps, | ||||||
| } from "@kobalte/core/switch"; | } from '@kobalte/core/switch'; | ||||||
| import { Switch as SwitchPrimitive } from "@kobalte/core/switch"; | import type { ParentProps, ValidComponent, VoidProps } from 'solid-js'; | ||||||
| import type { ParentProps, ValidComponent, VoidProps } from "solid-js"; | import { cn } from '@/modules/ui/utils/cn'; | ||||||
| import { splitProps } from "solid-js"; | import { Switch as SwitchPrimitive } from '@kobalte/core/switch'; | ||||||
|  | import { splitProps } from 'solid-js'; | ||||||
| 
 | 
 | ||||||
| export const SwitchLabel = SwitchPrimitive.Label; | export const SwitchLabel = SwitchPrimitive.Label; | ||||||
| export const Switch = SwitchPrimitive; | export const Switch = SwitchPrimitive; | ||||||
| export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage; | export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage; | ||||||
| export const SwitchDescription = SwitchPrimitive.Description; | export const SwitchDescription = SwitchPrimitive.Description; | ||||||
| 
 | 
 | ||||||
| type switchControlProps<T extends ValidComponent = "input"> = ParentProps< | type switchControlProps<T extends ValidComponent = 'input'> = ParentProps<SwitchControlProps<T> & { class?: string }>; | ||||||
| 	SwitchControlProps<T> & { class?: string } |  | ||||||
| >; |  | ||||||
| 
 | 
 | ||||||
| export const SwitchControl = <T extends ValidComponent = "input">( | export function SwitchControl<T extends ValidComponent = 'input'>(props: PolymorphicProps<T, switchControlProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, switchControlProps<T>>, |   const [local, rest] = splitProps(props as switchControlProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as switchControlProps, [ |     'children', | ||||||
| 		"class", |   ]); | ||||||
| 		"children", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<> |     <> | ||||||
| 			<SwitchPrimitive.Input class="[&:focus-visible+div]:(outline-none ring-1.5 ring-ring ring-offset-2 ring-offset-background)" /> |       <SwitchPrimitive.Input class="[&:focus-visible+div]:(outline-none ring-1.5 ring-ring ring-offset-2 ring-offset-background)" /> | ||||||
| 			<SwitchPrimitive.Control |       <SwitchPrimitive.Control | ||||||
| 				class={cn( |         class={cn( | ||||||
| 					"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input shadow-sm transition-shadow data-[disabled]:(cursor-not-allowed opacity-50) data-[checked]:bg-primary transition-property-[box-shadow,color,background-color]", |           'inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input shadow-sm transition-shadow data-[disabled]:(cursor-not-allowed opacity-50) data-[checked]:bg-primary transition-property-[box-shadow,color,background-color]', | ||||||
| 					local.class, |           local.class, | ||||||
| 				)} |         )} | ||||||
| 				{...rest} |         {...rest} | ||||||
| 			> |       > | ||||||
| 				{local.children} |         {local.children} | ||||||
| 			</SwitchPrimitive.Control> |       </SwitchPrimitive.Control> | ||||||
| 		</> |     </> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type switchThumbProps<T extends ValidComponent = "div"> = VoidProps< | type switchThumbProps<T extends ValidComponent = 'div'> = VoidProps<SwitchThumbProps<T> & { class?: string }>; | ||||||
| 	SwitchThumbProps<T> & { class?: string } |  | ||||||
| >; |  | ||||||
| 
 | 
 | ||||||
| export const SwitchThumb = <T extends ValidComponent = "div">( | export function SwitchThumb<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, switchThumbProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, switchThumbProps<T>>, |   const [local, rest] = splitProps(props as switchThumbProps, ['class']); | ||||||
| ) => { |  | ||||||
| 	const [local, rest] = splitProps(props as switchThumbProps, ["class"]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<SwitchPrimitive.Thumb |     <SwitchPrimitive.Thumb | ||||||
| 			class={cn( |       class={cn( | ||||||
| 				"pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg transition-transform data-[checked]:translate-x-4", |         'pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg transition-transform data-[checked]:translate-x-4', | ||||||
| 				local.class, |         local.class, | ||||||
| 			)} |       )} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
|  | |||||||
| @ -1,28 +1,26 @@ | |||||||
| import { cn } from "@/modules/ui/utils/cn"; | import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; | import type { TextFieldTextAreaProps } from '@kobalte/core/text-field'; | ||||||
| import type { TextFieldTextAreaProps } from "@kobalte/core/text-field"; | import type { ValidComponent, VoidProps } from 'solid-js'; | ||||||
| import { TextArea as TextFieldPrimitive } from "@kobalte/core/text-field"; | import { cn } from '@/modules/ui/utils/cn'; | ||||||
| import type { ValidComponent, VoidProps } from "solid-js"; | import { TextArea as TextFieldPrimitive } from '@kobalte/core/text-field'; | ||||||
| import { splitProps } from "solid-js"; | import { splitProps } from 'solid-js'; | ||||||
| 
 | 
 | ||||||
| type textAreaProps<T extends ValidComponent = "textarea"> = VoidProps< | type textAreaProps<T extends ValidComponent = 'textarea'> = VoidProps< | ||||||
| 	TextFieldTextAreaProps<T> & { |   TextFieldTextAreaProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	} |   } | ||||||
| >; | >; | ||||||
| 
 | 
 | ||||||
| export const TextArea = <T extends ValidComponent = "textarea">( | export function TextArea<T extends ValidComponent = 'textarea'>(props: PolymorphicProps<T, textAreaProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, textAreaProps<T>>, |   const [local, rest] = splitProps(props as textAreaProps, ['class']); | ||||||
| ) => { |  | ||||||
| 	const [local, rest] = splitProps(props as textAreaProps, ["class"]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<TextFieldPrimitive |     <TextFieldPrimitive | ||||||
| 			class={cn( |       class={cn( | ||||||
| 				"flex min-h-[60px] w-full rounded-md border border-input bg-inherit px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow", |         'flex min-h-[60px] w-full rounded-md border border-input bg-inherit px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow', | ||||||
| 				local.class, |         local.class, | ||||||
| 			)} |       )} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
|  | |||||||
| @ -1,126 +1,116 @@ | |||||||
| import { cn } from "@/modules/ui/utils/cn"; | import type { PolymorphicProps } from '@kobalte/core/polymorphic'; | ||||||
| import type { PolymorphicProps } from "@kobalte/core/polymorphic"; |  | ||||||
| import type { | import type { | ||||||
| 	TextFieldDescriptionProps, |   TextFieldDescriptionProps, | ||||||
| 	TextFieldErrorMessageProps, |   TextFieldErrorMessageProps, | ||||||
| 	TextFieldInputProps, |   TextFieldInputProps, | ||||||
| 	TextFieldLabelProps, |   TextFieldLabelProps, | ||||||
| 	TextFieldRootProps, |   TextFieldRootProps, | ||||||
| } from "@kobalte/core/text-field"; | } from '@kobalte/core/text-field'; | ||||||
| import { TextField as TextFieldPrimitive } from "@kobalte/core/text-field"; | import type { ValidComponent, VoidProps } from 'solid-js'; | ||||||
| import { cva } from "class-variance-authority"; | import { cn } from '@/modules/ui/utils/cn'; | ||||||
| import type { ValidComponent, VoidProps } from "solid-js"; | import { TextField as TextFieldPrimitive } from '@kobalte/core/text-field'; | ||||||
| import { splitProps } from "solid-js"; | import { cva } from 'class-variance-authority'; | ||||||
|  | import { splitProps } from 'solid-js'; | ||||||
| 
 | 
 | ||||||
| type textFieldProps<T extends ValidComponent = "div"> = | type textFieldProps<T extends ValidComponent = 'div'> = | ||||||
| 	TextFieldRootProps<T> & { |   TextFieldRootProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| export const TextFieldRoot = <T extends ValidComponent = "div">( | export function TextFieldRoot<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, textFieldProps<T>>, |   const [local, rest] = splitProps(props as textFieldProps, ['class']); | ||||||
| ) => { |  | ||||||
| 	const [local, rest] = splitProps(props as textFieldProps, ["class"]); |  | ||||||
| 
 | 
 | ||||||
| 	return <TextFieldPrimitive class={cn("space-y-1", local.class)} {...rest} />; |   return <TextFieldPrimitive class={cn('space-y-1', local.class)} {...rest} />; | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export const textfieldLabel = cva( | export const textfieldLabel = cva( | ||||||
| 	"text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium", |   'text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium', | ||||||
| 	{ |   { | ||||||
| 		variants: { |     variants: { | ||||||
| 			label: { |       label: { | ||||||
| 				true: "data-[invalid]:text-destructive", |         true: 'data-[invalid]:text-destructive', | ||||||
| 			}, |       }, | ||||||
| 			error: { |       error: { | ||||||
| 				true: "text-destructive text-xs", |         true: 'text-destructive text-xs', | ||||||
| 			}, |       }, | ||||||
| 			description: { |       description: { | ||||||
| 				true: "font-normal text-muted-foreground", |         true: 'font-normal text-muted-foreground', | ||||||
| 			}, |       }, | ||||||
| 		}, |     }, | ||||||
| 		defaultVariants: { |     defaultVariants: { | ||||||
| 			label: true, |       label: true, | ||||||
| 		}, |     }, | ||||||
| 	}, |   }, | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| type textFieldLabelProps<T extends ValidComponent = "label"> = | type textFieldLabelProps<T extends ValidComponent = 'label'> = | ||||||
| 	TextFieldLabelProps<T> & { |   TextFieldLabelProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| export const TextFieldLabel = <T extends ValidComponent = "label">( | export function TextFieldLabel<T extends ValidComponent = 'label'>(props: PolymorphicProps<T, textFieldLabelProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, textFieldLabelProps<T>>, |   const [local, rest] = splitProps(props as textFieldLabelProps, ['class']); | ||||||
| ) => { |  | ||||||
| 	const [local, rest] = splitProps(props as textFieldLabelProps, ["class"]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<TextFieldPrimitive.Label |     <TextFieldPrimitive.Label | ||||||
| 			class={cn(textfieldLabel(), local.class)} |       class={cn(textfieldLabel(), local.class)} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type textFieldErrorMessageProps<T extends ValidComponent = "div"> = | type textFieldErrorMessageProps<T extends ValidComponent = 'div'> = | ||||||
| 	TextFieldErrorMessageProps<T> & { |   TextFieldErrorMessageProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| export const TextFieldErrorMessage = <T extends ValidComponent = "div">( | export function TextFieldErrorMessage<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldErrorMessageProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, textFieldErrorMessageProps<T>>, |   const [local, rest] = splitProps(props as textFieldErrorMessageProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as textFieldErrorMessageProps, [ |   ]); | ||||||
| 		"class", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<TextFieldPrimitive.ErrorMessage |     <TextFieldPrimitive.ErrorMessage | ||||||
| 			class={cn(textfieldLabel({ error: true }), local.class)} |       class={cn(textfieldLabel({ error: true }), local.class)} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type textFieldDescriptionProps<T extends ValidComponent = "div"> = | type textFieldDescriptionProps<T extends ValidComponent = 'div'> = | ||||||
| 	TextFieldDescriptionProps<T> & { |   TextFieldDescriptionProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| export const TextFieldDescription = <T extends ValidComponent = "div">( | export function TextFieldDescription<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldDescriptionProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, textFieldDescriptionProps<T>>, |   const [local, rest] = splitProps(props as textFieldDescriptionProps, [ | ||||||
| ) => { |     'class', | ||||||
| 	const [local, rest] = splitProps(props as textFieldDescriptionProps, [ |   ]); | ||||||
| 		"class", |  | ||||||
| 	]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<TextFieldPrimitive.Description |     <TextFieldPrimitive.Description | ||||||
| 			class={cn(textfieldLabel({ description: true }), local.class)} |       class={cn(textfieldLabel({ description: true }), local.class)} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| type textFieldInputProps<T extends ValidComponent = "input"> = VoidProps< | type textFieldInputProps<T extends ValidComponent = 'input'> = VoidProps< | ||||||
| 	TextFieldInputProps<T> & { |   TextFieldInputProps<T> & { | ||||||
| 		class?: string; |     class?: string; | ||||||
| 	} |   } | ||||||
| >; | >; | ||||||
| 
 | 
 | ||||||
| export const TextField = <T extends ValidComponent = "input">( | export function TextField<T extends ValidComponent = 'input'>(props: PolymorphicProps<T, textFieldInputProps<T>>) { | ||||||
| 	props: PolymorphicProps<T, textFieldInputProps<T>>, |   const [local, rest] = splitProps(props as textFieldInputProps, ['class']); | ||||||
| ) => { |  | ||||||
| 	const [local, rest] = splitProps(props as textFieldInputProps, ["class"]); |  | ||||||
| 
 | 
 | ||||||
| 	return ( |   return ( | ||||||
| 		<TextFieldPrimitive.Input |     <TextFieldPrimitive.Input | ||||||
| 			class={cn( |       class={cn( | ||||||
| 				"flex h-9 w-full rounded-md border border-input bg-inherit px-3 py-1 text-sm shadow-sm file:(border-0 bg-transparent text-sm font-medium) placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow", |         'flex h-9 w-full rounded-md border border-input bg-inherit px-3 py-1 text-sm shadow-sm file:(border-0 bg-transparent text-sm font-medium) placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow', | ||||||
| 				local.class, |         local.class, | ||||||
| 			)} |       )} | ||||||
| 			{...rest} |       {...rest} | ||||||
| 		/> |     /> | ||||||
| 	); |   ); | ||||||
| }; | } | ||||||
|  | |||||||
| @ -1,11 +1,13 @@ | |||||||
| import type { LocaleKey } from '@/modules/i18n/i18n.types'; |  | ||||||
| import type { Component, ParentComponent } from 'solid-js'; | import type { Component, ParentComponent } from 'solid-js'; | ||||||
|  | import { useCommandPalette } from '@/modules/command-palette/command-palette.provider'; | ||||||
| import { useI18n } from '@/modules/i18n/i18n.provider'; | import { useI18n } from '@/modules/i18n/i18n.provider'; | ||||||
| import { Button } from '@/modules/ui/components/button'; | import { Button } from '@/modules/ui/components/button'; | ||||||
| import { A, useLocation, useNavigate } from '@solidjs/router'; | import { A } from '@solidjs/router'; | ||||||
|  | import { Badge } from '../components/badge'; | ||||||
| import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../components/dropdown-menu'; | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../components/dropdown-menu'; | ||||||
| import { useThemeStore } from '../themes/theme.store'; | import { useThemeStore } from '../themes/theme.store'; | ||||||
| import { cn } from '../utils/cn'; | import { cn } from '../utils/cn'; | ||||||
|  | import { socialLinks } from './app.layouts.constants'; | ||||||
| 
 | 
 | ||||||
| const ThemeSwitcher: Component = () => { | const ThemeSwitcher: Component = () => { | ||||||
|   const themeStore = useThemeStore(); |   const themeStore = useThemeStore(); | ||||||
| @ -30,17 +32,7 @@ const ThemeSwitcher: Component = () => { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const LanguageSwitcher: Component = () => { | const LanguageSwitcher: Component = () => { | ||||||
|   const { t, getLocale, setLocale, locales } = useI18n(); |   const { t, getLocale, changeLocale, locales } = useI18n(); | ||||||
|   const navigate = useNavigate(); |  | ||||||
|   const location = useLocation(); |  | ||||||
| 
 |  | ||||||
|   const changeLocale = (locale: LocaleKey) => { |  | ||||||
|     setLocale(locale); |  | ||||||
| 
 |  | ||||||
|     const pathWithoutLocale = location.pathname.split('/').slice(2).join('/'); |  | ||||||
|     const newPath = [locale, pathWithoutLocale].filter(Boolean).join('/'); |  | ||||||
|     navigate(`/${newPath}`); |  | ||||||
|   }; |  | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
| @ -62,14 +54,24 @@ const LanguageSwitcher: Component = () => { | |||||||
| export const Navbar: Component = () => { | export const Navbar: Component = () => { | ||||||
|   const themeStore = useThemeStore(); |   const themeStore = useThemeStore(); | ||||||
|   const { t } = useI18n(); |   const { t } = useI18n(); | ||||||
|  |   const { openCommandPalette } = useCommandPalette(); | ||||||
|  |   const getIsMacOs = () => navigator?.userAgent?.match(/Macintosh;/); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div class="border-b border-border bg-surface"> |     <div class="border-b border-border bg-surface"> | ||||||
|       <div class="flex items-center justify-between px-6 py-3 mx-auto max-w-1200px"> |       <div class="flex items-center justify-between px-6 py-3 mx-auto max-w-1200px"> | ||||||
|         <div class="flex items-baseline gap-4"> |         <div class="flex items-center gap-4"> | ||||||
|           <Button variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250" as={A} href="/" aria-label="Home"> |           <Button variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250" as={A} href="/" aria-label="Home"> | ||||||
|             <span class="font-bold text-foreground">IT</span> |             <span class="font-bold text-foreground">IT</span> | ||||||
|             <span class="text-80% font-extrabold border border-2px leading-none border-current rounded-md px-1 py-0.5 ml-1 text-lime">TOOLS</span> |             <span class="text-80% font-extrabold border border-2px leading-none border-current rounded-md px-1 py-0.5 ml-1 text-primary">TOOLS</span> | ||||||
|  |           </Button> | ||||||
|  | 
 | ||||||
|  |           <Button size="sm" variant="outline" class="bg-card transition flex items-center gap-2 text-muted-foreground" onClick={openCommandPalette}> | ||||||
|  |             <div class="i-tabler-search text-base"></div> | ||||||
|  |             {t('commandPalette.trigger.search')} | ||||||
|  |             <Badge variant="secondary" class="text-muted-foreground text-10px!"> | ||||||
|  |               {getIsMacOs() ? '⌘ + K' : 'Ctrl + K'} | ||||||
|  |             </Badge> | ||||||
|           </Button> |           </Button> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
| @ -155,6 +157,114 @@ export const Navbar: Component = () => { | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const Footer: Component = () => { | ||||||
|  |   const { t, createLocalizedUrl } = useI18n(); | ||||||
|  | 
 | ||||||
|  |   const getFooterSections = () => [ | ||||||
|  |     { | ||||||
|  |       title: t('footer.resources.title'), | ||||||
|  |       items: [ | ||||||
|  |         { label: t('footer.resources.all-tools'), to: createLocalizedUrl({ path: '/tools' }) }, | ||||||
|  |         { label: t('footer.resources.github'), href: 'https://github.com/CorentinTh/it-tools' }, | ||||||
|  |         { label: t('footer.resources.support'), href: 'https://buymeacoffee.com/cthmsst' }, | ||||||
|  |         { label: 'Humans.txt', href: '/humans.txt' }, | ||||||
|  |         { label: t('footer.resources.license'), href: 'https://github.com/CorentinTh/it-tools/blob/main/LICENSE' }, | ||||||
|  |       ], | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: t('footer.support.title'), | ||||||
|  |       items: [ | ||||||
|  |         { label: t('footer.support.report-bug'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' }, | ||||||
|  |         { label: t('footer.support.request-feature'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' }, | ||||||
|  |         { label: t('footer.support.contribute'), href: 'https://github.com/CorentinTh/it-tools/blob/main/CONTRIBUTING.md' }, | ||||||
|  |         { label: t('footer.support.contact'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' }, | ||||||
|  |       ], | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: t('footer.friends.title'), | ||||||
|  |       items: [ | ||||||
|  |         { label: 'Jugly.io', href: 'https://jugly.io' }, | ||||||
|  |         { label: 'Enclosed.cc', href: 'https://enclosed.cc' }, | ||||||
|  |       ], | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |   ]; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <footer class="bg-card border-t border-border"> | ||||||
|  |       <div class="py-12 px-6 mx-auto max-w-1200px"> | ||||||
|  | 
 | ||||||
|  |         <div class="flex items-start justify-between flex-col md:flex-row gap-12"> | ||||||
|  |           <div> | ||||||
|  |             <div class="flex items-center gap-2"> | ||||||
|  |               <A href="/" class="text-2xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250 group text-muted-foreground flex items-center gap-1"> | ||||||
|  |                 <span class="font-bold group-hover:text-foreground transition">IT</span> | ||||||
|  |                 <span class="text-80% font-extrabold border border-2px leading-none border-current rounded-md px-1 py-0.5 ml-1 group-hover:text-primary transition">TOOLS</span> | ||||||
|  |               </A> | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <div class="flex items-center gap-2 mt-4"> | ||||||
|  |               {socialLinks.map(({ icon, href, label }) => ( | ||||||
|  |                 <a href={href} target="_blank" rel="noopener noreferrer" class="text-2xl text-muted-foreground hover:text-primary transition" aria-label={label}> | ||||||
|  |                   <div class={icon}></div> | ||||||
|  |                 </a> | ||||||
|  |               ))} | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <div class="text-muted-foreground mt-2"> | ||||||
|  |               Crafted with | ||||||
|  |               {' '} | ||||||
|  |               <span class="i-tabler-heart inline-block text-base mb--0.5"></span> | ||||||
|  |               {' '} | ||||||
|  |               by | ||||||
|  |               {' '} | ||||||
|  |               <a href="https://corentin.tech" target="_blank" rel="noopener" class="hover:text-primary transition"> | ||||||
|  |                 Corentin Thomasset | ||||||
|  |               </a> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <div class="grid grid-cols-1 sm:grid-cols-3 gap-12"> | ||||||
|  |             {getFooterSections().map(({ title, items }) => ( | ||||||
|  |               <div> | ||||||
|  |                 <h4 class="font-semibold text-foreground">{title}</h4> | ||||||
|  |                 <ul class="mt-4"> | ||||||
|  |                   {items.map(({ label, to, href }) => ( | ||||||
|  |                     <li class="mt-1"> | ||||||
|  |                       {to | ||||||
|  |                         ? ( | ||||||
|  |                             <A href={to} class="text-muted-foreground hover:text-primary transition"> | ||||||
|  |                               {label} | ||||||
|  |                             </A> | ||||||
|  |                           ) | ||||||
|  |                         : ( | ||||||
|  |                             <a href={href} target="_blank" rel="noopener" class="text-muted-foreground hover:text-primary transition"> | ||||||
|  |                               {label} | ||||||
|  |                             </a> | ||||||
|  |                           )} | ||||||
|  |                     </li> | ||||||
|  |                   ))} | ||||||
|  |                 </ul> | ||||||
|  |               </div> | ||||||
|  |             ))} | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <div class="text-xs text-muted-foreground border-t border-border pt-4 mt-12"> | ||||||
|  |           <span> | ||||||
|  |             © | ||||||
|  |             {new Date().getFullYear()} | ||||||
|  |             {' '} | ||||||
|  |             Corentin Thomasset | ||||||
|  |           </span> | ||||||
|  |         </div> | ||||||
|  |         <div class="text-xs text-foreground opacity-80%"> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </footer> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export const AppLayout: ParentComponent = (props) => { | export const AppLayout: ParentComponent = (props) => { | ||||||
|   return ( |   return ( | ||||||
|     <div class="flex flex-col h-screen min-h-0"> |     <div class="flex flex-col h-screen min-h-0"> | ||||||
| @ -163,6 +273,7 @@ export const AppLayout: ParentComponent = (props) => { | |||||||
| 
 | 
 | ||||||
|       <div class="flex-1 pb-20 ">{props.children}</div> |       <div class="flex-1 pb-20 ">{props.children}</div> | ||||||
| 
 | 
 | ||||||
|  |       <Footer /> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										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'; | } from 'unocss'; | ||||||
| import presetAnimations from 'unocss-preset-animations'; | import presetAnimations from 'unocss-preset-animations'; | ||||||
| import { toolDefinitions } from './src/modules/tools/tools.registry'; | import { toolDefinitions } from './src/modules/tools/tools.registry'; | ||||||
|  | import { socialLinks } from './src/modules/ui/layouts/app.layouts.constants'; | ||||||
| 
 | 
 | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
|   presets: [ |   presets: [ | ||||||
| @ -111,5 +112,9 @@ export default defineConfig({ | |||||||
|   }, |   }, | ||||||
|   safelist: [ |   safelist: [ | ||||||
|     ...toolDefinitions.map(tool => tool.icon), |     ...toolDefinitions.map(tool => tool.icon), | ||||||
|  |     ...socialLinks.map(({ icon }) => icon), | ||||||
|   ], |   ], | ||||||
|  |   shortcuts: { | ||||||
|  |     'i-logo': 'i-tabler-terminal', | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
|  | |||||||
							
								
								
									
										1596
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1596
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user