This commit is contained in:
commit
62903bf565
|
@ -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
|
|
@ -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.
|
|
@ -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',
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
export { default as Input } from './Input.vue'
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
export { default as Separator } from './Separator.vue'
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li>
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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')
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
export { default as Skeleton } from './Skeleton.vue'
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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'
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<slot />
|
||||
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
})
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<NuxtLayout name="main">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<NuxtLayout name="main">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<NuxtLayout name="landing-page">
|
||||
<div>
|
||||
demo page
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<NuxtLayout name="landing-page">
|
||||
<LandingIntroductionHeroSct />
|
||||
<LandingIntroductionHowItSct />
|
||||
<LandingIntroductionFeaturesSct />
|
||||
<LandingIntroductionCtaSct />
|
||||
</NuxtLayout>
|
||||
</template>
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
|
@ -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: [],
|
||||
}
|
|
@ -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',
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
Loading…
Reference in New Issue