Added vue frontend project, fixed swagger path

This commit is contained in:
2026-04-05 21:51:03 +03:00
parent 9d845c8899
commit 4902889401
35 changed files with 3810 additions and 475 deletions
+2 -2
View File
@@ -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. Описание переменных
+35 -1
View File
@@ -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")
+1 -1
View File
@@ -68,7 +68,7 @@ func Load() (Config, error) {
apiPort = "8000"
}
if openAPIEndpoint == "" {
openAPIEndpoint = "/docs"
openAPIEndpoint = "/openapi"
}
}
-3
View File
@@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar"]
}
+12 -11
View File
@@ -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>
+2027
View File
File diff suppressed because it is too large Load Diff
+17 -12
View File
@@ -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"
}
}
}
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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

+127
View File
@@ -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>
+44
View File
@@ -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>
+92
View File
@@ -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>
+33
View File
@@ -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>
-93
View File
@@ -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>
+87
View File
@@ -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>
+380
View File
@@ -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 &amp; 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>
+179
View File
@@ -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>
+61
View File
@@ -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>
+4 -4
View File
@@ -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');
-296
View File
@@ -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);
}
}
View File
+3
View File
@@ -0,0 +1,3 @@
@import './fonts.css';
@import './tailwind.css';
@import './theme.css';
+4
View File
@@ -0,0 +1,4 @@
@import 'tailwindcss' source(none);
@source '../**/*.{js,ts,jsx,tsx,vue}';
@import 'tw-animate-css';
+181
View File
@@ -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;
}
}
-16
View File
@@ -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
View File
@@ -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"
]
}
-26
View File
@@ -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
View File
@@ -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'],
})