chore(cd): added deploy on cloudflare pages

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

View file

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

View file

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

View file

@ -1,7 +1,9 @@
import type { ParentComponent } from 'solid-js';
import type { LocaleKey } from './i18n.types';
import { joinUrlPaths } from '@corentinth/chisels';
import * as i18n from '@solid-primitives/i18n';
import { makePersisted } from '@solid-primitives/storage';
import { useNavigate } from '@solidjs/router';
import { merge } from 'lodash-es';
import { createContext, createResource, createSignal, Show, useContext } from 'solid-js';
import defaultDict from '../../locales/en.json';
@ -23,6 +25,7 @@ const RootI18nContext = createContext<{
function useI18n() {
const context = useContext(RootI18nContext);
const navigate = useNavigate();
if (!context) {
throw new Error('I18n context not found');
@ -35,6 +38,18 @@ function useI18n() {
getLocale,
setLocale,
locales,
createLocalizedUrl: ({ path }: { path: string }) => {
const newPath = joinUrlPaths(getLocale(), path);
return `/${newPath}`;
},
changeLocale: (locale: LocaleKey) => {
setLocale(locale);
const pathWithoutLocale = location.pathname.split('/').slice(2).join('/');
const newPath = joinUrlPaths(locale, pathWithoutLocale);
navigate(`/${newPath}`);
},
};
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,31 @@
import type { Component } from 'solid-js';
import { CopyButton } from '@/modules/shared/copy/copy-button';
import { createRefreshableSignal } from '@/modules/shared/signals';
import { Button } from '@/modules/ui/components/button';
import { useCurrentTool } from '../../tools.provider';
import defaultDictionary from './locales/en.json';
import { generateRandomPort } from './random-port-generator.services';
const RandomPortGenerator: Component = () => {
const [getPort, refreshPort] = createRefreshableSignal(generateRandomPort);
const { t } = useCurrentTool({ defaultDictionary });
return (
<div class="mx-auto max-w-1200px p-6">
<div>
{getPort()}
</div>
<div class="flex gap-4 mt-4">
<Button onClick={refreshPort} variant="outline">
<div class="i-tabler-refresh mr-2 text-base text-muted-foreground" />
{t('refresh')}
</Button>
<CopyButton textToCopy={getPort} toastMessage={t('copy-toast')} />
</div>
</div>
);
};
export default RandomPortGenerator;

View file

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

View file

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

View file

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

View file

@ -1,4 +1,6 @@
{
"name": "Générateur de token",
"description": "Génère une chaîne de caractères aléatoire, contrôlez les caractères que vous voulez, lettres majuscules, minuscules, chiffres et/ou symboles.",
"uppercase": "Lettres majuscules (A-Z)",
"lowercase": "Lettres minuscules (a-z)",
"numbers": "Chiffres (0-9)",

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,10 @@
import { keyBy, map } from 'lodash-es';
import { randomPortGeneratorTool } from './definitions/random-port-generator/random-port-generator.tool';
import { tokenGeneratorTool } from './definitions/token-generator/token-generator.tool';
export const toolDefinitions = [
tokenGeneratorTool,
randomPortGeneratorTool,
];
export const toolSlugs = map(toolDefinitions, 'slug');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,62 +1,54 @@
import { cn } from "@/modules/ui/utils/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
import type {
SwitchControlProps,
SwitchThumbProps,
} from "@kobalte/core/switch";
import { Switch as SwitchPrimitive } from "@kobalte/core/switch";
import type { ParentProps, ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
SwitchControlProps,
SwitchThumbProps,
} from '@kobalte/core/switch';
import type { ParentProps, ValidComponent, VoidProps } from 'solid-js';
import { cn } from '@/modules/ui/utils/cn';
import { Switch as SwitchPrimitive } from '@kobalte/core/switch';
import { splitProps } from 'solid-js';
export const SwitchLabel = SwitchPrimitive.Label;
export const Switch = SwitchPrimitive;
export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage;
export const SwitchDescription = SwitchPrimitive.Description;
type switchControlProps<T extends ValidComponent = "input"> = ParentProps<
SwitchControlProps<T> & { class?: string }
>;
type switchControlProps<T extends ValidComponent = 'input'> = ParentProps<SwitchControlProps<T> & { class?: string }>;
export const SwitchControl = <T extends ValidComponent = "input">(
props: PolymorphicProps<T, switchControlProps<T>>,
) => {
const [local, rest] = splitProps(props as switchControlProps, [
"class",
"children",
]);
export function SwitchControl<T extends ValidComponent = 'input'>(props: PolymorphicProps<T, switchControlProps<T>>) {
const [local, rest] = splitProps(props as switchControlProps, [
'class',
'children',
]);
return (
<>
<SwitchPrimitive.Input class="[&:focus-visible+div]:(outline-none ring-1.5 ring-ring ring-offset-2 ring-offset-background)" />
<SwitchPrimitive.Control
class={cn(
"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input shadow-sm transition-shadow data-[disabled]:(cursor-not-allowed opacity-50) data-[checked]:bg-primary transition-property-[box-shadow,color,background-color]",
local.class,
)}
{...rest}
>
{local.children}
</SwitchPrimitive.Control>
</>
);
};
return (
<>
<SwitchPrimitive.Input class="[&:focus-visible+div]:(outline-none ring-1.5 ring-ring ring-offset-2 ring-offset-background)" />
<SwitchPrimitive.Control
class={cn(
'inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input shadow-sm transition-shadow data-[disabled]:(cursor-not-allowed opacity-50) data-[checked]:bg-primary transition-property-[box-shadow,color,background-color]',
local.class,
)}
{...rest}
>
{local.children}
</SwitchPrimitive.Control>
</>
);
}
type switchThumbProps<T extends ValidComponent = "div"> = VoidProps<
SwitchThumbProps<T> & { class?: string }
>;
type switchThumbProps<T extends ValidComponent = 'div'> = VoidProps<SwitchThumbProps<T> & { class?: string }>;
export const SwitchThumb = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, switchThumbProps<T>>,
) => {
const [local, rest] = splitProps(props as switchThumbProps, ["class"]);
export function SwitchThumb<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, switchThumbProps<T>>) {
const [local, rest] = splitProps(props as switchThumbProps, ['class']);
return (
<SwitchPrimitive.Thumb
class={cn(
"pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg transition-transform data-[checked]:translate-x-4",
local.class,
)}
{...rest}
/>
);
};
return (
<SwitchPrimitive.Thumb
class={cn(
'pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg transition-transform data-[checked]:translate-x-4',
local.class,
)}
{...rest}
/>
);
}

View file

@ -1,28 +1,26 @@
import { cn } from "@/modules/ui/utils/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { TextFieldTextAreaProps } from "@kobalte/core/text-field";
import { TextArea as TextFieldPrimitive } from "@kobalte/core/text-field";
import type { ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
import type { TextFieldTextAreaProps } from '@kobalte/core/text-field';
import type { ValidComponent, VoidProps } from 'solid-js';
import { cn } from '@/modules/ui/utils/cn';
import { TextArea as TextFieldPrimitive } from '@kobalte/core/text-field';
import { splitProps } from 'solid-js';
type textAreaProps<T extends ValidComponent = "textarea"> = VoidProps<
TextFieldTextAreaProps<T> & {
class?: string;
}
type textAreaProps<T extends ValidComponent = 'textarea'> = VoidProps<
TextFieldTextAreaProps<T> & {
class?: string;
}
>;
export const TextArea = <T extends ValidComponent = "textarea">(
props: PolymorphicProps<T, textAreaProps<T>>,
) => {
const [local, rest] = splitProps(props as textAreaProps, ["class"]);
export function TextArea<T extends ValidComponent = 'textarea'>(props: PolymorphicProps<T, textAreaProps<T>>) {
const [local, rest] = splitProps(props as textAreaProps, ['class']);
return (
<TextFieldPrimitive
class={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-inherit px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow",
local.class,
)}
{...rest}
/>
);
};
return (
<TextFieldPrimitive
class={cn(
'flex min-h-[60px] w-full rounded-md border border-input bg-inherit px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow',
local.class,
)}
{...rest}
/>
);
}

View file

@ -1,126 +1,116 @@
import { cn } from "@/modules/ui/utils/cn";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
import type { PolymorphicProps } from '@kobalte/core/polymorphic';
import type {
TextFieldDescriptionProps,
TextFieldErrorMessageProps,
TextFieldInputProps,
TextFieldLabelProps,
TextFieldRootProps,
} from "@kobalte/core/text-field";
import { TextField as TextFieldPrimitive } from "@kobalte/core/text-field";
import { cva } from "class-variance-authority";
import type { ValidComponent, VoidProps } from "solid-js";
import { splitProps } from "solid-js";
TextFieldDescriptionProps,
TextFieldErrorMessageProps,
TextFieldInputProps,
TextFieldLabelProps,
TextFieldRootProps,
} from '@kobalte/core/text-field';
import type { ValidComponent, VoidProps } from 'solid-js';
import { cn } from '@/modules/ui/utils/cn';
import { TextField as TextFieldPrimitive } from '@kobalte/core/text-field';
import { cva } from 'class-variance-authority';
import { splitProps } from 'solid-js';
type textFieldProps<T extends ValidComponent = "div"> =
TextFieldRootProps<T> & {
class?: string;
};
type textFieldProps<T extends ValidComponent = 'div'> =
TextFieldRootProps<T> & {
class?: string;
};
export const TextFieldRoot = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, textFieldProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldProps, ["class"]);
export function TextFieldRoot<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldProps<T>>) {
const [local, rest] = splitProps(props as textFieldProps, ['class']);
return <TextFieldPrimitive class={cn("space-y-1", local.class)} {...rest} />;
};
return <TextFieldPrimitive class={cn('space-y-1', local.class)} {...rest} />;
}
export const textfieldLabel = cva(
"text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium",
{
variants: {
label: {
true: "data-[invalid]:text-destructive",
},
error: {
true: "text-destructive text-xs",
},
description: {
true: "font-normal text-muted-foreground",
},
},
defaultVariants: {
label: true,
},
},
'text-sm data-[disabled]:(cursor-not-allowed opacity-70) font-medium',
{
variants: {
label: {
true: 'data-[invalid]:text-destructive',
},
error: {
true: 'text-destructive text-xs',
},
description: {
true: 'font-normal text-muted-foreground',
},
},
defaultVariants: {
label: true,
},
},
);
type textFieldLabelProps<T extends ValidComponent = "label"> =
TextFieldLabelProps<T> & {
class?: string;
};
type textFieldLabelProps<T extends ValidComponent = 'label'> =
TextFieldLabelProps<T> & {
class?: string;
};
export const TextFieldLabel = <T extends ValidComponent = "label">(
props: PolymorphicProps<T, textFieldLabelProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldLabelProps, ["class"]);
export function TextFieldLabel<T extends ValidComponent = 'label'>(props: PolymorphicProps<T, textFieldLabelProps<T>>) {
const [local, rest] = splitProps(props as textFieldLabelProps, ['class']);
return (
<TextFieldPrimitive.Label
class={cn(textfieldLabel(), local.class)}
{...rest}
/>
);
};
return (
<TextFieldPrimitive.Label
class={cn(textfieldLabel(), local.class)}
{...rest}
/>
);
}
type textFieldErrorMessageProps<T extends ValidComponent = "div"> =
TextFieldErrorMessageProps<T> & {
class?: string;
};
type textFieldErrorMessageProps<T extends ValidComponent = 'div'> =
TextFieldErrorMessageProps<T> & {
class?: string;
};
export const TextFieldErrorMessage = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, textFieldErrorMessageProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldErrorMessageProps, [
"class",
]);
export function TextFieldErrorMessage<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldErrorMessageProps<T>>) {
const [local, rest] = splitProps(props as textFieldErrorMessageProps, [
'class',
]);
return (
<TextFieldPrimitive.ErrorMessage
class={cn(textfieldLabel({ error: true }), local.class)}
{...rest}
/>
);
};
return (
<TextFieldPrimitive.ErrorMessage
class={cn(textfieldLabel({ error: true }), local.class)}
{...rest}
/>
);
}
type textFieldDescriptionProps<T extends ValidComponent = "div"> =
TextFieldDescriptionProps<T> & {
class?: string;
};
type textFieldDescriptionProps<T extends ValidComponent = 'div'> =
TextFieldDescriptionProps<T> & {
class?: string;
};
export const TextFieldDescription = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, textFieldDescriptionProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldDescriptionProps, [
"class",
]);
export function TextFieldDescription<T extends ValidComponent = 'div'>(props: PolymorphicProps<T, textFieldDescriptionProps<T>>) {
const [local, rest] = splitProps(props as textFieldDescriptionProps, [
'class',
]);
return (
<TextFieldPrimitive.Description
class={cn(textfieldLabel({ description: true }), local.class)}
{...rest}
/>
);
};
return (
<TextFieldPrimitive.Description
class={cn(textfieldLabel({ description: true }), local.class)}
{...rest}
/>
);
}
type textFieldInputProps<T extends ValidComponent = "input"> = VoidProps<
TextFieldInputProps<T> & {
class?: string;
}
type textFieldInputProps<T extends ValidComponent = 'input'> = VoidProps<
TextFieldInputProps<T> & {
class?: string;
}
>;
export const TextField = <T extends ValidComponent = "input">(
props: PolymorphicProps<T, textFieldInputProps<T>>,
) => {
const [local, rest] = splitProps(props as textFieldInputProps, ["class"]);
export function TextField<T extends ValidComponent = 'input'>(props: PolymorphicProps<T, textFieldInputProps<T>>) {
const [local, rest] = splitProps(props as textFieldInputProps, ['class']);
return (
<TextFieldPrimitive.Input
class={cn(
"flex h-9 w-full rounded-md border border-input bg-inherit px-3 py-1 text-sm shadow-sm file:(border-0 bg-transparent text-sm font-medium) placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow",
local.class,
)}
{...rest}
/>
);
};
return (
<TextFieldPrimitive.Input
class={cn(
'flex h-9 w-full rounded-md border border-input bg-inherit px-3 py-1 text-sm shadow-sm file:(border-0 bg-transparent text-sm font-medium) placeholder:text-muted-foreground focus-visible:(outline-none ring-1.5 ring-ring) disabled:(cursor-not-allowed opacity-50) transition-shadow',
local.class,
)}
{...rest}
/>
);
}

View file

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

View file

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