This commit is contained in:
Rynare 2025-03-30 23:26:39 +07:00
commit 62903bf565
84 changed files with 13679 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

13
app.config.ts Normal file
View File

@ -0,0 +1,13 @@
export default defineAppConfig({
ui: {
slideover: {
overlay: {
transition: {
enter: 'ease-in-out duration-200',
leave: 'ease-in-out duration-200',
}
},
width: 'w-screen max-w-sm',
}
}
})

3
app.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
<NuxtPage />
</template>

View File

@ -0,0 +1,82 @@
@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 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem
;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--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%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%
;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

20
components.json Normal file
View File

@ -0,0 +1,20 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "shad-tailwind.config.ts",
"css": "assets/css/shad-tailwind.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"composables": "@/composables",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib"
},
"iconLibrary": "lucide"
}

View File

@ -0,0 +1,188 @@
<script setup lang="ts">
import { GalleryVerticalEnd } from 'lucide-vue-next'
import type { SidebarProps } from '~/components/ui/sidebar';
const props = defineProps<SidebarProps>()
// This is sample data.
const data = {
navMain: [
{
title: 'Getting Started',
url: '#',
items: [
{
title: 'Installation',
url: '#',
},
{
title: 'Project Structure',
url: '#',
},
],
},
{
title: 'Building Your Application',
url: '#',
items: [
{
title: 'Routing',
url: '#',
},
{
title: 'Data Fetching',
url: '#',
isActive: true,
},
{
title: 'Rendering',
url: '#',
},
{
title: 'Caching',
url: '#',
},
{
title: 'Styling',
url: '#',
},
{
title: 'Optimizing',
url: '#',
},
{
title: 'Configuring',
url: '#',
},
{
title: 'Testing',
url: '#',
},
{
title: 'Authentication',
url: '#',
},
{
title: 'Deploying',
url: '#',
},
{
title: 'Upgrading',
url: '#',
},
{
title: 'Examples',
url: '#',
},
],
},
{
title: 'API Reference',
url: '#',
items: [
{
title: 'Components',
url: '#',
},
{
title: 'File Conventions',
url: '#',
},
{
title: 'Functions',
url: '#',
},
{
title: 'next.config.js Options',
url: '#',
},
{
title: 'CLI',
url: '#',
},
{
title: 'Edge Runtime',
url: '#',
},
],
},
{
title: 'Architecture',
url: '#',
items: [
{
title: 'Accessibility',
url: '#',
},
{
title: 'Fast Refresh',
url: '#',
},
{
title: 'Next.js Compiler',
url: '#',
},
{
title: 'Supported Browsers',
url: '#',
},
{
title: 'Turbopack',
url: '#',
},
],
},
{
title: 'Community',
url: '#',
items: [
{
title: 'Contribution Guide',
url: '#',
},
],
},
],
}
</script>
<template>
<ShadSidebar v-bind="props">
<ShadSidebarHeader>
<ShadSidebarMenu>
<ShadSidebarMenuItem>
<ShadSidebarMenuButton size="lg" as-child>
<a href="#">
<div
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<GalleryVerticalEnd class="size-4" />
</div>
<div class="flex flex-col gap-0.5 leading-none">
<span class="font-semibold">Documentation</span>
<span class="">v1.0.0</span>
</div>
</a>
</ShadSidebarMenuButton>
</ShadSidebarMenuItem>
</ShadSidebarMenu>
</ShadSidebarHeader>
<ShadSidebarContent>
<ShadSidebarGroup>
<ShadSidebarMenu>
<ShadSidebarMenuItem v-for="item in data.navMain" :key="item.title">
<ShadSidebarMenuButton as-child>
<a :href="item.url" class="font-medium">
{{ item.title }}
</a>
</ShadSidebarMenuButton>
<ShadSidebarMenuSub v-if="item.items.length">
<ShadSidebarMenuSubItem v-for="childItem in item.items" :key="childItem.title">
<ShadSidebarMenuSubButton as-child :is-active="childItem.isActive">
<a :href="childItem.url">{{ childItem.title }}</a>
</ShadSidebarMenuSubButton>
</ShadSidebarMenuSubItem>
</ShadSidebarMenuSub>
</ShadSidebarMenuItem>
</ShadSidebarMenu>
</ShadSidebarGroup>
</ShadSidebarContent>
<SidebarRail />
</ShadSidebar>
</template>

View File

@ -0,0 +1,32 @@
<template>
<!-- Footer -->
<footer class="border-t bg-background">
<div class=" flex flex-col items-center justify-between gap-4 py-10 md:h-24 md:flex-row md:py-0">
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-primary-foreground">
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
<path d="M21 3v5h-5"></path>
</svg>
</div>
<p class="text-center text-sm leading-loose text-muted-foreground md:text-left">
© 2025 SmartLog. All rights reserved.
</p>
</div>
<div class="flex gap-4">
<a href="#" class="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
Terms
</a>
<a href="#" class="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
Privacy
</a>
<a href="#" class="text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
Contact
</a>
</div>
</div>
</footer>
</template>

View File

@ -0,0 +1,85 @@
<template>
<!-- Navigation -->
<header
class="fixed left-0 right-0 top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<NuxtUiContainer>
<div class="flex h-16 items-center">
<div class="flex items-center gap-2">
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-primary-foreground">
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
<path d="M21 3v5h-5"></path>
</svg>
</div>
<span class="text-xl font-bold">SmartLog</span>
</div>
<nav class="ml-auto flex items-center gap-4 sm:gap-6">
<div v-if="window.width.value <= 640">
<NuxtUiButton icon="i-heroicons-bars-3-bottom-left-16-solid" @click="isOpen = true"
color="white" variant="link" />
<NuxtUiSlideover v-model="isOpen" side="left">
<div class="p-4 flex flex-col h-full bg-gray-900 text-white">
<!-- Tombol close -->
<NuxtUiButton color="gray" variant="ghost" size="sm" icon="i-heroicons-x-mark-20-solid"
class="absolute end-5 top-5 z-10 " square padded @click="isOpen = false" />
<!-- Navigasi -->
<div class="mt-10 space-y-6">
<NuxtLink to="/#features" class="text-lg block hover:text-green-500"
@click="isOpen = false">
Features
</NuxtLink>
<NuxtLink to="/#how-it-works" class="text-lg block hover:text-green-500"
@click="isOpen = false">
How It Works
</NuxtLink>
<NuxtLink to="/#pricing" class="text-lg block hover:text-green-500"
@click="isOpen = false">
Pricing
</NuxtLink>
<NuxtLink to="/demo" class="text-lg block text-green-500 font-semibold"
@click="isOpen = false">
Demo
</NuxtLink>
</div>
<!-- Tombol "Get Started" -->
<div class="mt-auto flex justify-start">
<NuxtUiButton label="Log In" color="green" />
</div>
</div>
</NuxtUiSlideover>
</div>
<div v-else class="ml-auto flex items-center gap-4 sm:gap-6">
<NuxtLink href="/#features" class="text-sm font-medium transition-colors hover:text-primary">
Features
</NuxtLink>
<NuxtLink href="/#how-it-works"
class="text-sm font-medium transition-colors hover:text-primary">How
It
Works</NuxtLink>
<NuxtLink href="/#pricing" class="text-sm font-medium transition-colors hover:text-primary">
Pricing
</NuxtLink>
<NuxtLink href="/demo"
class="text-sm font-medium text-primary transition-colors hover:text-primary/80">
Demo</NuxtLink>
<NuxtUiButton color="green">
Get Started
</NuxtUiButton>
</div>
</nav>
</div>
</NuxtUiContainer>
</header>
</template>
<script lang="ts" setup>
import { useWindowSize } from '@vueuse/core';
const window = useWindowSize()
const isOpen = ref(false)
</script>

View File

@ -0,0 +1,22 @@
<template>
<!-- CTA Section -->
<section class="container py-20">
<div class="mx-auto max-w-4xl rounded-lg p-8 md:p-12">
<div class="flex flex-col items-center justify-between gap-4 text-center md:flex-row md:text-left">
<div class="space-y-2">
<h2 class="text-3xl font-bold tracking-tighter text-primary-foreground sm:text-4xl">Ready to
start predicting?</h2>
<p class="text-primary-foreground/80 md:text-lg">
Try our demo today and see the power of AI predictions.
</p>
</div>
<NuxtUiButton color="green"
class="h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow ">
<span>
Try Demo
</span>
</NuxtUiButton>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,113 @@
<template>
<!-- Features Section -->
<section id="features" class="container py-20">
<div class="mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center">
<h2 class="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">Powerful Prediction Features
</h2>
<p class="max-w-[85%] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
Our AI-powered platform offers a range of features to help you make accurate predictions.
</p>
</div>
<div class="mx-auto grid justify-center gap-4 sm:grid-cols-2 md:max-w-[64rem] md:grid-cols-3 mt-16">
<div class="relative overflow-hidden rounded-lg border bg-background p-6">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-primary">
<path d="M3 3v18h18"></path>
<path d="m19 9-5 5-4-4-3 3"></path>
</svg>
</div>
<div class="mt-4 space-y-2">
<h3 class="font-bold">Data Analysis</h3>
<p class="text-sm text-muted-foreground">
Analyze historical data to identify patterns and trends.
</p>
</div>
</div>
<div class="relative overflow-hidden rounded-lg border bg-background p-6">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-primary">
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
<path d="M21 3v5h-5"></path>
</svg>
</div>
<div class="mt-4 space-y-2">
<h3 class="font-bold">Real-time Predictions</h3>
<p class="text-sm text-muted-foreground">
Get instant predictions based on the latest data inputs.
</p>
</div>
</div>
<div class="relative overflow-hidden rounded-lg border bg-background p-6">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-primary">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"></path>
</svg>
</div>
<div class="mt-4 space-y-2">
<h3 class="font-bold">Secure & Private</h3>
<p class="text-sm text-muted-foreground">
Your data is encrypted and protected at all times.
</p>
</div>
</div>
<div class="relative overflow-hidden rounded-lg border bg-background p-6">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-primary">
<path d="M2 3h20"></path>
<path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"></path>
<path d="m7 21 5-5 5 5"></path>
</svg>
</div>
<div class="mt-4 space-y-2">
<h3 class="font-bold">Custom Reports</h3>
<p class="text-sm text-muted-foreground">
Generate detailed reports with visualizations and insights.
</p>
</div>
</div>
<div class="relative overflow-hidden rounded-lg border bg-background p-6">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-primary">
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
</svg>
</div>
<div class="mt-4 space-y-2">
<h3 class="font-bold">AI Models</h3>
<p class="text-sm text-muted-foreground">
Choose from various AI models optimized for different prediction tasks.
</p>
</div>
</div>
<div class="relative overflow-hidden rounded-lg border bg-background p-6">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-primary">
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path>
<path d="m2 12 5-5"></path>
<path d="M2 12h10"></path>
<path d="m12 12 5 5"></path>
</svg>
</div>
<div class="mt-4 space-y-2">
<h3 class="font-bold">API Integration</h3>
<p class="text-sm text-muted-foreground">
Easily integrate our prediction API into your existing systems.
</p>
</div>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,54 @@
<template>
<!-- Hero Section -->
<section class="container py-20 md:py-32">
<div class="grid gap-10 md:grid-cols-2 md:gap-16">
<div class="flex flex-col justify-center space-y-6 sm:pb-32">
<div class="space-y-2">
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
Predict the Future with ARIMA
</h1>
<p class="text-lg text-muted-foreground md:text-xl">
Harness the power of ARIMA to make accurate predictions and informed decisions.
</p>
</div>
<div class="flex flex-col gap-2 min-[400px]:flex-row">
<NuxtUiButton
class="inline-flex h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow"
to="/demo">
Try Demo
</NuxtUiButton>
<NuxtUiButton variant="outline" color="white"
class="inline-flex h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow">
Learn More
</NuxtUiButton>
</div>
</div>
<div class="flex items-center justify-center">
<div
class="relative h-[350px] w-[350px] rounded-full bg-gradient-to-r from-primary/20 to-primary/40 p-4">
<div class="absolute inset-0 flex items-center justify-center">
<div class="h-[250px] w-[250px] rounded-full bg-background p-4 shadow-lg">
<div
class="flex h-full w-full items-center justify-center rounded-full bg-gradient-to-br from-primary/10 to-primary/30">
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round"
stroke-linejoin="round" class="text-primary">
<path d="M12 2v4"></path>
<path d="M12 18v4"></path>
<path d="m4.93 4.93 2.83 2.83"></path>
<path d="m16.24 16.24 2.83 2.83"></path>
<path d="M2 12h4"></path>
<path d="M18 12h4"></path>
<path d="m4.93 19.07 2.83-2.83"></path>
<path d="m16.24 7.76 2.83-2.83"></path>
<circle cx="12" cy="12" r="5"></circle>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,36 @@
<template>
<!-- How It Works Section -->
<section id="how-it-works" class="container py-20 bg-muted/50 rounded-lg my-10">
<div class="mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center">
<h2 class="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">How It Works</h2>
<p class="max-w-[85%] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
Our prediction platform is easy to use and delivers accurate results in just a few steps.
</p>
</div>
<div class="mx-auto grid max-w-5xl grid-cols-1 gap-8 md:grid-cols-3 md:gap-12 mt-16">
<div class="flex flex-col items-center space-y-4 text-center">
<div
class="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-3xl font-bold text-primary-foreground">
1</div>
<h3 class="text-xl font-bold">Input Your Data</h3>
<p class="text-muted-foreground">Upload your historical data or connect to your data sources.</p>
</div>
<div class="flex flex-col items-center space-y-4 text-center">
<div
class="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-3xl font-bold text-primary-foreground">
2</div>
<h3 class="text-xl font-bold">Select AI Model</h3>
<p class="text-muted-foreground">Choose the prediction model that best fits your needs.</p>
</div>
<div class="flex flex-col items-center space-y-4 text-center">
<div
class="flex h-16 w-16 items-center justify-center rounded-full bg-primary text-3xl font-bold text-primary-foreground">
3</div>
<h3 class="text-xl font-bold">Get Predictions</h3>
<p class="text-muted-foreground">Receive accurate predictions and actionable insights.</p>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,3 @@
<template>
</template>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'reka-ui'
import { type ButtonVariants, buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
}
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,35 @@
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 gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
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,24 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useVModel } from '@vueuse/core'
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes['class']
}>()
const emits = defineEmits<{
(e: 'update:modelValue', payload: string | number): void
}>()
const modelValue = useVModel(props, 'modelValue', emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<input v-model="modelValue" :class="cn('flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)">
</template>

View File

@ -0,0 +1 @@
export { default as Input } from './Input.vue'

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { Separator, type SeparatorProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<
SeparatorProps & { class?: HTMLAttributes['class'], label?: string }
>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<Separator
v-bind="delegatedProps"
:class="
cn(
'shrink-0 bg-border relative',
props.orientation === 'vertical' ? 'w-px h-full' : 'h-px w-full',
props.class,
)
"
>
<span
v-if="props.label"
:class="
cn(
'text-xs text-muted-foreground bg-background absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex justify-center items-center',
props.orientation === 'vertical' ? 'w-[1px] px-1 py-2' : 'h-[1px] py-1 px-2',
)
"
>{{ props.label }}</span>
</Separator>
</template>

View File

@ -0,0 +1 @@
export { default as Separator } from './Separator.vue'

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot v-bind="forwarded">
<slot />
</DialogRoot>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogClose, type DialogCloseProps } from 'reka-ui'
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose v-bind="props">
<slot />
</DialogClose>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { X } from 'lucide-vue-next'
import {
DialogClose,
DialogContent,
type DialogContentEmits,
type DialogContentProps,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { type SheetVariants, sheetVariants } from '.'
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes['class']
side?: SheetVariants['side']
}
defineOptions({
inheritAttrs: false,
})
const props = defineProps<SheetContentProps>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = computed(() => {
const { class: _, side, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<DialogContent
:class="cn(sheetVariants({ side }), props.class)"
v-bind="{ ...forwarded, ...$attrs }"
>
<slot />
<DialogClose
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
>
<X class="w-4 h-4" />
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { DialogDescription, type DialogDescriptionProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DialogDescription
:class="cn('text-sm text-muted-foreground', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogDescription>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
</script>
<template>
<div
:class="
cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
</script>
<template>
<div
:class="
cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)
"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { DialogTitle, type DialogTitleProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DialogTitle
:class="cn('text-lg font-semibold text-foreground', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogTitle>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { DialogTrigger, type DialogTriggerProps } from 'reka-ui'
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger v-bind="props">
<slot />
</DialogTrigger>
</template>

View File

@ -0,0 +1,31 @@
import { cva, type VariantProps } from 'class-variance-authority'
export { default as Sheet } from './Sheet.vue'
export { default as SheetClose } from './SheetClose.vue'
export { default as SheetContent } from './SheetContent.vue'
export { default as SheetDescription } from './SheetDescription.vue'
export { default as SheetFooter } from './SheetFooter.vue'
export { default as SheetHeader } from './SheetHeader.vue'
export { default as SheetTitle } from './SheetTitle.vue'
export { default as SheetTrigger } from './SheetTrigger.vue'
export const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
},
},
defaultVariants: {
side: 'right',
},
},
)
export type SheetVariants = VariantProps<typeof sheetVariants>

View File

@ -0,0 +1,85 @@
<script setup lang="ts">
import type { SidebarProps } from '.'
import { cn } from '@/lib/utils'
import { Sheet, SheetContent } from '@/components/ui/sheet'
import { SIDEBAR_WIDTH_MOBILE, useSidebar } from './utils'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SidebarProps>(), {
side: 'left',
variant: 'sidebar',
collapsible: 'offcanvas',
})
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
</script>
<template>
<div
v-if="collapsible === 'none'"
:class="cn('flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground', props.class)"
v-bind="$attrs"
>
<slot />
</div>
<Sheet v-else-if="isMobile" :open="openMobile" v-bind="$attrs" @update:open="setOpenMobile">
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
:side="side"
class="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
:style="{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
}"
>
<div class="flex h-full w-full flex-col">
<slot />
</div>
</SheetContent>
</Sheet>
<div
v-else class="group peer hidden md:block"
:data-state="state"
:data-collapsible="state === 'collapsed' ? collapsible : ''"
:data-variant="variant"
:data-side="side"
>
<!-- This is what handles the sidebar gap on desktop -->
<div
:class="cn(
'duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
)"
/>
<div
:class="cn(
'duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
props.class,
)"
v-bind="$attrs"
>
<div
data-sidebar="sidebar"
class="flex h-full w-full flex-col text-sidebar-foreground bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
<slot />
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-sidebar="content"
:class="cn('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-sidebar="footer"
:class="cn('flex flex-col gap-2 p-2', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-sidebar="group"
:class="cn('relative flex w-full min-w-0 flex-col p-2', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive } from 'reka-ui'
const props = defineProps<PrimitiveProps & {
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Primitive
data-sidebar="group-action"
:as="as"
:as-child="asChild"
:class="cn(
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'after:absolute after:-inset-2 after:md:hidden',
'group-data-[collapsible=icon]:hidden',
props.class,
)"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-sidebar="group-content"
:class="cn('w-full text-sm', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive } from 'reka-ui'
const props = defineProps<PrimitiveProps & {
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Primitive
data-sidebar="group-label"
:as="as"
:as-child="asChild"
:class="cn(
'duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
props.class)"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-sidebar="header"
:class="cn('flex flex-col gap-2 p-2', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Input } from '@/components/ui/input'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Input
data-sidebar="input"
:class="cn(
'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring',
props.class,
)"
>
<slot />
</Input>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<main
:class="cn(
'relative flex min-h-svh flex-1 flex-col bg-background',
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
props.class,
)"
>
<slot />
</main>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<ul
data-sidebar="menu"
:class="cn('flex w-full min-w-0 flex-col gap-1', props.class)"
>
<slot />
</ul>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'reka-ui'
const props = withDefaults(defineProps<PrimitiveProps & {
showOnHover?: boolean
class?: HTMLAttributes['class']
}>(), {
as: 'button',
})
</script>
<template>
<Primitive
data-sidebar="menu-action"
:class="cn(
'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
'after:absolute after:-inset-2 after:md:hidden',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
showOnHover
&& 'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
props.class,
)"
:as="as"
:as-child="asChild"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-sidebar="menu-badge"
:class="cn(
'absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
props.class,
)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,49 @@
<script setup lang="ts">
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { type Component, computed } from 'vue'
import SidebarMenuButtonChild, { type SidebarMenuButtonProps } from './SidebarMenuButtonChild.vue'
import { useSidebar } from './utils'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SidebarMenuButtonProps & {
tooltip?: string | Component
}>(), {
as: 'button',
variant: 'default',
size: 'default',
})
const { isMobile, state } = useSidebar()
const delegatedProps = computed(() => {
const { tooltip, ...delegated } = props
return delegated
})
</script>
<template>
<SidebarMenuButtonChild v-if="!tooltip" v-bind="{ ...delegatedProps, ...$attrs }">
<slot />
</SidebarMenuButtonChild>
<Tooltip v-else>
<TooltipTrigger as-child>
<SidebarMenuButtonChild v-bind="{ ...delegatedProps, ...$attrs }">
<slot />
</SidebarMenuButtonChild>
</TooltipTrigger>
<TooltipContent
side="right"
align="center"
:hidden="state !== 'collapsed' || isMobile"
>
<template v-if="typeof tooltip === 'string'">
{{ tooltip }}
</template>
<component :is="tooltip" v-else />
</TooltipContent>
</Tooltip>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive, type PrimitiveProps } from 'reka-ui'
import { type SidebarMenuButtonVariants, sidebarMenuButtonVariants } from '.'
export interface SidebarMenuButtonProps extends PrimitiveProps {
variant?: SidebarMenuButtonVariants['variant']
size?: SidebarMenuButtonVariants['size']
isActive?: boolean
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<SidebarMenuButtonProps>(), {
as: 'button',
variant: 'default',
size: 'default',
})
</script>
<template>
<Primitive
data-sidebar="menu-button"
:data-size="size"
:data-active="isActive"
:class="cn(sidebarMenuButtonVariants({ variant, size }), props.class)"
:as="as"
:as-child="asChild"
v-bind="$attrs"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<li
data-sidebar="menu-item"
:class="cn('group/menu-item relative', props.class)"
>
<slot />
</li>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'
import { computed, type HTMLAttributes } from 'vue'
const props = defineProps<{
showIcon?: boolean
class?: HTMLAttributes['class']
}>()
const width = computed(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
})
</script>
<template>
<div
data-sidebar="menu-skeleton"
:class="cn('rounded-md h-8 flex gap-2 px-2 items-center', props.class)"
>
<Skeleton
v-if="showIcon"
class="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
<Skeleton
class="h-4 flex-1 max-w-[--skeleton-width]"
data-sidebar="menu-skeleton-text"
:style="{ '--skeleton-width': width }"
/>
</div>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<ul
data-sidebar="menu-badge"
:class="cn(
'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5',
'group-data-[collapsible=icon]:hidden',
props.class,
)"
>
<slot />
</ul>
</template>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Primitive } from 'reka-ui'
const props = withDefaults(defineProps<PrimitiveProps & {
size?: 'sm' | 'md'
isActive?: boolean
class?: HTMLAttributes['class']
}>(), {
as: 'a',
size: 'md',
})
</script>
<template>
<Primitive
data-sidebar="menu-sub-button"
:as="as"
:as-child="asChild"
:data-size="size"
:data-active="isActive"
:class="cn(
'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
props.class,
)"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
</script>
<template>
<li>
<slot />
</li>
</template>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { useEventListener, useMediaQuery, useVModel } from '@vueuse/core'
import { TooltipProvider } from 'reka-ui'
import { computed, type HTMLAttributes, type Ref, ref } from 'vue'
import { provideSidebarContext, SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from './utils'
const props = withDefaults(defineProps<{
defaultOpen?: boolean
open?: boolean
class?: HTMLAttributes['class']
}>(), {
defaultOpen: true,
open: undefined,
})
const emits = defineEmits<{
'update:open': [open: boolean]
}>()
const isMobile = useMediaQuery('(max-width: 768px)')
const openMobile = ref(false)
const open = useVModel(props, 'open', emits, {
defaultValue: props.defaultOpen ?? false,
passive: (props.open === undefined) as false,
}) as Ref<boolean>
function setOpen(value: boolean) {
open.value = value // emits('update:open', value)
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}
function setOpenMobile(value: boolean) {
openMobile.value = value
}
// Helper to toggle the sidebar.
function toggleSidebar() {
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
}
useEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
toggleSidebar()
}
})
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = computed(() => open.value ? 'expanded' : 'collapsed')
provideSidebarContext({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
})
</script>
<template>
<TooltipProvider :delay-duration="0">
<div
:style="{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
}"
:class="cn('group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar', props.class)"
v-bind="$attrs"
>
<slot />
</div>
</TooltipProvider>
</template>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useSidebar } from './utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const { toggleSidebar } = useSidebar()
</script>
<template>
<button
data-sidebar="rail"
aria-label="Toggle Sidebar"
:tabindex="-1"
title="Toggle Sidebar"
:class="cn(
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
props.class,
)"
@click="toggleSidebar"
>
<slot />
</button>
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Separator } from '@/components/ui/separator'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<Separator
data-sidebar="separator"
:class="cn('mx-2 w-auto bg-sidebar-border', props.class)"
>
<slot />
</Separator>
</template>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { PanelLeft } from 'lucide-vue-next'
import { useSidebar } from './utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const { toggleSidebar } = useSidebar()
</script>
<template>
<Button
data-sidebar="trigger"
variant="ghost"
size="icon"
:class="cn('h-7 w-7', props.class)"
@click="toggleSidebar"
>
<PanelLeft />
<span class="sr-only">Toggle Sidebar</span>
</Button>
</template>

View File

@ -0,0 +1,60 @@
import type { VariantProps } from 'class-variance-authority'
import type { HTMLAttributes } from 'vue'
import { cva } from 'class-variance-authority'
export interface SidebarProps {
side?: 'left' | 'right'
variant?: 'sidebar' | 'floating' | 'inset'
collapsible?: 'offcanvas' | 'icon' | 'none'
class?: HTMLAttributes['class']
}
export { default as Sidebar } from './Sidebar.vue'
export { default as SidebarContent } from './SidebarContent.vue'
export { default as SidebarFooter } from './SidebarFooter.vue'
export { default as SidebarGroup } from './SidebarGroup.vue'
export { default as SidebarGroupAction } from './SidebarGroupAction.vue'
export { default as SidebarGroupContent } from './SidebarGroupContent.vue'
export { default as SidebarGroupLabel } from './SidebarGroupLabel.vue'
export { default as SidebarHeader } from './SidebarHeader.vue'
export { default as SidebarInput } from './SidebarInput.vue'
export { default as SidebarInset } from './SidebarInset.vue'
export { default as SidebarMenu } from './SidebarMenu.vue'
export { default as SidebarMenuAction } from './SidebarMenuAction.vue'
export { default as SidebarMenuBadge } from './SidebarMenuBadge.vue'
export { default as SidebarMenuButton } from './SidebarMenuButton.vue'
export { default as SidebarMenuItem } from './SidebarMenuItem.vue'
export { default as SidebarMenuSkeleton } from './SidebarMenuSkeleton.vue'
export { default as SidebarMenuSub } from './SidebarMenuSub.vue'
export { default as SidebarMenuSubButton } from './SidebarMenuSubButton.vue'
export { default as SidebarMenuSubItem } from './SidebarMenuSubItem.vue'
export { default as SidebarProvider } from './SidebarProvider.vue'
export { default as SidebarRail } from './SidebarRail.vue'
export { default as SidebarSeparator } from './SidebarSeparator.vue'
export { default as SidebarTrigger } from './SidebarTrigger.vue'
export { useSidebar } from './utils'
export const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
{
variants: {
variant: {
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
outline:
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
},
size: {
default: 'h-8 text-sm',
sm: 'h-7 text-xs',
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
export type SidebarMenuButtonVariants = VariantProps<typeof sidebarMenuButtonVariants>

View File

@ -0,0 +1,19 @@
import type { ComputedRef, Ref } from 'vue'
import { createContext } from 'reka-ui'
export const SIDEBAR_COOKIE_NAME = 'sidebar:state'
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
export const SIDEBAR_WIDTH = '16rem'
export const SIDEBAR_WIDTH_MOBILE = '18rem'
export const SIDEBAR_WIDTH_ICON = '3rem'
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
export const [useSidebar, provideSidebarContext] = createContext<{
state: ComputedRef<'expanded' | 'collapsed'>
open: Ref<boolean>
setOpen: (value: boolean) => void
isMobile: Ref<boolean>
openMobile: Ref<boolean>
setOpenMobile: (value: boolean) => void
toggleSidebar: () => void
}>('Sidebar')

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
interface SkeletonProps {
class?: HTMLAttributes['class']
}
const props = defineProps<SkeletonProps>()
</script>
<template>
<div :class="cn('animate-pulse rounded-md bg-primary/10', props.class)" />
</template>

View File

@ -0,0 +1 @@
export { default as Skeleton } from './Skeleton.vue'

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { TooltipRoot, type TooltipRootEmits, type TooltipRootProps, useForwardPropsEmits } from 'reka-ui'
const props = defineProps<TooltipRootProps>()
const emits = defineEmits<TooltipRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<TooltipRoot v-bind="forwarded">
<slot />
</TooltipRoot>
</template>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { TooltipContent, type TooltipContentEmits, type TooltipContentProps, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(), {
sideOffset: 4,
})
const emits = defineEmits<TooltipContentEmits>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<TooltipPortal>
<TooltipContent v-bind="{ ...forwarded, ...$attrs }" :class="cn('z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 />
</TooltipContent>
</TooltipPortal>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { TooltipProvider, type TooltipProviderProps } from 'reka-ui'
const props = defineProps<TooltipProviderProps>()
</script>
<template>
<TooltipProvider v-bind="props">
<slot />
</TooltipProvider>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { TooltipTrigger, type TooltipTriggerProps } from 'reka-ui'
const props = defineProps<TooltipTriggerProps>()
</script>
<template>
<TooltipTrigger v-bind="props">
<slot />
</TooltipTrigger>
</template>

View File

@ -0,0 +1,4 @@
export { default as Tooltip } from './Tooltip.vue'
export { default as TooltipContent } from './TooltipContent.vue'
export { default as TooltipProvider } from './TooltipProvider.vue'
export { default as TooltipTrigger } from './TooltipTrigger.vue'

3
layouts/auth.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
<slot />
</template>

10
layouts/landing-page.vue Normal file
View File

@ -0,0 +1,10 @@
<template>
<NuxtUiContainer
class="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 flex flex-col">
<LandingHeader />
<div class="grow">
<slot />
</div>
<LandingFooter />
</NuxtUiContainer>
</template>

22
layouts/main.vue Normal file
View File

@ -0,0 +1,22 @@
<template>
<ShadSidebarProvider>
<DashboardSidebar />
<ShadSidebarInset>
<header class="flex h-16 shrink-0 items-center gap-2 border-b">
<div class="flex items-center gap-2 px-3">
<ShadSidebarTrigger />
<ShadSeparator orientation="vertical" class="mr-2 h-4" />
</div>
</header>
<div class="flex flex-1 flex-col gap-4 p-4">
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div class="aspect-video rounded-xl bg-muted/50" />
<div class="aspect-video rounded-xl bg-muted/50" />
<div class="aspect-video rounded-xl bg-muted/50" />
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
<slot />
</div>
</ShadSidebarInset>
</ShadSidebarProvider>
</template>

15
lib/utils.ts Normal file
View File

@ -0,0 +1,15 @@
import type { Updater } from '@tanstack/vue-table'
import type { Ref } from 'vue'
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
ref.value
= typeof updaterOrValue === 'function'
? updaterOrValue(ref.value)
: updaterOrValue
}

20
nuxt.config.ts Normal file
View File

@ -0,0 +1,20 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
modules: ['@nuxt/image', '@nuxt/ui', 'shadcn-nuxt'],
ui: {
prefix: 'NuxtUi'
},
shadcn: {
/**
* Prefix for all the imported component
*/
prefix: 'Shad',
/**
* Directory that the component lives in.
* @default "./components/ui"
*/
componentDir: './components/ui'
}
})

11549
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "^2.21.1",
"@vueuse/core": "^13.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.485.0",
"nuxt": "^3.16.1",
"reka-ui": "^2.1.1",
"shadcn-nuxt": "^1.0.3",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
}
}

View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout name="auth">
<div></div>
</NuxtLayout>
</template>

View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout name="auth">
<div></div>
</NuxtLayout>
</template>

View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout name="auth">
<div></div>
</NuxtLayout>
</template>

5
pages/auth/sign-in.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout name="auth">
<div></div>
</NuxtLayout>
</template>

5
pages/auth/sign-up.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout name="auth">
<div></div>
</NuxtLayout>
</template>

View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout name="main">
<div></div>
</NuxtLayout>
</template>

View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout name="main">
<div></div>
</NuxtLayout>
</template>

7
pages/demo.vue Normal file
View File

@ -0,0 +1,7 @@
<template>
<NuxtLayout name="landing-page">
<div>
demo page
</div>
</NuxtLayout>
</template>

8
pages/index.vue Normal file
View File

@ -0,0 +1,8 @@
<template>
<NuxtLayout name="landing-page">
<LandingIntroductionHeroSct />
<LandingIntroductionHowItSct />
<LandingIntroductionFeaturesSct />
<LandingIntroductionCtaSct />
</NuxtLayout>
</template>

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
public/robots.txt Normal file
View File

@ -0,0 +1 @@

3
server/tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

22
shad-tailwind.config.ts Normal file
View File

@ -0,0 +1,22 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: [],
theme: {
extend: {
colors: {
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
}
}
},
plugins: [],
}

22
tailwind.config.ts Normal file
View File

@ -0,0 +1,22 @@
import type { Config } from 'tailwindcss'
export default <Partial<Config>>{
theme: {
extend: {
aspectRatio: {
auto: 'auto',
square: '1 / 1',
video: '16 / 9'
}
},
screens: {
'mobile': '360px',
'tablet': '520px',
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1440px',
}
}
}

4
tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}