feat(pages): home page base

This commit is contained in:
Corentin Thomasset 2024-10-24 21:05:54 +02:00
parent 87cfc9c1f3
commit 202896fa95
No known key found for this signature in database
GPG key ID: DBD997E935996158
38 changed files with 3130 additions and 775 deletions

View file

@ -1,6 +1,5 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

View file

@ -0,0 +1,77 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border:240 5.9% 90%;
--input:240 5.9% 90%;
--ring:240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background:0 0% 9%;
--foreground:0 0% 98%;
--card: 0 0% 7%;
--card-foreground:0 0% 98%;
--popover:240 10% 3.9%;
--popover-foreground:0 0% 98%;
--primary: 83 79% 55%;
--primary-foreground:240 5.9% 10%;
--secondary:240 3.7% 15.9%;
--secondary-foreground:0 0% 98%;
--muted:240 3.7% 15.9%;
--muted-foreground:240 5% 64.9%;
--accent:240 3.7% 15.9%;
--accent-foreground:0 0% 98%;
--destructive:0 62.8% 30.6%;
--destructive-foreground:0 0% 98%;
--border:240 3.7% 15.9%;
--input:240 3.7% 15.9%;
--ring:240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -0,0 +1,19 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tsConfigPath": ".nuxt/tsconfig.json",
"tailwind": {
"config": "tailwind.config.js",
"css": "assets/css/tailwind.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"framework": "nuxt",
"aliases": {
"components": "@/src/modules/ui/components",
"ui": "@/src/modules/ui/components",
"utils": "@/src/modules/shared/style/cn"
}
}

View file

@ -0,0 +1,12 @@
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en',
messages: {
en: {
welcome: 'Welcome',
},
fr: {
welcome: 'Bienvenue',
},
},
}));

View file

@ -2,4 +2,40 @@
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
devtools: { enabled: true },
extends: [
'src/modules/app',
],
modules: [
'@nuxtjs/tailwindcss',
'shadcn-nuxt',
'@nuxt/fonts',
'@nuxt/icon',
'@vueuse/nuxt',
'@nuxtjs/color-mode',
'@nuxtjs/i18n',
],
fonts: {
provider: 'bunny',
defaults: {
weights: [400, 500, 600, 700, 800],
},
},
colorMode: {
preference: 'system',
fallback: 'dark',
classSuffix: '',
storage: 'cookie',
storageKey: 'itts-color-mode',
},
i18n: {
strategy: 'prefix',
vueI18n: './i18n.config.ts',
locales: ['en', 'fr'],
defaultLocale: 'en',
},
});

View file

@ -2,6 +2,8 @@
"name": "@it-tools/app",
"type": "module",
"private": true,
"packageManager": "pnpm@9.12.2",
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
@ -17,13 +19,28 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@nuxt/fonts": "^0.10.2",
"@nuxt/icon": "^1.5.6",
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/i18n": "^8.5.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.453.0",
"nuxt": "^3.13.2",
"radix-vue": "^1.9.7",
"shadcn-nuxt": "^0.10.4",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"vue": "latest",
"vue-router": "latest"
},
"devDependencies": {
"@antfu/eslint-config": "^3.8.0",
"@nuxtjs/tailwindcss": "^6.12.2",
"@vueuse/core": "^11.1.0",
"@vueuse/nuxt": "^11.1.0",
"eslint": "^9.13.0",
"typescript": "^5.6.3",
"vitest": "^2.1.3"
}
}

View file

@ -0,0 +1,113 @@
<script setup>
const localePath = useLocalePath();
const sections = computed(() => [
{
title: 'Lorem',
items: [
{ label: 'Foo', to: '/foo' },
{ label: 'Bar', to: '/bar' },
{ label: 'Baz', to: '/baz' },
],
},
{
title: 'Ipsum',
items: [
{ label: 'Foo', to: '/foo' },
{ label: 'Bar', to: '/bar' },
{ label: 'Baz', to: '/baz' },
],
},
{
title: 'Dolor',
items: [
{ label: 'Foo', to: '/foo' },
{ label: 'Bar', to: '/bar' },
{ label: 'Baz', to: '/baz' },
],
},
]);
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',
},
];
</script>
<template>
<footer class="bg-card border-t border-border">
<div class="py-12 px-6 max-w-screen-xl mx-auto ">
<div class="flex items-start justify-between flex-col md:flex-row gap-12">
<div>
<div class="flex items-center gap-2">
<NuxtLink :to="localePath('/')" 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-[2px] leading-none border-current rounded-md px-1 pt-0.5 ml-1 group-hover:text-primary transition">TOOLS</span>
</NuxtLink>
</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}" />
</a>
))} -->
<a
v-for="socialLink in socialLinks" :key="socialLink.label" :href="socialLink.href" target="_blank" rel="noopener noreferrer" class="text-2xl text-muted-foreground hover:text-primary transition" :aria-label="socialLink.label"
>
<Icon :name="socialLink.icon" />
</a>
</div>
<div class="text-muted-foreground mt-2">
Crafted on Earth 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">
<div v-for="section in sections" :key="section.title">
<h4 class="font-semibold text-foreground">
{{ section.title }}
</h4>
<ul class="mt-4">
<li v-for="item in section.items" :key="item.label" class="mt-1">
<NuxtLink v-if="item.to" :to="localePath(item.to)" class="text-muted-foreground hover:text-primary transition">
{{ item.label }}
</NuxtLink>
<a v-else :href="item.href" target="_blank" rel="noopener" class="text-muted-foreground hover:text-primary transition">
{{ item.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>
</footer>
</template>

View file

@ -0,0 +1,41 @@
<script setup>
import { Button } from '@/src/modules/ui/components/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/src/modules/ui/components/dropdown-menu';
const colorMode = useColorMode();
</script>
<template>
<div class="w-full border-b">
<div class="max-w-screen-xl mx-auto flex items-center justify-between py-2 px-6">
<NuxtLink variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto px-1 rounded-none !transition-border-color-250" :as="Button" :to="localePath('/')" aria-label="Home">
<span class="font-bold text-foreground">IT</span>
<span class="text-[80%] font-extrabold border-[2px] leading-none border-current rounded-md px-1 py-0.5 ml-1.5 text-primary">TOOLS</span>
</NuxtLink>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon">
<Icon name="i-tabler-moon" class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Icon name="i-tabler-sun" class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem class="cursor-pointer" :class="{ 'font-bold': colorMode.preference === 'light' }" @click="colorMode.preference = 'light'">
<Icon name="i-tabler-sun" class="mr-2 size-4" />
Light
</DropdownMenuItem>
<DropdownMenuItem class="cursor-pointer" :class="{ 'font-bold': colorMode.preference === 'dark' }" @click="colorMode.preference = 'dark'">
<Icon name="i-tabler-moon" class="mr-2 size-4" />
Dark
</DropdownMenuItem>
<DropdownMenuItem class="cursor-pointer" :class="{ 'font-bold': colorMode.preference === 'system' }" @click="colorMode.preference = 'system'">
<Icon name="i-tabler-device-laptop" class="mr-2 size-4" />
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</template>

View file

@ -0,0 +1,11 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{ fadeBottom?: boolean; faderClass?: string }>(), { fadeBottom: true });
</script>
<template>
<div class="w-full bg-[linear-gradient(to_right,#80808010_1px,transparent_1px),linear-gradient(to_bottom,#80808010_1px,transparent_1px)] bg-[size:48px_48px] pt-20">
<slot />
<div v-if="props.fadeBottom" class="bg-gradient-to-t from-background to-transparent h-24 mt-24" :class="props.faderClass" />
</div>
</template>

View file

@ -0,0 +1,11 @@
<template>
<div class="w-full min-h-screen text-sm relative font-sans flex flex-col">
<app-header />
<div class="flex-1 pb-6">
<slot />
</div>
<app-footer />
</div>
</template>

View file

@ -0,0 +1,34 @@
<script setup lang="ts">
import { Button } from '@/src/modules/ui/components/button';
const localePath = useLocalePath();
</script>
<template>
<div class="flex justify-center text-center">
<div>
<h1 class="text-3xl font-light text-muted-foreground">
404
</h1>
<h2 class="font-semibold text-lg my-2">
Page not found
</h2>
<p class="text-muted-foreground">
The page you are looking for does not seem to exist.
</p>
<p class="text-muted-foreground">
Please check the URL and try again.
</p>
<Button as-child variant="secondary" class="mt-4">
<NuxtLink :to="localePath('/')">
<Icon name="i-tabler-arrow-left" class="mr-2 size-4" />
Go back home
</NuxtLink>
</Button>
</div>
</div>
</template>

View file

@ -0,0 +1,57 @@
<script setup>
import { Badge } from '@/src/modules/ui/components/badge';
import { Button } from '@/src/modules/ui/components/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/src/modules/ui/components/dropdown-menu';
</script>
<template>
<grid-background>
<div class="flex gap-24 mx-auto justify-center pb-8 mt-8 items-center px-6">
<div class="max-w-xl">
<div class="flex gap-2">
<Badge class="text-primary bg-primary/10 hover:bg-primary/10">
<!-- {{ $t('landing.hero.badges.open-source') }} -->
Open Source
</Badge>
<Badge class="text-primary bg-primary/10 hover:bg-primary/10">
<!-- {{ $t('landing.hero.badges.free') }} -->
Free
</Badge>
<Badge class="text-primary bg-primary/10 hover:bg-primary/10">
<!-- {{ $t('landing.hero.badges.self-hostable') }} -->
Self-hostable
</Badge>
</div>
<h1 class="text-5xl font-semibold border-b border-transparent hover:no-underline h-auto py-0 px-1 ml--1 rounded-none !transition-border-color-250 my-6">
<span class="font-bold ">IT</span>
<span class="text-[90%] text-primary font-extrabold border-[5px] leading-none border-current rounded-xl px-2 py-0.5 ml-3">TOOLS</span>
</h1>
<p class="text-xl text-gray-400 mb-4">
<!-- {{ $t('app.description') }} -->
The open-source collection of handy online tools to help developers in their daily life.
</p>
<div class="flex gap-4">
<Button>
<!-- {{ $t('landing.hero.all-the-tools') }} -->
All the tools
<Icon name="i-tabler-arrow-right" class="ml-2 size-4" />
</Button>
<Button variant="outline">
<!-- {{ $t('landing.hero.search-tools') }} -->
<Icon name="i-tabler-search" class="mr-2 size-4" />
Search tools
</Button>
</div>
</div>
<div class="relative hidden sm:block">
<div class="absolute top-4 left-0 w-full h-full flex items-center justify-center blur-2xl rounded-full opacity-20 bg-gradient-to-br from-primary to-transparent" />
<Icon name="i-tabler-terminal" class="text-9xl text-primary m-8" />
</div>
</div>
</grid-background>
</template>

View file

@ -0,0 +1,4 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));

View file

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/src/modules/shared/style/cn'
import { type BadgeVariants, badgeVariants } from '.'
const props = defineProps<{
variant?: BadgeVariants['variant']
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot />
</div>
</template>

View file

@ -0,0 +1,25 @@
import { cva, type VariantProps } from 'class-variance-authority'
export { default as Badge } from './Badge.vue'
export const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View file

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { cn } from '@/src/modules/shared/style/cn';
import { Primitive, type PrimitiveProps } from 'radix-vue';
import { type ButtonVariants, buttonVariants } from '.';
type Props = {
variant?: ButtonVariants['variant'];
size?: ButtonVariants['size'];
class?: HTMLAttributes['class'];
} & PrimitiveProps;
const props = withDefaults(defineProps<Props>(), {
as: 'button',
});
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View file

@ -0,0 +1,32 @@
import { cva, type VariantProps } from 'class-variance-authority';
export { default as Button } from './Button.vue';
export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import { DropdownMenuRoot, type DropdownMenuRootEmits, type DropdownMenuRootProps, useForwardPropsEmits } from 'radix-vue'
const props = defineProps<DropdownMenuRootProps>()
const emits = defineEmits<DropdownMenuRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRoot v-bind="forwarded">
<slot />
</DropdownMenuRoot>
</template>

View file

@ -0,0 +1,39 @@
<script setup lang="ts">
import { cn } from '@/src/modules/shared/style/cn'
import {
DropdownMenuCheckboxItem,
type DropdownMenuCheckboxItemEmits,
type DropdownMenuCheckboxItemProps,
DropdownMenuItemIndicator,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuCheckboxItem
v-bind="forwarded"
: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 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Icon name="i-tabler-check" class="w-4 h-4" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>

View file

@ -0,0 +1,38 @@
<script setup lang="ts">
import { cn } from '@/src/modules/shared/style/cn'
import {
DropdownMenuContent,
type DropdownMenuContentEmits,
type DropdownMenuContentProps,
DropdownMenuPortal,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
{
sideOffset: 4,
},
)
const emits = defineEmits<DropdownMenuContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuPortal>
<DropdownMenuContent
v-bind="forwarded"
:class="cn('z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)"
>
<slot />
</DropdownMenuContent>
</DropdownMenuPortal>
</template>

View file

@ -0,0 +1,11 @@
<script setup lang="ts">
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'radix-vue'
const props = defineProps<DropdownMenuGroupProps>()
</script>
<template>
<DropdownMenuGroup v-bind="props">
<slot />
</DropdownMenuGroup>
</template>

View file

@ -0,0 +1,28 @@
<script setup lang="ts">
import { cn } from '@/src/modules/shared/style/cn'
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuItem
v-bind="forwardedProps"
: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 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
props.class,
)"
>
<slot />
</DropdownMenuItem>
</template>

View file

@ -0,0 +1,24 @@
<script setup lang="ts">
import { cn } from '@/src/modules/shared/style/cn'
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DropdownMenuLabel
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>
</template>

View file

@ -0,0 +1,19 @@
<script setup lang="ts">
import {
DropdownMenuRadioGroup,
type DropdownMenuRadioGroupEmits,
type DropdownMenuRadioGroupProps,
useForwardPropsEmits,
} from 'radix-vue'
const props = defineProps<DropdownMenuRadioGroupProps>()
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRadioGroup v-bind="forwarded">
<slot />
</DropdownMenuRadioGroup>
</template>

View file

@ -0,0 +1,40 @@
<script setup lang="ts">
import { cn } from '@/src/modules/shared/style/cn';
import {
DropdownMenuItemIndicator,
DropdownMenuRadioItem,
type DropdownMenuRadioItemEmits,
type DropdownMenuRadioItemProps,
useForwardPropsEmits,
} from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>();
const emits = defineEmits<DropdownMenuRadioItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DropdownMenuRadioItem
v-bind="forwarded"
: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 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Icon name="i-tabler-point-filled" class="w-4 h-4" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuRadioItem>
</template>

View file

@ -0,0 +1,22 @@
<script setup lang="ts">
import { cn } from '@/src/modules/shared/style/cn'
import {
DropdownMenuSeparator,
type DropdownMenuSeparatorProps,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DropdownMenuSeparatorProps & {
class?: HTMLAttributes['class']
}>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DropdownMenuSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
</template>

View file

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/src/modules/shared/style/cn'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<span :class="cn('ml-auto text-xs tracking-widest opacity-60', props.class)">
<slot />
</span>
</template>

View file

@ -0,0 +1,19 @@
<script setup lang="ts">
import {
DropdownMenuSub,
type DropdownMenuSubEmits,
type DropdownMenuSubProps,
useForwardPropsEmits,
} from 'radix-vue'
const props = defineProps<DropdownMenuSubProps>()
const emits = defineEmits<DropdownMenuSubEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuSub v-bind="forwarded">
<slot />
</DropdownMenuSub>
</template>

View file

@ -0,0 +1,30 @@
<script setup lang="ts">
import { cn } from '@/src/modules/shared/style/cn'
import {
DropdownMenuSubContent,
type DropdownMenuSubContentEmits,
type DropdownMenuSubContentProps,
useForwardPropsEmits,
} from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DropdownMenuSubContent
v-bind="forwarded"
:class="cn('z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)"
>
<slot />
</DropdownMenuSubContent>
</template>

View file

@ -0,0 +1,32 @@
<script setup lang="ts">
import { cn } from '@/src/modules/shared/style/cn';
import {
DropdownMenuSubTrigger,
type DropdownMenuSubTriggerProps,
useForwardProps,
} from 'radix-vue';
import { computed, type HTMLAttributes } from 'vue';
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<DropdownMenuSubTrigger
v-bind="forwardedProps"
:class="cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
props.class,
)"
>
<slot />
<Icon name="i-tabler-chevron-right" class="ml-auto h-4 w-4" />
</DropdownMenuSubTrigger>
</template>

View file

@ -0,0 +1,13 @@
<script setup lang="ts">
import { DropdownMenuTrigger, type DropdownMenuTriggerProps, useForwardProps } from 'radix-vue'
const props = defineProps<DropdownMenuTriggerProps>()
const forwardedProps = useForwardProps(props)
</script>
<template>
<DropdownMenuTrigger class="outline-none" v-bind="forwardedProps">
<slot />
</DropdownMenuTrigger>
</template>

View file

@ -0,0 +1,16 @@
export { default as DropdownMenu } from './DropdownMenu.vue'
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'
export { default as DropdownMenuSub } from './DropdownMenuSub.vue'
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
export { DropdownMenuPortal } from 'radix-vue'

View file

@ -0,0 +1,91 @@
import defaultTheme from 'tailwindcss/defaultTheme';
import animate from 'tailwindcss-animate';
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{vue,js,ts,jsx,tsx}', 'app.vue'],
darkMode: ['class'],
safelist: ['dark'],
prefix: '',
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
fontFamily: {
sans: ['Inter', ...defaultTheme.fontFamily.sans],
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
xl: 'calc(var(--radius) + 4px)',
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: 0 },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: 0 },
},
'collapsible-down': {
from: { height: 0 },
to: { height: 'var(--radix-collapsible-content-height)' },
},
'collapsible-up': {
from: { height: 'var(--radix-collapsible-content-height)' },
to: { height: 0 },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'collapsible-down': 'collapsible-down 0.2s ease-in-out',
'collapsible-up': 'collapsible-up 0.2s ease-in-out',
},
},
},
plugins: [animate],
};