mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-25 09:16:15 -04:00
feat(pages): home page base
This commit is contained in:
parent
87cfc9c1f3
commit
202896fa95
38 changed files with 3130 additions and 775 deletions
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtWelcome />
|
||||
</div>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
|
77
packages/app/assets/css/tailwind.css
Normal file
77
packages/app/assets/css/tailwind.css
Normal 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;
|
||||
}
|
||||
}
|
19
packages/app/components.json
Normal file
19
packages/app/components.json
Normal 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"
|
||||
}
|
||||
}
|
12
packages/app/i18n.config.ts
Normal file
12
packages/app/i18n.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export default defineI18nConfig(() => ({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
welcome: 'Welcome',
|
||||
},
|
||||
fr: {
|
||||
welcome: 'Bienvenue',
|
||||
},
|
||||
},
|
||||
}));
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
113
packages/app/src/modules/app/components/app-footer.vue
Normal file
113
packages/app/src/modules/app/components/app-footer.vue
Normal 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>
|
||||
©
|
||||
{{ new Date().getFullYear() }}
|
||||
Corentin Thomasset
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-foreground opacity-80%" />
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
41
packages/app/src/modules/app/components/app-header.vue
Normal file
41
packages/app/src/modules/app/components/app-header.vue
Normal 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>
|
11
packages/app/src/modules/app/components/grid-background.vue
Normal file
11
packages/app/src/modules/app/components/grid-background.vue
Normal 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>
|
11
packages/app/src/modules/app/layouts/default.vue
Normal file
11
packages/app/src/modules/app/layouts/default.vue
Normal 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>
|
0
packages/app/src/modules/app/nuxt.config.ts
Normal file
0
packages/app/src/modules/app/nuxt.config.ts
Normal file
34
packages/app/src/modules/app/pages/[...404].vue
Normal file
34
packages/app/src/modules/app/pages/[...404].vue
Normal 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>
|
57
packages/app/src/modules/app/pages/index.vue
Normal file
57
packages/app/src/modules/app/pages/index.vue
Normal 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>
|
4
packages/app/src/modules/shared/style/cn.ts
Normal file
4
packages/app/src/modules/shared/style/cn.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));
|
16
packages/app/src/modules/ui/components/badge/Badge.vue
Normal file
16
packages/app/src/modules/ui/components/badge/Badge.vue
Normal 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>
|
25
packages/app/src/modules/ui/components/badge/index.ts
Normal file
25
packages/app/src/modules/ui/components/badge/index.ts
Normal 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>
|
26
packages/app/src/modules/ui/components/button/Button.vue
Normal file
26
packages/app/src/modules/ui/components/button/Button.vue
Normal 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>
|
32
packages/app/src/modules/ui/components/button/index.ts
Normal file
32
packages/app/src/modules/ui/components/button/index.ts
Normal 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>;
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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'
|
91
packages/app/tailwind.config.js
Normal file
91
packages/app/tailwind.config.js
Normal 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],
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue