Added vue frontend project, fixed swagger path
This commit is contained in:
@@ -26,7 +26,7 @@ API_PORT=8000
|
||||
API_SECRET=change-me
|
||||
|
||||
OPEN_API_ENABLED=true
|
||||
OPEN_API_ENDPOINT=/docs
|
||||
OPEN_API_ENDPOINT=/openapi
|
||||
```
|
||||
|
||||
### 2. Обязательные переменные по режимам
|
||||
@@ -49,7 +49,7 @@ OPEN_API_ENDPOINT=/docs
|
||||
- `DB_PATH=sqlite://data/app.db`
|
||||
- `API_HOST=localhost`
|
||||
- `API_PORT=8000`
|
||||
- `OPEN_API_ENDPOINT=/docs`
|
||||
- `OPEN_API_ENDPOINT=/openapi`
|
||||
|
||||
### 4. Описание переменных
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
@@ -34,9 +36,41 @@ func NewServer(cfg config.Config) *Server {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.Default()
|
||||
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")
|
||||
|
||||
@@ -68,7 +68,7 @@ func Load() (Config, error) {
|
||||
apiPort = "8000"
|
||||
}
|
||||
if openAPIEndpoint == "" {
|
||||
openAPIEndpoint = "/docs"
|
||||
openAPIEndpoint = "/openapi"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
+12
-11
@@ -1,13 +1,14 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>FamilyHub</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</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,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"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": {
|
||||
"@types/node": "^24.12.0",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vue/tsconfig": "^0.9.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.1",
|
||||
"vue-tsc": "^3.2.5"
|
||||
"@types/node": "^25.5.2",
|
||||
"@vitejs/plugin-vue": "5.2.4",
|
||||
"vite": "6.3.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"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">
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
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 './style.css'
|
||||
import App from './App.vue'
|
||||
import { createApp } from '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": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"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 path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
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