Added vue frontend project, fixed swagger path
This commit is contained in:
@@ -26,7 +26,7 @@ API_PORT=8000
|
|||||||
API_SECRET=change-me
|
API_SECRET=change-me
|
||||||
|
|
||||||
OPEN_API_ENABLED=true
|
OPEN_API_ENABLED=true
|
||||||
OPEN_API_ENDPOINT=/docs
|
OPEN_API_ENDPOINT=/openapi
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Обязательные переменные по режимам
|
### 2. Обязательные переменные по режимам
|
||||||
@@ -49,7 +49,7 @@ OPEN_API_ENDPOINT=/docs
|
|||||||
- `DB_PATH=sqlite://data/app.db`
|
- `DB_PATH=sqlite://data/app.db`
|
||||||
- `API_HOST=localhost`
|
- `API_HOST=localhost`
|
||||||
- `API_PORT=8000`
|
- `API_PORT=8000`
|
||||||
- `OPEN_API_ENDPOINT=/docs`
|
- `OPEN_API_ENDPOINT=/openapi`
|
||||||
|
|
||||||
### 4. Описание переменных
|
### 4. Описание переменных
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
swaggerFiles "github.com/swaggo/files"
|
swaggerFiles "github.com/swaggo/files"
|
||||||
@@ -34,9 +36,41 @@ func NewServer(cfg config.Config) *Server {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
if cfg.OpenAPIEnabled {
|
if cfg.OpenAPIEnabled {
|
||||||
router.GET("/openapi/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
openAPIEndpoint := cfg.OpenAPIEndpoint
|
||||||
|
if openAPIEndpoint == "" {
|
||||||
|
openAPIEndpoint = "/openapi"
|
||||||
|
}
|
||||||
|
swaggerHandler := ginSwagger.WrapHandler(swaggerFiles.Handler)
|
||||||
|
|
||||||
|
router.GET(openAPIEndpoint, func(c *gin.Context) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
proxyCtx, _ := gin.CreateTestContext(recorder)
|
||||||
|
proxyCtx.Request = c.Request.Clone(c.Request.Context())
|
||||||
|
proxyCtx.Request.URL.Path = openAPIEndpoint + "/index.html"
|
||||||
|
proxyCtx.Request.RequestURI = openAPIEndpoint + "/index.html"
|
||||||
|
|
||||||
|
swaggerHandler(proxyCtx)
|
||||||
|
|
||||||
|
for key, values := range recorder.Header() {
|
||||||
|
for _, value := range values {
|
||||||
|
c.Writer.Header().Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body := strings.Replace(
|
||||||
|
recorder.Body.String(),
|
||||||
|
"<head>",
|
||||||
|
"<head><base href=\""+openAPIEndpoint+"/\">",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Status(recorder.Code)
|
||||||
|
_, _ = c.Writer.WriteString(body)
|
||||||
|
})
|
||||||
|
router.GET(openAPIEndpoint+"/*any", swaggerHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiV1 := router.Group("/api/v1")
|
apiV1 := router.Group("/api/v1")
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func Load() (Config, error) {
|
|||||||
apiPort = "8000"
|
apiPort = "8000"
|
||||||
}
|
}
|
||||||
if openAPIEndpoint == "" {
|
if openAPIEndpoint == "" {
|
||||||
openAPIEndpoint = "/docs"
|
openAPIEndpoint = "/openapi"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
-3
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["Vue.volar"]
|
|
||||||
}
|
|
||||||
+5
-4
@@ -1,13 +1,14 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>frontend</title>
|
<title>FamilyHub</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
Generated
+2027
File diff suppressed because it is too large
Load Diff
+17
-12
@@ -1,22 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "familyhub-vue",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"build": "vite build",
|
||||||
"build": "vue-tsc -b && vite build",
|
"dev": "vite"
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.30"
|
"@tailwindcss/vite": "4.1.12",
|
||||||
|
"lucide-vue-next": "0.487.0",
|
||||||
|
"tailwindcss": "4.1.12",
|
||||||
|
"tw-animate-css": "^1.3.8",
|
||||||
|
"vue": "3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.12.0",
|
"@types/node": "^25.5.2",
|
||||||
"@vitejs/plugin-vue": "^6.0.5",
|
"@vitejs/plugin-vue": "5.2.4",
|
||||||
"@vue/tsconfig": "^0.9.0",
|
"vite": "6.3.5"
|
||||||
"typescript": "~5.9.3",
|
},
|
||||||
"vite": "^8.0.1",
|
"pnpm": {
|
||||||
"vue-tsc": "^3.2.5"
|
"overrides": {
|
||||||
|
"vite": "6.3.5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* PostCSS Configuration
|
||||||
|
*
|
||||||
|
* Tailwind CSS v4 (via @tailwindcss/vite) automatically sets up all required
|
||||||
|
* PostCSS plugins — you do NOT need to include `tailwindcss` or `autoprefixer` here.
|
||||||
|
*
|
||||||
|
* This file only exists for adding additional PostCSS plugins, if needed.
|
||||||
|
* For example:
|
||||||
|
*
|
||||||
|
* import postcssNested from 'postcss-nested'
|
||||||
|
* export default { plugins: [postcssNested()] }
|
||||||
|
*
|
||||||
|
* Otherwise, you can leave this file empty.
|
||||||
|
*/
|
||||||
|
export default {}
|
||||||
+55
-2
@@ -1,7 +1,60 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
import { ref } from 'vue';
|
||||||
|
import Header from './components/Header.vue';
|
||||||
|
import Navigation from './components/Navigation.vue';
|
||||||
|
import BalanceWidget from './components/BalanceWidget.vue';
|
||||||
|
import TodayWidget from './components/TodayWidget.vue';
|
||||||
|
import RecentActivityWidget from './components/RecentActivityWidget.vue';
|
||||||
|
import SwipeCards from './components/SwipeCards.vue';
|
||||||
|
import FinanceScreen from './components/FinanceScreen.vue';
|
||||||
|
import SettingsScreen from './components/SettingsScreen.vue';
|
||||||
|
|
||||||
|
const activeScreen = ref('home');
|
||||||
|
const previousScreen = ref('home');
|
||||||
|
|
||||||
|
function handleNavigate(screen: string) {
|
||||||
|
if (screen === 'settings') {
|
||||||
|
if (activeScreen.value !== 'settings') {
|
||||||
|
previousScreen.value = activeScreen.value;
|
||||||
|
}
|
||||||
|
activeScreen.value = 'settings';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screen === 'close-settings') {
|
||||||
|
activeScreen.value = previousScreen.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeScreen.value = screen;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<HelloWorld />
|
<FinanceScreen
|
||||||
|
v-if="activeScreen === 'finance'"
|
||||||
|
@navigate="handleNavigate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingsScreen
|
||||||
|
v-else-if="activeScreen === 'settings'"
|
||||||
|
@navigate="handleNavigate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-else class="min-h-screen bg-[#0A0A0F] dark">
|
||||||
|
<div class="mx-auto flex min-h-screen max-w-md flex-col relative">
|
||||||
|
<Header @navigate="handleNavigate" />
|
||||||
|
|
||||||
|
<main class="flex-1 overflow-y-auto px-5 pb-32">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<BalanceWidget />
|
||||||
|
<TodayWidget />
|
||||||
|
<SwipeCards />
|
||||||
|
<RecentActivityWidget />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Navigation :active-screen="activeScreen" @navigate="handleNavigate" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.5 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 496 B |
@@ -0,0 +1,127 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { BarChart3, Calendar, PieChart, TrendingUp } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
const categoryData = [
|
||||||
|
{ name: 'Food', value: 850, color: '#f97316' },
|
||||||
|
{ name: 'Transport', value: 420, color: '#ef4444' },
|
||||||
|
{ name: 'Shopping', value: 680, color: '#8b5cf6' },
|
||||||
|
{ name: 'Bills', value: 1250, color: '#3b82f6' },
|
||||||
|
{ name: 'Others', value: 320, color: '#6366f1' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const monthlyData = [
|
||||||
|
{ month: 'Oct', income: 7200, expenses: 4800 },
|
||||||
|
{ month: 'Nov', income: 8100, expenses: 5200 },
|
||||||
|
{ month: 'Dec', income: 7800, expenses: 4500 },
|
||||||
|
{ month: 'Jan', income: 8400, expenses: 5100 },
|
||||||
|
{ month: 'Feb', income: 7900, expenses: 4900 },
|
||||||
|
{ month: 'Mar', income: 8240, expenses: 3120 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const totalExpenses = categoryData.reduce((sum, item) => sum + item.value, 0);
|
||||||
|
|
||||||
|
const donutGradient = (() => {
|
||||||
|
let cursor = 0;
|
||||||
|
const segments = categoryData.map((item) => {
|
||||||
|
const start = cursor;
|
||||||
|
const end = cursor + (item.value / totalExpenses) * 100;
|
||||||
|
cursor = end;
|
||||||
|
return `${item.color} ${start}% ${end}%`;
|
||||||
|
});
|
||||||
|
return `conic-gradient(${segments.join(', ')})`;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4">
|
||||||
|
<div class="mb-3 flex h-9 w-9 items-center justify-center rounded-[11px] bg-emerald-500/10">
|
||||||
|
<TrendingUp class="h-[18px] w-[18px] text-emerald-400" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<p class="mb-1 text-[12px] font-medium text-zinc-500">Avg. Income</p>
|
||||||
|
<p class="text-[20px] font-bold text-white">$8,107</p>
|
||||||
|
<p class="mt-1 text-[11px] font-medium text-emerald-400">+5.2% growth</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4">
|
||||||
|
<div class="mb-3 flex h-9 w-9 items-center justify-center rounded-[11px] bg-rose-500/10">
|
||||||
|
<BarChart3 class="h-[18px] w-[18px] text-rose-400" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<p class="mb-1 text-[12px] font-medium text-zinc-500">Avg. Expenses</p>
|
||||||
|
<p class="text-[20px] font-bold text-white">$4,603</p>
|
||||||
|
<p class="mt-1 text-[11px] font-medium text-rose-400">-12% decrease</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5">
|
||||||
|
<div class="mb-4 flex items-center gap-2">
|
||||||
|
<div class="flex h-9 w-9 items-center justify-center rounded-[11px] bg-purple-500/10">
|
||||||
|
<PieChart class="h-[18px] w-[18px] text-purple-400" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-[15px] font-semibold text-white">Spending by Category</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 flex h-48 items-center justify-center">
|
||||||
|
<div class="relative h-40 w-40 rounded-full" :style="{ background: donutGradient }">
|
||||||
|
<div class="absolute inset-[22%] rounded-full bg-[#16161F]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
<div v-for="category in categoryData" :key="category.name" class="flex items-center gap-3">
|
||||||
|
<div class="h-3 w-3 flex-shrink-0 rounded-full" :style="{ backgroundColor: category.color }" />
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="mb-1 flex items-center justify-between">
|
||||||
|
<span class="text-[13px] font-medium text-white">{{ category.name }}</span>
|
||||||
|
<span class="text-[12px] font-semibold text-zinc-400">${{ category.value }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 overflow-hidden rounded-full bg-white/[0.05]">
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full"
|
||||||
|
:style="{
|
||||||
|
width: `${((category.value / totalExpenses) * 100).toFixed(1)}%`,
|
||||||
|
backgroundColor: category.color,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="w-10 flex-shrink-0 text-right text-[12px] font-medium text-zinc-600">
|
||||||
|
{{ ((category.value / totalExpenses) * 100).toFixed(1) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5">
|
||||||
|
<div class="mb-4 flex items-center gap-2">
|
||||||
|
<div class="flex h-9 w-9 items-center justify-center rounded-[11px] bg-blue-500/10">
|
||||||
|
<Calendar class="h-[18px] w-[18px] text-blue-400" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-[15px] font-semibold text-white">Last 6 Months</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="month in monthlyData" :key="month.month" class="flex items-center gap-3 rounded-[12px] bg-white/[0.02] p-3">
|
||||||
|
<div class="w-12 text-center">
|
||||||
|
<p class="text-[13px] font-semibold text-white">{{ month.month }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-1.5 flex items-center justify-between">
|
||||||
|
<span class="text-[11px] text-zinc-500">Income</span>
|
||||||
|
<span class="text-[12px] font-semibold text-emerald-400">${{ month.income }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-[11px] text-zinc-500">Expenses</span>
|
||||||
|
<span class="text-[12px] font-semibold text-rose-400">${{ month.expenses }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-[14px] font-bold text-white">${{ month.income - month.expenses }}</p>
|
||||||
|
<p class="text-[11px] text-zinc-600">{{ (((month.income - month.expenses) / month.income) * 100).toFixed(0) }}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'lucide-vue-next';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative overflow-hidden rounded-[20px] bg-gradient-to-br from-purple-600 via-purple-500 to-blue-600 p-6 shadow-[0_8px_32px_rgba(139,92,246,0.3)]">
|
||||||
|
<div class="absolute -top-20 -right-20 h-40 w-40 rounded-full bg-blue-400 opacity-20 blur-3xl" />
|
||||||
|
<div class="absolute -bottom-20 -left-20 h-40 w-40 rounded-full bg-purple-400 opacity-20 blur-3xl" />
|
||||||
|
|
||||||
|
<div class="relative z-10">
|
||||||
|
<div class="mb-6 flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="mb-2 text-[13px] font-medium text-purple-100/70">Total Balance</p>
|
||||||
|
<h2 class="text-[40px] font-bold leading-none tracking-tight text-white">$24,850</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5 rounded-full border border-white/10 bg-white/15 px-3 py-1.5 backdrop-blur-sm">
|
||||||
|
<TrendingUp class="h-3.5 w-3.5 text-emerald-300" :stroke-width="2.5" />
|
||||||
|
<span class="text-[13px] font-semibold text-white">+12.5%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="rounded-[16px] border border-white/10 bg-white/10 p-4 backdrop-blur-sm">
|
||||||
|
<div class="mb-2 flex items-center gap-1.5">
|
||||||
|
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-400/20">
|
||||||
|
<ArrowUpRight class="h-3.5 w-3.5 text-emerald-400" :stroke-width="2.5" />
|
||||||
|
</div>
|
||||||
|
<p class="text-[12px] font-medium text-white/70">Income</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-[20px] font-bold text-white">$8,240</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-[16px] border border-white/10 bg-white/10 p-4 backdrop-blur-sm">
|
||||||
|
<div class="mb-2 flex items-center gap-1.5">
|
||||||
|
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-rose-400/20">
|
||||||
|
<ArrowDownRight class="h-3.5 w-3.5 text-rose-400" :stroke-width="2.5" />
|
||||||
|
</div>
|
||||||
|
<p class="text-[12px] font-medium text-white/70">Expenses</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-[20px] font-bold text-white">$3,120</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Briefcase,
|
||||||
|
Car,
|
||||||
|
ChevronRight,
|
||||||
|
Coffee,
|
||||||
|
Film,
|
||||||
|
Gift,
|
||||||
|
Heart,
|
||||||
|
Home,
|
||||||
|
ShoppingBag,
|
||||||
|
Utensils,
|
||||||
|
Zap,
|
||||||
|
} from 'lucide-vue-next';
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ name: 'Food & Dining', icon: Utensils, color: 'orange', spent: 850, budget: 1200 },
|
||||||
|
{ name: 'Shopping', icon: ShoppingBag, color: 'emerald', spent: 680, budget: 800 },
|
||||||
|
{ name: 'Transport', icon: Car, color: 'red', spent: 420, budget: 500 },
|
||||||
|
{ name: 'Housing', icon: Home, color: 'indigo', spent: 1250, budget: 1250 },
|
||||||
|
{ name: 'Entertainment', icon: Film, color: 'purple', spent: 215, budget: 300 },
|
||||||
|
{ name: 'Coffee', icon: Coffee, color: 'amber', spent: 145, budget: 150 },
|
||||||
|
{ name: 'Bills & Utilities', icon: Zap, color: 'yellow', spent: 380, budget: 400 },
|
||||||
|
{ name: 'Work', icon: Briefcase, color: 'blue', spent: 120, budget: 200 },
|
||||||
|
{ name: 'Gifts & Donations', icon: Heart, color: 'pink', spent: 95, budget: 150 },
|
||||||
|
{ name: 'Others', icon: Gift, color: 'zinc', spent: 65, budget: 100 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const colorMap = {
|
||||||
|
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400', bar: 'bg-orange-500' },
|
||||||
|
emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400', bar: 'bg-emerald-500' },
|
||||||
|
red: { bg: 'bg-red-500/10', text: 'text-red-400', bar: 'bg-red-500' },
|
||||||
|
indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400', bar: 'bg-indigo-500' },
|
||||||
|
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400', bar: 'bg-purple-500' },
|
||||||
|
amber: { bg: 'bg-amber-500/10', text: 'text-amber-400', bar: 'bg-amber-500' },
|
||||||
|
yellow: { bg: 'bg-yellow-500/10', text: 'text-yellow-400', bar: 'bg-yellow-500' },
|
||||||
|
blue: { bg: 'bg-blue-500/10', text: 'text-blue-400', bar: 'bg-blue-500' },
|
||||||
|
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400', bar: 'bg-pink-500' },
|
||||||
|
zinc: { bg: 'bg-zinc-500/10', text: 'text-zinc-400', bar: 'bg-zinc-500' },
|
||||||
|
} as const;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
<div
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.name"
|
||||||
|
class="group cursor-pointer rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||||
|
>
|
||||||
|
<div class="mb-3 flex items-center gap-3">
|
||||||
|
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', colorMap[category.color].bg]">
|
||||||
|
<component :is="category.icon" :class="['h-[19px] w-[19px]', colorMap[category.color].text]" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<h4 class="mb-0.5 text-[14px] font-semibold text-white">{{ category.name }}</h4>
|
||||||
|
<p class="text-[12px] text-zinc-500">${{ category.spent }} of ${{ category.budget }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<p :class="['text-[14px] font-bold', category.spent > category.budget ? 'text-rose-400' : 'text-emerald-400']">
|
||||||
|
{{ category.spent > category.budget ? '+' : '' }}${{ Math.abs(category.budget - category.spent) }}
|
||||||
|
</p>
|
||||||
|
<p class="text-[11px] text-zinc-600">{{ ((category.spent / category.budget) * 100).toFixed(0) }}%</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChevronRight class="h-5 w-5 text-zinc-700 transition-colors group-hover:text-zinc-500" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-2 overflow-hidden rounded-full bg-white/[0.05]">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'h-full rounded-full',
|
||||||
|
category.spent > category.budget ? 'bg-rose-500' : colorMap[category.color].bar,
|
||||||
|
]"
|
||||||
|
:style="{ width: `${Math.min((category.spent / category.budget) * 100, 100)}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { Eye, EyeOff, TrendingUp } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
const isVisible = ref(true);
|
||||||
|
const chartData = [20000, 21500, 20800, 23200, 22500, 24850];
|
||||||
|
|
||||||
|
const polylinePoints = computed(() => {
|
||||||
|
const max = Math.max(...chartData);
|
||||||
|
const min = Math.min(...chartData);
|
||||||
|
const width = 96;
|
||||||
|
const height = 64;
|
||||||
|
|
||||||
|
return chartData
|
||||||
|
.map((value, index) => {
|
||||||
|
const x = (index / (chartData.length - 1)) * width;
|
||||||
|
const y = height - ((value - min) / (max - min || 1)) * (height - 6) - 3;
|
||||||
|
return `${x},${y}`;
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative overflow-hidden rounded-[20px] bg-gradient-to-br from-purple-600 via-purple-500 to-blue-600 p-6 shadow-[0_8px_32px_rgba(139,92,246,0.3)]">
|
||||||
|
<div class="absolute -top-20 -right-20 h-40 w-40 rounded-full bg-blue-400 opacity-20 blur-3xl" />
|
||||||
|
<div class="absolute -bottom-10 -left-10 h-32 w-32 rounded-full bg-purple-400 opacity-20 blur-3xl" />
|
||||||
|
|
||||||
|
<div class="relative z-10">
|
||||||
|
<div class="mb-6 flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="mb-2 flex items-center gap-2">
|
||||||
|
<p class="text-[13px] font-medium text-purple-100/70">Total Balance</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="isVisible = !isVisible"
|
||||||
|
class="flex h-6 w-6 items-center justify-center rounded-lg bg-white/10 transition-colors hover:bg-white/20"
|
||||||
|
>
|
||||||
|
<Eye v-if="isVisible" class="h-3.5 w-3.5 text-white" :stroke-width="2" />
|
||||||
|
<EyeOff v-else class="h-3.5 w-3.5 text-white" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mb-1 text-[36px] font-bold leading-none tracking-tight text-white">
|
||||||
|
{{ isVisible ? '$24,850.00' : '••••••' }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<div class="flex items-center gap-1 rounded-md bg-emerald-400/20 px-2 py-0.5">
|
||||||
|
<TrendingUp class="h-3 w-3 text-emerald-300" :stroke-width="2.5" />
|
||||||
|
<span class="text-[12px] font-semibold text-emerald-300">+12.5%</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[12px] text-purple-200/60">vs last month</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ml-2 h-16 w-24">
|
||||||
|
<svg viewBox="0 0 96 64" class="h-full w-full" fill="none">
|
||||||
|
<polyline :points="polylinePoints" stroke="#ffffff" stroke-width="2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="rounded-[14px] border border-white/10 bg-white/10 p-3 backdrop-blur-sm">
|
||||||
|
<p class="mb-1 text-[11px] font-medium text-white/60">This Month</p>
|
||||||
|
<p class="text-[18px] font-bold text-white">$8,240</p>
|
||||||
|
<p class="mt-0.5 text-[11px] font-medium text-emerald-300">+18% income</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-[14px] border border-white/10 bg-white/10 p-3 backdrop-blur-sm">
|
||||||
|
<p class="mb-1 text-[11px] font-medium text-white/60">Expenses</p>
|
||||||
|
<p class="text-[18px] font-bold text-white">$3,120</p>
|
||||||
|
<p class="mt-0.5 text-[11px] font-medium text-rose-300">-8% saved</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Bell, Plus, Settings, Wallet as WalletIcon } from 'lucide-vue-next';
|
||||||
|
import FinanceBalanceCard from './FinanceBalanceCard.vue';
|
||||||
|
import TransactionsList from './TransactionsList.vue';
|
||||||
|
import AnalyticsView from './AnalyticsView.vue';
|
||||||
|
import CategoriesView from './CategoriesView.vue';
|
||||||
|
import Navigation from './Navigation.vue';
|
||||||
|
|
||||||
|
type Tab = 'transactions' | 'analytics' | 'categories';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
navigate: [screen: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const activeTab = ref<Tab>('transactions');
|
||||||
|
|
||||||
|
const tabs: Array<{ id: Tab; label: string }> = [
|
||||||
|
{ id: 'transactions', label: 'Transactions' },
|
||||||
|
{ id: 'analytics', label: 'Analytics' },
|
||||||
|
{ id: 'categories', label: 'Categories' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-[#0A0A0F] dark">
|
||||||
|
<div class="mx-auto flex min-h-screen max-w-md flex-col relative">
|
||||||
|
<header class="flex items-center justify-between px-5 pt-6 pb-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-11 w-11 items-center justify-center rounded-[16px] bg-gradient-to-br from-purple-500 to-blue-600 shadow-lg shadow-purple-500/20">
|
||||||
|
<WalletIcon class="h-5 w-5 text-white" :stroke-width="2.5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">Family budget</p>
|
||||||
|
<h1 class="text-[17px] font-semibold tracking-tight text-white">Finance</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="button" class="flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/5 bg-[#1A1A24] transition-colors hover:bg-[#222230]">
|
||||||
|
<Bell class="h-[18px] w-[18px] text-zinc-400" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/5 bg-[#1A1A24] transition-colors hover:bg-[#222230]"
|
||||||
|
@click="emit('navigate', 'settings')"
|
||||||
|
>
|
||||||
|
<Settings class="h-[18px] w-[18px] text-zinc-400" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 overflow-y-auto px-5 pb-24">
|
||||||
|
<div class="mb-5">
|
||||||
|
<FinanceBalanceCard />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<div class="flex items-center gap-2 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-1.5">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.id"
|
||||||
|
type="button"
|
||||||
|
@click="activeTab = tab.id"
|
||||||
|
class="relative flex-1 rounded-[12px] px-4 py-2.5 text-[13px] font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="activeTab === tab.id"
|
||||||
|
class="absolute inset-0 rounded-[12px] border border-purple-500/20 bg-gradient-to-br from-purple-500/15 to-blue-500/15"
|
||||||
|
/>
|
||||||
|
<span :class="['relative z-10', activeTab === tab.id ? 'text-purple-400' : 'text-zinc-500']">
|
||||||
|
{{ tab.label }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TransactionsList v-if="activeTab === 'transactions'" />
|
||||||
|
<AnalyticsView v-else-if="activeTab === 'analytics'" />
|
||||||
|
<CategoriesView v-else />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="group fixed bottom-24 right-6 z-10 flex h-14 w-14 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-blue-600 shadow-[0_8px_32px_rgba(139,92,246,0.4)] transition-all hover:shadow-[0_12px_40px_rgba(139,92,246,0.6)] active:scale-95"
|
||||||
|
>
|
||||||
|
<Plus class="h-6 w-6 text-white transition-transform duration-300 group-hover:rotate-90" :stroke-width="2.5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Navigation active-screen="finance" @navigate="emit('navigate', $event)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Bell, Settings, User } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
navigate: [screen: string];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="flex items-center justify-between px-5 pt-6 pb-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-11 w-11 items-center justify-center rounded-[16px] bg-gradient-to-br from-purple-500 to-blue-600 shadow-lg shadow-purple-500/20">
|
||||||
|
<User class="h-5 w-5 text-white" :stroke-width="2.5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">Good evening</p>
|
||||||
|
<h1 class="text-[17px] font-semibold tracking-tight text-white">Anderson Family</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="button" class="flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/5 bg-[#1A1A24] transition-colors hover:bg-[#222230]">
|
||||||
|
<Bell class="h-[18px] w-[18px] text-zinc-400" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/5 bg-[#1A1A24] transition-colors hover:bg-[#222230]"
|
||||||
|
@click="emit('navigate', 'settings')"
|
||||||
|
>
|
||||||
|
<Settings class="h-[18px] w-[18px] text-zinc-400" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import viteLogo from '../assets/vite.svg'
|
|
||||||
import heroImg from '../assets/hero.png'
|
|
||||||
import vueLogo from '../assets/vue.svg'
|
|
||||||
|
|
||||||
const count = ref(0)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section id="center">
|
|
||||||
<div class="hero">
|
|
||||||
<img :src="heroImg" class="base" width="170" height="179" alt="" />
|
|
||||||
<img :src="vueLogo" class="framework" alt="Vue logo" />
|
|
||||||
<img :src="viteLogo" class="vite" alt="Vite logo" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1>Get started</h1>
|
|
||||||
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
|
|
||||||
</div>
|
|
||||||
<button class="counter" @click="count++">Count is {{ count }}</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="ticks"></div>
|
|
||||||
|
|
||||||
<section id="next-steps">
|
|
||||||
<div id="docs">
|
|
||||||
<svg class="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#documentation-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Documentation</h2>
|
|
||||||
<p>Your questions, answered</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://vite.dev/" target="_blank">
|
|
||||||
<img class="logo" :src="viteLogo" alt="" />
|
|
||||||
Explore Vite
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://vuejs.org/" target="_blank">
|
|
||||||
<img class="button-icon" :src="vueLogo" alt="" />
|
|
||||||
Learn more
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="social">
|
|
||||||
<svg class="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#social-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Connect with us</h2>
|
|
||||||
<p>Join the Vite community</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
|
||||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#github-icon"></use>
|
|
||||||
</svg>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://chat.vite.dev/" target="_blank">
|
|
||||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#discord-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://x.com/vite_js" target="_blank">
|
|
||||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#x-icon"></use>
|
|
||||||
</svg>
|
|
||||||
X.com
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
|
||||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#bluesky-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Bluesky
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="ticks"></div>
|
|
||||||
<section id="spacer"></section>
|
|
||||||
</template>
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {Calendar, Heart, Home, Sparkles, Wallet} from 'lucide-vue-next';
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
icon: unknown;
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
activeScreen: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
navigate: [screen: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const navItems: NavItem[] = [
|
||||||
|
{ icon: Calendar, label: 'Calendar', id: 'calendar' },
|
||||||
|
{ icon: Wallet, label: 'Finance', id: 'finance' },
|
||||||
|
{ icon: Home, label: 'Home', id: 'home' },
|
||||||
|
{ icon: Heart, label: 'Votes', id: 'votes' },
|
||||||
|
{ icon: Sparkles, label: 'Intimacy', id: 'intimacy' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="pointer-events-none fixed bottom-0 left-0 right-0 mx-auto max-w-md px-5 pb-5">
|
||||||
|
<div class="pointer-events-auto rounded-[20px] border border-white/[0.08] bg-[#1A1A24]/95 px-2 py-3 shadow-[0_8px_32px_rgba(0,0,0,0.6)] backdrop-blur-xl">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
v-for="item in navItems"
|
||||||
|
:key="item.id"
|
||||||
|
type="button"
|
||||||
|
@click="emit('navigate', item.id)"
|
||||||
|
:class="[
|
||||||
|
'relative flex flex-col items-center gap-1.5 py-2 transition-all',
|
||||||
|
item.id === 'home' ? 'px-5' : 'px-3',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="props.activeScreen === item.id"
|
||||||
|
class="absolute inset-0 rounded-[14px] border border-purple-500/20 bg-gradient-to-br from-purple-500/15 to-blue-500/15"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="item.id === 'home'"
|
||||||
|
:class="[
|
||||||
|
'relative z-10 flex h-12 w-12 items-center justify-center rounded-full transition-all',
|
||||||
|
props.activeScreen === item.id
|
||||||
|
? 'bg-gradient-to-br from-purple-500 to-blue-600 shadow-lg shadow-purple-500/30'
|
||||||
|
: 'bg-zinc-800',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="item.icon"
|
||||||
|
:class="[
|
||||||
|
'h-6 w-6',
|
||||||
|
props.activeScreen === item.id ? 'text-white' : 'text-zinc-400',
|
||||||
|
]"
|
||||||
|
:stroke-width="props.activeScreen === item.id ? 2.5 : 2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<component
|
||||||
|
:is="item.icon"
|
||||||
|
:class="[
|
||||||
|
'relative z-10 h-5 w-5 transition-colors',
|
||||||
|
props.activeScreen === item.id ? 'text-purple-400' : 'text-zinc-500',
|
||||||
|
]"
|
||||||
|
:stroke-width="props.activeScreen === item.id ? 2.5 : 2"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'relative z-10 text-[10px] font-medium transition-colors',
|
||||||
|
props.activeScreen === item.id ? 'text-purple-400' : 'text-zinc-600',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Activity, CreditCard, Popcorn, ShoppingCart, Users } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
const activities = [
|
||||||
|
{
|
||||||
|
icon: ShoppingCart,
|
||||||
|
title: 'Grocery Shopping',
|
||||||
|
subtitle: 'Sarah • $124.50',
|
||||||
|
time: '2h',
|
||||||
|
bgColor: 'bg-emerald-500/10',
|
||||||
|
textColor: 'text-emerald-400',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Popcorn,
|
||||||
|
title: 'Movie Vote',
|
||||||
|
subtitle: '3 votes for "Dune 2"',
|
||||||
|
time: '5h',
|
||||||
|
bgColor: 'bg-purple-500/10',
|
||||||
|
textColor: 'text-purple-400',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: CreditCard,
|
||||||
|
title: 'Subscription',
|
||||||
|
subtitle: 'Netflix • $15.99',
|
||||||
|
time: '1d',
|
||||||
|
bgColor: 'bg-rose-500/10',
|
||||||
|
textColor: 'text-rose-400',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Users,
|
||||||
|
title: 'New Event',
|
||||||
|
subtitle: 'Family BBQ Saturday',
|
||||||
|
time: '2d',
|
||||||
|
bgColor: 'bg-blue-500/10',
|
||||||
|
textColor: 'text-blue-400',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5 shadow-[0_4px_20px_rgba(0,0,0,0.4)]">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex h-9 w-9 items-center justify-center rounded-[12px] bg-blue-500/10">
|
||||||
|
<Activity class="h-[18px] w-[18px] text-blue-400" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-[15px] font-semibold text-white">Activity</h3>
|
||||||
|
</div>
|
||||||
|
<button class="text-[12px] font-medium text-purple-400 transition-colors hover:text-purple-300">
|
||||||
|
View all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="activity in activities"
|
||||||
|
:key="activity.title"
|
||||||
|
class="group flex cursor-pointer items-center gap-3 rounded-[12px] p-3 transition-all hover:bg-white/[0.03]"
|
||||||
|
>
|
||||||
|
<div :class="['flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-[12px] transition-transform group-hover:scale-110', activity.bgColor]">
|
||||||
|
<component :is="activity.icon" :class="['h-[18px] w-[18px]', activity.textColor]" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="mb-0.5 text-[14px] font-medium text-white">{{ activity.title }}</p>
|
||||||
|
<p class="truncate text-[12px] text-zinc-500">{{ activity.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<span class="flex-shrink-0 text-[12px] font-medium text-zinc-600">{{ activity.time }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, type Component } from 'vue'
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
Calendar,
|
||||||
|
ChevronRight,
|
||||||
|
Crown,
|
||||||
|
Eye,
|
||||||
|
Film,
|
||||||
|
Heart,
|
||||||
|
LayoutGrid,
|
||||||
|
Lock,
|
||||||
|
LogOut,
|
||||||
|
Palette,
|
||||||
|
X,
|
||||||
|
Settings as SettingsIcon,
|
||||||
|
Shield,
|
||||||
|
Users,
|
||||||
|
Utensils,
|
||||||
|
Wallet,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
|
interface ModuleItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: Component;
|
||||||
|
color: keyof typeof moduleColorMap;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingItemData {
|
||||||
|
icon: Component;
|
||||||
|
color: keyof typeof itemColorMap;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
badge?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
onNavigate?: (screen: string) => void;
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
navigate: [screen: string];
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const itemColorMap = {
|
||||||
|
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
|
||||||
|
blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
|
||||||
|
amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
|
||||||
|
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
|
||||||
|
emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
|
||||||
|
indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
|
||||||
|
cyan: { bg: 'bg-cyan-500/10', text: 'text-cyan-400' },
|
||||||
|
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
|
||||||
|
zinc: { bg: 'bg-zinc-500/10', text: 'text-zinc-400' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const moduleColorMap = {
|
||||||
|
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
|
||||||
|
blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
|
||||||
|
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
|
||||||
|
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
|
||||||
|
red: { bg: 'bg-red-500/10', text: 'text-red-400' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const familyItems: SettingItemData[] = [
|
||||||
|
{
|
||||||
|
icon: Users,
|
||||||
|
color: 'blue',
|
||||||
|
label: 'Family Members',
|
||||||
|
description: 'Manage family access',
|
||||||
|
badge: '4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Crown,
|
||||||
|
color: 'amber',
|
||||||
|
label: 'Roles & Permissions',
|
||||||
|
description: 'Control what members can do',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const customizationItems: SettingItemData[] = [
|
||||||
|
{
|
||||||
|
icon: LayoutGrid,
|
||||||
|
color: 'purple',
|
||||||
|
label: 'Dashboard Widgets',
|
||||||
|
description: 'Customize your home screen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Palette,
|
||||||
|
color: 'pink',
|
||||||
|
label: 'Theme & Appearance',
|
||||||
|
description: 'Dark mode, colors, fonts',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const privacyItems: SettingItemData[] = [
|
||||||
|
{
|
||||||
|
icon: Lock,
|
||||||
|
color: 'emerald',
|
||||||
|
label: 'Privacy Settings',
|
||||||
|
description: 'Control data sharing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Eye,
|
||||||
|
color: 'indigo',
|
||||||
|
label: 'Visibility Controls',
|
||||||
|
description: 'Who can see what',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
color: 'cyan',
|
||||||
|
label: 'Security',
|
||||||
|
description: 'Password, 2FA, biometrics',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const advancedItems: SettingItemData[] = [
|
||||||
|
{
|
||||||
|
icon: Bell,
|
||||||
|
color: 'orange',
|
||||||
|
label: 'Notifications',
|
||||||
|
description: 'Push, email, SMS settings',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SettingsIcon,
|
||||||
|
color: 'zinc',
|
||||||
|
label: 'App Preferences',
|
||||||
|
description: 'Language, timezone, units',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const modules = reactive<ModuleItem[]>([
|
||||||
|
{ id: 'finance', name: 'Finance Tracking', icon: Wallet, color: 'purple', enabled: true },
|
||||||
|
{ id: 'calendar', name: 'Family Calendar', icon: Calendar, color: 'blue', enabled: true },
|
||||||
|
{ id: 'food', name: 'Food Voting', icon: Utensils, color: 'orange', enabled: true },
|
||||||
|
{ id: 'movies', name: 'Movie Voting', icon: Film, color: 'pink', enabled: true },
|
||||||
|
{ id: 'activities', name: 'Activity Feed', icon: Heart, color: 'red', enabled: true },
|
||||||
|
])
|
||||||
|
|
||||||
|
function toggleModule(id: string) {
|
||||||
|
const moduleItem = modules.find((item) => item.id === id)
|
||||||
|
if (moduleItem) {
|
||||||
|
moduleItem.enabled = !moduleItem.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigate(screen: string) {
|
||||||
|
emit('navigate', screen)
|
||||||
|
props.onNavigate?.(screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
function animationDelay(index: number, base = 0.1) {
|
||||||
|
return { animationDelay: `${base + index * 0.05}s` }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-[#0A0A0F] dark">
|
||||||
|
<div class="relative mx-auto flex min-h-screen max-w-md flex-col">
|
||||||
|
<header class="flex items-center justify-between px-5 pt-6 pb-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-11 w-11 items-center justify-center rounded-[16px] bg-gradient-to-br from-purple-500 to-blue-600 shadow-lg shadow-purple-500/20">
|
||||||
|
<SettingsIcon class="h-5 w-5 text-white" :stroke-width="2.5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">Manage your family hub</p>
|
||||||
|
<h1 class="text-[17px] font-semibold tracking-tight text-white">Settings</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/5 bg-[#1A1A24] transition-colors hover:bg-[#222230]"
|
||||||
|
@click="navigate('close-settings')"
|
||||||
|
>
|
||||||
|
<X class="h-[18px] w-[18px] text-zinc-400" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 overflow-y-auto px-5 pb-28">
|
||||||
|
<div class="space-y-5">
|
||||||
|
<section class="screen-enter relative overflow-hidden rounded-[20px] border border-purple-500/20 bg-gradient-to-br from-purple-600/20 via-purple-500/10 to-blue-600/20 p-5" :style="animationDelay(0)">
|
||||||
|
<div class="absolute -top-10 -right-10 h-32 w-32 rounded-full bg-purple-500 opacity-10 blur-3xl" />
|
||||||
|
<div class="relative z-10 flex items-center gap-4">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-blue-600 text-[22px] font-bold text-white">
|
||||||
|
JS
|
||||||
|
</div>
|
||||||
|
<div class="absolute -right-1 -bottom-1 flex h-6 w-6 items-center justify-center rounded-full border-2 border-[#0A0A0F] bg-emerald-500">
|
||||||
|
<Crown class="h-3 w-3 text-white" :stroke-width="2.5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<h2 class="mb-0.5 text-[18px] font-bold text-white">John Smith</h2>
|
||||||
|
<p class="mb-1 text-[13px] font-medium text-purple-300">Family Admin</p>
|
||||||
|
<p class="text-[12px] text-zinc-500">john.smith@email.com</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="flex h-9 w-9 items-center justify-center rounded-[12px] bg-white/10 transition-colors hover:bg-white/20">
|
||||||
|
<ChevronRight class="h-[18px] w-[18px] text-white" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screen-enter" :style="animationDelay(1)">
|
||||||
|
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
|
||||||
|
Family
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<button
|
||||||
|
v-for="item in familyItems"
|
||||||
|
:key="item.label"
|
||||||
|
type="button"
|
||||||
|
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||||
|
>
|
||||||
|
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', itemColorMap[item.color].bg]">
|
||||||
|
<component :is="item.icon" :class="['h-[19px] w-[19px]', itemColorMap[item.color].text]" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1 text-left">
|
||||||
|
<p class="mb-0.5 text-[14px] font-semibold text-white">{{ item.label }}</p>
|
||||||
|
<p class="text-[12px] text-zinc-500">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.badge" class="rounded-full border border-purple-500/20 bg-purple-500/15 px-2.5 py-1">
|
||||||
|
<span class="text-[12px] font-semibold text-purple-400">{{ item.badge }}</span>
|
||||||
|
</div>
|
||||||
|
<ChevronRight class="h-5 w-5 flex-shrink-0 text-zinc-700 transition-colors group-hover:text-zinc-500" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screen-enter" :style="animationDelay(2)">
|
||||||
|
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
|
||||||
|
Customization
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<button
|
||||||
|
v-for="item in customizationItems"
|
||||||
|
:key="item.label"
|
||||||
|
type="button"
|
||||||
|
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||||
|
>
|
||||||
|
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', itemColorMap[item.color].bg]">
|
||||||
|
<component :is="item.icon" :class="['h-[19px] w-[19px]', itemColorMap[item.color].text]" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1 text-left">
|
||||||
|
<p class="mb-0.5 text-[14px] font-semibold text-white">{{ item.label }}</p>
|
||||||
|
<p class="text-[12px] text-zinc-500">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
<ChevronRight class="h-5 w-5 flex-shrink-0 text-zinc-700 transition-colors group-hover:text-zinc-500" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screen-enter" :style="animationDelay(3)">
|
||||||
|
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
|
||||||
|
Active Modules
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="(module, index) in modules"
|
||||||
|
:key="module.id"
|
||||||
|
class="module-enter flex items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4"
|
||||||
|
:style="animationDelay(index, 0.25)"
|
||||||
|
>
|
||||||
|
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px]', moduleColorMap[module.color].bg]">
|
||||||
|
<component :is="module.icon" :class="['h-[19px] w-[19px]', moduleColorMap[module.color].text]" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="text-[14px] font-semibold text-white">{{ module.name }}</p>
|
||||||
|
<p :class="['text-[12px]', module.enabled ? 'text-emerald-400' : 'text-zinc-600']">
|
||||||
|
{{ module.enabled ? 'Active' : 'Disabled' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:aria-pressed="module.enabled"
|
||||||
|
:class="[
|
||||||
|
'relative h-7 w-12 rounded-full transition-all',
|
||||||
|
module.enabled ? 'bg-gradient-to-r from-purple-500 to-blue-600' : 'bg-zinc-800',
|
||||||
|
]"
|
||||||
|
@click="toggleModule(module.id)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'absolute top-1 left-1 h-5 w-5 rounded-full bg-white shadow-lg transition-transform duration-300',
|
||||||
|
module.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screen-enter" :style="animationDelay(4)">
|
||||||
|
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
|
||||||
|
Privacy & Security
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<button
|
||||||
|
v-for="item in privacyItems"
|
||||||
|
:key="item.label"
|
||||||
|
type="button"
|
||||||
|
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||||
|
>
|
||||||
|
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', itemColorMap[item.color].bg]">
|
||||||
|
<component :is="item.icon" :class="['h-[19px] w-[19px]', itemColorMap[item.color].text]" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1 text-left">
|
||||||
|
<p class="mb-0.5 text-[14px] font-semibold text-white">{{ item.label }}</p>
|
||||||
|
<p class="text-[12px] text-zinc-500">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
<ChevronRight class="h-5 w-5 flex-shrink-0 text-zinc-700 transition-colors group-hover:text-zinc-500" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screen-enter" :style="animationDelay(5)">
|
||||||
|
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
|
||||||
|
Advanced
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<button
|
||||||
|
v-for="item in advancedItems"
|
||||||
|
:key="item.label"
|
||||||
|
type="button"
|
||||||
|
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||||
|
>
|
||||||
|
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', itemColorMap[item.color].bg]">
|
||||||
|
<component :is="item.icon" :class="['h-[19px] w-[19px]', itemColorMap[item.color].text]" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1 text-left">
|
||||||
|
<p class="mb-0.5 text-[14px] font-semibold text-white">{{ item.label }}</p>
|
||||||
|
<p class="text-[12px] text-zinc-500">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
<ChevronRight class="h-5 w-5 flex-shrink-0 text-zinc-700 transition-colors group-hover:text-zinc-500" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="screen-enter group flex w-full items-center justify-center gap-2 rounded-[16px] border border-red-500/20 bg-red-500/10 p-4 transition-all hover:border-red-500/30 hover:bg-red-500/20"
|
||||||
|
:style="animationDelay(6)"
|
||||||
|
>
|
||||||
|
<LogOut class="h-[18px] w-[18px] text-red-400 transition-colors group-hover:text-red-300" :stroke-width="2" />
|
||||||
|
<span class="text-[14px] font-semibold text-red-400 transition-colors group-hover:text-red-300">
|
||||||
|
Sign Out
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="py-4 text-center">
|
||||||
|
<p class="text-[12px] text-zinc-700">Family OS v2.4.0</p>
|
||||||
|
<p class="mt-1 text-[11px] text-zinc-800">© 2026 Family Hub Inc.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.screen-enter,
|
||||||
|
.module-enter {
|
||||||
|
animation: screen-enter 0.45s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes screen-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { Clock, Heart, MapPin, Sparkles, Star, X } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
interface SwipeItem {
|
||||||
|
id: number;
|
||||||
|
type: 'food' | 'movie';
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
rating: number;
|
||||||
|
location?: string;
|
||||||
|
duration?: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards: SwipeItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: 'food',
|
||||||
|
title: 'Italian Restaurant',
|
||||||
|
subtitle: 'Authentic pasta & pizza',
|
||||||
|
rating: 4.8,
|
||||||
|
location: '2.3 km away',
|
||||||
|
image: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&q=80',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
type: 'movie',
|
||||||
|
title: 'Dune: Part Two',
|
||||||
|
subtitle: 'Sci-Fi Epic',
|
||||||
|
rating: 4.9,
|
||||||
|
duration: '2h 46m',
|
||||||
|
image: 'https://images.unsplash.com/photo-1536440136628-849c177e76a1?w=800&q=80',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
type: 'food',
|
||||||
|
title: 'Sushi House',
|
||||||
|
subtitle: 'Fresh Japanese cuisine',
|
||||||
|
rating: 4.7,
|
||||||
|
location: '1.8 km away',
|
||||||
|
image: 'https://images.unsplash.com/photo-1579584425555-c3ce17fd4351?w=800&q=80',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentIndex = ref(0);
|
||||||
|
const swipeDirection = ref<'left' | 'right' | null>(null);
|
||||||
|
const pointerStartX = ref<number | null>(null);
|
||||||
|
|
||||||
|
const currentCard = computed(() => cards[currentIndex.value]);
|
||||||
|
|
||||||
|
function nextCard() {
|
||||||
|
currentIndex.value = (currentIndex.value + 1) % cards.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSwipe(direction: 'left' | 'right') {
|
||||||
|
swipeDirection.value = direction;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
nextCard();
|
||||||
|
swipeDirection.value = null;
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerDown(event: PointerEvent) {
|
||||||
|
pointerStartX.value = event.clientX;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerUp(event: PointerEvent) {
|
||||||
|
if (pointerStartX.value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaX = event.clientX - pointerStartX.value;
|
||||||
|
pointerStartX.value = null;
|
||||||
|
|
||||||
|
if (deltaX > 60) {
|
||||||
|
handleSwipe('right');
|
||||||
|
} else if (deltaX < -60) {
|
||||||
|
handleSwipe('left');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5 shadow-[0_4px_20px_rgba(0,0,0,0.4)]">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex h-9 w-9 items-center justify-center rounded-[12px] border border-purple-500/20 bg-gradient-to-br from-purple-500/20 to-blue-500/20">
|
||||||
|
<Sparkles class="h-[18px] w-[18px] text-purple-400" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-[15px] font-semibold text-white">Vote Now</h3>
|
||||||
|
<p class="text-[11px] text-zinc-500">Swipe to decide</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div
|
||||||
|
v-for="(_, i) in cards"
|
||||||
|
:key="i"
|
||||||
|
:class="[
|
||||||
|
'h-1.5 rounded-full transition-all',
|
||||||
|
i === currentIndex ? 'w-6 bg-gradient-to-r from-purple-500 to-blue-500' : 'w-1.5 bg-zinc-700',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative mb-4 h-[360px]">
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 cursor-grab overflow-hidden rounded-[18px] shadow-xl active:cursor-grabbing"
|
||||||
|
:class="swipeDirection ? 'transition-all duration-200 ease-out opacity-0 scale-95' : ''"
|
||||||
|
@pointerdown="onPointerDown"
|
||||||
|
@pointerup="onPointerUp"
|
||||||
|
>
|
||||||
|
<img :src="currentCard.image" :alt="currentCard.title" class="h-full w-full object-cover" />
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent" />
|
||||||
|
|
||||||
|
<div class="absolute top-4 right-4 flex items-center gap-1.5 rounded-full border border-white/10 bg-black/60 px-3 py-1.5 backdrop-blur-md">
|
||||||
|
<Star class="h-3.5 w-3.5 fill-amber-400 text-amber-400" :stroke-width="2" />
|
||||||
|
<span class="text-[13px] font-bold text-white">{{ currentCard.rating }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-0 left-0 right-0 p-5">
|
||||||
|
<div class="mb-3">
|
||||||
|
<span class="mb-3 inline-flex items-center gap-1.5 rounded-lg border border-purple-500/20 bg-purple-500/20 px-2.5 py-1.5 text-[11px] font-semibold text-purple-300 backdrop-blur-sm">
|
||||||
|
{{ currentCard.type === 'food' ? 'Restaurant' : 'Movie' }}
|
||||||
|
</span>
|
||||||
|
<h4 class="mb-1 text-[24px] font-bold leading-tight text-white">{{ currentCard.title }}</h4>
|
||||||
|
<p class="mb-3 text-[14px] text-zinc-300">{{ currentCard.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4 text-[12px] text-zinc-400">
|
||||||
|
<div v-if="currentCard.location" class="flex items-center gap-1.5">
|
||||||
|
<MapPin class="h-3.5 w-3.5" :stroke-width="2" />
|
||||||
|
<span class="font-medium">{{ currentCard.location }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="currentCard.duration" class="flex items-center gap-1.5">
|
||||||
|
<Clock class="h-3.5 w-3.5" :stroke-width="2" />
|
||||||
|
<span class="font-medium">{{ currentCard.duration }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="swipeDirection" class="pointer-events-none absolute inset-0 z-10 flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'flex h-24 w-24 items-center justify-center rounded-full border-[3px] backdrop-blur-sm shadow-2xl',
|
||||||
|
swipeDirection === 'left' ? 'border-rose-500 bg-rose-500/20' : 'border-emerald-500 bg-emerald-500/20',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<X v-if="swipeDirection === 'left'" class="h-12 w-12 text-rose-500" :stroke-width="3" />
|
||||||
|
<Heart
|
||||||
|
v-else
|
||||||
|
class="h-12 w-12 fill-emerald-500 text-emerald-500"
|
||||||
|
:stroke-width="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleSwipe('left')"
|
||||||
|
class="group flex h-14 w-14 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.06] transition-all hover:border-rose-500/30 hover:bg-rose-500/10 active:scale-95"
|
||||||
|
>
|
||||||
|
<X class="h-6 w-6 text-zinc-400 transition-colors group-hover:text-rose-400" :stroke-width="2.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleSwipe('right')"
|
||||||
|
class="flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-blue-500 shadow-[0_8px_24px_rgba(139,92,246,0.35)] transition-all hover:from-purple-600 hover:to-blue-600 hover:shadow-[0_12px_32px_rgba(139,92,246,0.5)] active:scale-95"
|
||||||
|
>
|
||||||
|
<Heart class="h-7 w-7 text-white" :stroke-width="2.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Calendar, Clock, MapPin } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
const todayEvents = [
|
||||||
|
{
|
||||||
|
time: '6:30 PM',
|
||||||
|
title: 'Family Dinner',
|
||||||
|
location: 'Home',
|
||||||
|
colorFrom: '#a855f7',
|
||||||
|
colorTo: '#9333ea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: '8:00 PM',
|
||||||
|
title: 'Movie Night',
|
||||||
|
location: 'Living Room',
|
||||||
|
colorFrom: '#3b82f6',
|
||||||
|
colorTo: '#2563eb',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5 shadow-[0_4px_20px_rgba(0,0,0,0.4)]">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex h-9 w-9 items-center justify-center rounded-[12px] bg-purple-500/10">
|
||||||
|
<Calendar class="h-[18px] w-[18px] text-purple-400" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-[15px] font-semibold text-white">Today</h3>
|
||||||
|
<p class="text-[12px] text-zinc-500">Wednesday, April 2</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-[12px] font-medium text-zinc-600">{{ todayEvents.length }} events</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
<div
|
||||||
|
v-for="event in todayEvents"
|
||||||
|
:key="event.title"
|
||||||
|
class="group flex cursor-pointer items-center gap-3 rounded-[14px] border border-white/[0.04] bg-white/[0.02] p-3.5 transition-all hover:border-white/[0.08] hover:bg-white/[0.05]"
|
||||||
|
>
|
||||||
|
<div class="flex min-w-[52px] flex-col items-center justify-center rounded-[10px] border border-white/[0.06] bg-white/[0.04] py-2 px-2.5">
|
||||||
|
<Clock class="mb-1 h-3.5 w-3.5 text-zinc-500" :stroke-width="2" />
|
||||||
|
<span class="text-[13px] font-semibold leading-none text-white">{{ event.time }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<h4 class="mb-0.5 text-[14px] font-semibold text-white">{{ event.title }}</h4>
|
||||||
|
<div class="flex items-center gap-1 text-zinc-500">
|
||||||
|
<MapPin class="h-3 w-3" :stroke-width="2" />
|
||||||
|
<span class="text-[12px]">{{ event.location }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="h-12 w-1.5 rounded-full opacity-60 transition-opacity group-hover:opacity-100"
|
||||||
|
:style="{ background: `linear-gradient(to bottom, ${event.colorFrom}, ${event.colorTo})` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Car,
|
||||||
|
Coffee,
|
||||||
|
Film,
|
||||||
|
Heart,
|
||||||
|
Home,
|
||||||
|
ShoppingBag,
|
||||||
|
TrendingUp,
|
||||||
|
Utensils,
|
||||||
|
} from 'lucide-vue-next';
|
||||||
|
|
||||||
|
interface Transaction {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
category: string;
|
||||||
|
amount: number;
|
||||||
|
type: 'income' | 'expense';
|
||||||
|
icon: unknown;
|
||||||
|
color: keyof typeof colorMap;
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionGroup {
|
||||||
|
date: string;
|
||||||
|
total: number;
|
||||||
|
transactions: Transaction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorMap = {
|
||||||
|
emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
|
||||||
|
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
|
||||||
|
amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
|
||||||
|
blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
|
||||||
|
red: { bg: 'bg-red-500/10', text: 'text-red-400' },
|
||||||
|
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
|
||||||
|
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
|
||||||
|
indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const transactionGroups: TransactionGroup[] = [
|
||||||
|
{
|
||||||
|
date: 'Today',
|
||||||
|
total: -245.5,
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Whole Foods Market',
|
||||||
|
category: 'Groceries',
|
||||||
|
amount: -124.5,
|
||||||
|
type: 'expense',
|
||||||
|
icon: ShoppingBag,
|
||||||
|
color: 'emerald',
|
||||||
|
time: '2:30 PM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Uber Eats',
|
||||||
|
category: 'Food & Dining',
|
||||||
|
amount: -45,
|
||||||
|
type: 'expense',
|
||||||
|
icon: Utensils,
|
||||||
|
color: 'orange',
|
||||||
|
time: '12:15 PM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Starbucks',
|
||||||
|
category: 'Coffee',
|
||||||
|
amount: -12.5,
|
||||||
|
type: 'expense',
|
||||||
|
icon: Coffee,
|
||||||
|
color: 'amber',
|
||||||
|
time: '9:00 AM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
title: 'Freelance Payment',
|
||||||
|
category: 'Income',
|
||||||
|
amount: 850,
|
||||||
|
type: 'income',
|
||||||
|
icon: TrendingUp,
|
||||||
|
color: 'blue',
|
||||||
|
time: '8:00 AM',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: 'Yesterday',
|
||||||
|
total: -89.99,
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
title: 'Shell Gas Station',
|
||||||
|
category: 'Transport',
|
||||||
|
amount: -65,
|
||||||
|
type: 'expense',
|
||||||
|
icon: Car,
|
||||||
|
color: 'red',
|
||||||
|
time: '6:45 PM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
title: 'Netflix Subscription',
|
||||||
|
category: 'Entertainment',
|
||||||
|
amount: -15.99,
|
||||||
|
type: 'expense',
|
||||||
|
icon: Film,
|
||||||
|
color: 'purple',
|
||||||
|
time: '12:00 PM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
title: 'Charity Donation',
|
||||||
|
category: 'Donation',
|
||||||
|
amount: -25,
|
||||||
|
type: 'expense',
|
||||||
|
icon: Heart,
|
||||||
|
color: 'pink',
|
||||||
|
time: '10:30 AM',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: 'Apr 1',
|
||||||
|
total: -1250,
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
id: '8',
|
||||||
|
title: 'Rent Payment',
|
||||||
|
category: 'Housing',
|
||||||
|
amount: -1250,
|
||||||
|
type: 'expense',
|
||||||
|
icon: Home,
|
||||||
|
color: 'indigo',
|
||||||
|
time: '9:00 AM',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div v-for="group in transactionGroups" :key="group.date">
|
||||||
|
<div class="mb-3 flex items-center justify-between px-1">
|
||||||
|
<h3 class="text-[14px] font-semibold text-white">{{ group.date }}</h3>
|
||||||
|
<span :class="['text-[13px] font-semibold', group.total >= 0 ? 'text-emerald-400' : 'text-zinc-400']">
|
||||||
|
{{ group.total >= 0 ? '+' : '' }}${{ Math.abs(group.total).toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="transaction in group.transactions"
|
||||||
|
:key="transaction.id"
|
||||||
|
class="group flex cursor-pointer items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-3.5 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||||
|
>
|
||||||
|
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', colorMap[transaction.color].bg]">
|
||||||
|
<component :is="transaction.icon" :class="['h-[19px] w-[19px]', colorMap[transaction.color].text]" :stroke-width="2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="mb-0.5 truncate text-[14px] font-semibold text-white">{{ transaction.title }}</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-[12px] text-zinc-500">{{ transaction.category }}</span>
|
||||||
|
<span class="text-zinc-700">•</span>
|
||||||
|
<span class="text-[12px] text-zinc-600">{{ transaction.time }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<p :class="['text-[15px] font-bold', transaction.type === 'income' ? 'text-emerald-400' : 'text-white']">
|
||||||
|
{{ transaction.type === 'income' ? '+' : '-' }}${{ Math.abs(transaction.amount).toFixed(2) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue';
|
||||||
import './style.css'
|
import App from './App.vue';
|
||||||
import App from './App.vue'
|
import './styles/index.css';
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).mount('#root');
|
||||||
|
|||||||
@@ -1,296 +0,0 @@
|
|||||||
:root {
|
|
||||||
--text: #6b6375;
|
|
||||||
--text-h: #08060d;
|
|
||||||
--bg: #fff;
|
|
||||||
--border: #e5e4e7;
|
|
||||||
--code-bg: #f4f3ec;
|
|
||||||
--accent: #aa3bff;
|
|
||||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
|
||||||
--accent-border: rgba(170, 59, 255, 0.5);
|
|
||||||
--social-bg: rgba(244, 243, 236, 0.5);
|
|
||||||
--shadow:
|
|
||||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
|
||||||
|
|
||||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
--mono: ui-monospace, Consolas, monospace;
|
|
||||||
|
|
||||||
font: 18px/145% var(--sans);
|
|
||||||
letter-spacing: 0.18px;
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: var(--text);
|
|
||||||
background: var(--bg);
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--text: #9ca3af;
|
|
||||||
--text-h: #f3f4f6;
|
|
||||||
--bg: #16171d;
|
|
||||||
--border: #2e303a;
|
|
||||||
--code-bg: #1f2028;
|
|
||||||
--accent: #c084fc;
|
|
||||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
|
||||||
--accent-border: rgba(192, 132, 252, 0.5);
|
|
||||||
--social-bg: rgba(47, 48, 58, 0.5);
|
|
||||||
--shadow:
|
|
||||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#social .button-icon {
|
|
||||||
filter: invert(1) brightness(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2 {
|
|
||||||
font-family: var(--heading);
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-h);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 56px;
|
|
||||||
letter-spacing: -1.68px;
|
|
||||||
margin: 32px 0;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 36px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 118%;
|
|
||||||
letter-spacing: -0.24px;
|
|
||||||
margin: 0 0 8px;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
code,
|
|
||||||
.counter {
|
|
||||||
font-family: var(--mono);
|
|
||||||
display: inline-flex;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--text-h);
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 135%;
|
|
||||||
padding: 4px 8px;
|
|
||||||
background: var(--code-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.counter {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
color: var(--accent);
|
|
||||||
background: var(--accent-bg);
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--accent-border);
|
|
||||||
}
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid var(--accent);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.base,
|
|
||||||
.framework,
|
|
||||||
.vite {
|
|
||||||
inset-inline: 0;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.base {
|
|
||||||
width: 170px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.framework,
|
|
||||||
.vite {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.framework {
|
|
||||||
z-index: 1;
|
|
||||||
top: 34px;
|
|
||||||
height: 28px;
|
|
||||||
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
|
||||||
scale(1.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.vite {
|
|
||||||
z-index: 0;
|
|
||||||
top: 107px;
|
|
||||||
height: 26px;
|
|
||||||
width: auto;
|
|
||||||
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
|
||||||
scale(0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
width: 1126px;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
border-inline: 1px solid var(--border);
|
|
||||||
min-height: 100svh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
#center {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 25px;
|
|
||||||
place-content: center;
|
|
||||||
place-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
padding: 32px 20px 24px;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#next-steps {
|
|
||||||
display: flex;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
flex: 1 1 0;
|
|
||||||
padding: 32px;
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
padding: 24px 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#docs {
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#next-steps ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 32px 0 0;
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--text-h);
|
|
||||||
font-size: 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--social-bg);
|
|
||||||
display: flex;
|
|
||||||
padding: 6px 12px;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: box-shadow 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
.button-icon {
|
|
||||||
height: 18px;
|
|
||||||
width: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
margin-top: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
li {
|
|
||||||
flex: 1 1 calc(50% - 8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#spacer {
|
|
||||||
height: 88px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
height: 48px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ticks {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -4.5px;
|
|
||||||
border: 5px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
left: 0;
|
|
||||||
border-left-color: var(--border);
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
right: 0;
|
|
||||||
border-right-color: var(--border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@import './fonts.css';
|
||||||
|
@import './tailwind.css';
|
||||||
|
@import './theme.css';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@import 'tailwindcss' source(none);
|
||||||
|
@source '../**/*.{js,ts,jsx,tsx,vue}';
|
||||||
|
|
||||||
|
@import 'tw-animate-css';
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-size: 16px;
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: #ffffff;
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: #030213;
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--secondary: oklch(0.95 0.0058 264.53);
|
||||||
|
--secondary-foreground: #030213;
|
||||||
|
--muted: #ececf0;
|
||||||
|
--muted-foreground: #717182;
|
||||||
|
--accent: #e9ebef;
|
||||||
|
--accent-foreground: #030213;
|
||||||
|
--destructive: #d4183d;
|
||||||
|
--destructive-foreground: #ffffff;
|
||||||
|
--border: rgba(0, 0, 0, 0.1);
|
||||||
|
--input: transparent;
|
||||||
|
--input-background: #f3f3f5;
|
||||||
|
--switch-background: #cbced4;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-normal: 400;
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: #030213;
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.145 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.145 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.985 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.396 0.141 25.723);
|
||||||
|
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||||
|
--border: oklch(0.269 0 0);
|
||||||
|
--input: oklch(0.269 0 0);
|
||||||
|
--ring: oklch(0.439 0 0);
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-normal: 400;
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(0.269 0 0);
|
||||||
|
--sidebar-ring: oklch(0.439 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-input-background: var(--input-background);
|
||||||
|
--color-switch-background: var(--switch-background);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default typography styles for HTML elements (h1-h4, p, label, button, input).
|
||||||
|
* These are in @layer base, so Tailwind utility classes (like text-sm, text-lg) automatically override them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: var(--font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
"types": ["vite/client"],
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
|
||||||
}
|
|
||||||
+22
-4
@@ -1,7 +1,25 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"compilerOptions": {
|
||||||
"references": [
|
"target": "ES2022",
|
||||||
{ "path": "./tsconfig.app.json" },
|
"useDefineForClassFields": true,
|
||||||
{ "path": "./tsconfig.node.json" }
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["node"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"vite.config.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
||||||
"target": "ES2023",
|
|
||||||
"lib": ["ES2023"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"types": ["node"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
+18
-2
@@ -1,7 +1,23 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// Alias @ to the src directory
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// File types to support raw imports. Never add .css, .tsx, or .ts files to this.
|
||||||
|
assetsInclude: ['**/*.svg', '**/*.csv'],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user