refactor: update UI styles to use new header background color

- Changed background color for various select elements and containers in Users.vue and Videos.vue to use 'bg-header'.
- Updated background color for status and role filters in the admin section.
- Adjusted background colors in Home.vue, QuickActions.vue, and other components to enhance UI consistency.
- Refactored Billing.vue and DomainsDns.vue to align with new design standards.
- Modified settings components to utilize new header color for better visual hierarchy.
- Improved accessibility and visual feedback in the SettingsRow and SettingsSectionCard components.
- Updated authentication middleware to include timestamp cookie for session management.
- Enhanced gRPC client to build internal metadata for service calls.
This commit is contained in:
2026-03-16 17:09:31 +07:00
parent b4bbacd9f1
commit 90d8409aa9
43 changed files with 174 additions and 241 deletions

View File

@@ -364,7 +364,7 @@ onMounted(loadTemplates);
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
<textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">VAST URL</label>
@@ -372,7 +372,7 @@ onMounted(loadTemplates);
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Ad format</label>
<select v-model="createForm.adFormat" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="createForm.adFormat" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="format in formatOptions" :key="format" :value="format">{{ format }}</option>
</select>
</div>
@@ -412,7 +412,7 @@ onMounted(loadTemplates);
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" />
<textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" />
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">VAST URL</label>
@@ -420,7 +420,7 @@ onMounted(loadTemplates);
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Ad format</label>
<select v-model="editForm.adFormat" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="editForm.adFormat" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="format in formatOptions" :key="format" :value="format">{{ format }}</option>
</select>
</div>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { client as rpcClient } from "@/api/rpcclient";
import { useAdminRuntimeMqtt } from "@/composables/useAdminRuntimeMqtt";
import AppButton from "@/components/app/AppButton.vue";
import AppDialog from "@/components/app/AppDialog.vue";
import AppInput from "@/components/app/AppInput.vue";
import { useAdminRuntimeMqtt } from "@/composables/useAdminRuntimeMqtt";
import { computed, onMounted, reactive, ref } from "vue";
import AdminSectionShell from "./components/AdminSectionShell.vue";
@@ -415,7 +415,7 @@ onMounted(loadJobs);
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Command</label>
<textarea v-model="createForm.command" rows="4" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="ffmpeg -i ..." />
<textarea v-model="createForm.command" rows="4" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="ffmpeg -i ..." />
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Image</label>
@@ -439,7 +439,7 @@ onMounted(loadJobs);
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Environment</label>
<textarea v-model="createForm.envText" rows="5" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="KEY=value per line" />
<textarea v-model="createForm.envText" rows="5" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="KEY=value per line" />
</div>
</div>
</div>

View File

@@ -246,7 +246,7 @@ onMounted(loadPayments);
</div>
<div class="space-y-2">
<label class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Status</label>
<select v-model="statusFilter" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="statusFilter" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="status in statusFilterOptions" :key="status || 'all'" :value="status">{{ status || 'ALL' }}</option>
</select>
</div>
@@ -336,7 +336,7 @@ onMounted(loadPayments);
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Payment method</label>
<select v-model="createForm.paymentMethod" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="createForm.paymentMethod" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="method in paymentMethodOptions" :key="method" :value="method">{{ method }}</option>
</select>
</div>
@@ -359,7 +359,7 @@ onMounted(loadPayments);
<div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Status</label>
<select v-model="statusForm.status" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="statusForm.status" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="status in statusOptions" :key="status" :value="status">{{ status }}</option>
</select>
</div>

View File

@@ -306,11 +306,11 @@ onMounted(loadPlans);
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
<textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Features</label>
<textarea v-model="createForm.featuresText" rows="4" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="One feature per line" />
<textarea v-model="createForm.featuresText" rows="4" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="One feature per line" />
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Price</label>
@@ -318,7 +318,7 @@ onMounted(loadPlans);
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Cycle</label>
<select v-model="createForm.cycle" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="createForm.cycle" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="cycle in cycleOptions" :key="cycle" :value="cycle">{{ cycle }}</option>
</select>
</div>
@@ -354,11 +354,11 @@ onMounted(loadPlans);
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" />
<textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" />
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Features</label>
<textarea v-model="editForm.featuresText" rows="4" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" />
<textarea v-model="editForm.featuresText" rows="4" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" />
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Price</label>
@@ -366,7 +366,7 @@ onMounted(loadPlans);
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Cycle</label>
<select v-model="editForm.cycle" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="editForm.cycle" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="cycle in cycleOptions" :key="cycle" :value="cycle">{{ cycle }}</option>
</select>
</div>

View File

@@ -329,7 +329,7 @@ onMounted(loadUsers);
</div>
<div class="space-y-2">
<label class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Role filter</label>
<select v-model="roleFilter" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="roleFilter" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="role in roleFilterOptions" :key="role || 'all'" :value="role">{{ role || 'ALL' }}</option>
</select>
</div>
@@ -423,7 +423,7 @@ onMounted(loadUsers);
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Role</label>
<select v-model="createForm.role" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="createForm.role" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="role in roleOptions" :key="role" :value="role">{{ role }}</option>
</select>
</div>
@@ -459,7 +459,7 @@ onMounted(loadUsers);
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Role</label>
<select v-model="editForm.role" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="editForm.role" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="role in roleOptions" :key="role" :value="role">{{ role }}</option>
</select>
</div>
@@ -486,7 +486,7 @@ onMounted(loadUsers);
<div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Role</label>
<select v-model="roleForm.role" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="roleForm.role" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="role in roleOptions" :key="role" :value="role">{{ role }}</option>
</select>
</div>

View File

@@ -345,7 +345,7 @@ onMounted(loadVideos);
</div>
<div class="space-y-2">
<label class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Status</label>
<select v-model="statusFilter" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="statusFilter" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="status in statusFilterOptions" :key="status || 'all'" :value="status">{{ status || 'ALL' }}</option>
</select>
</div>
@@ -428,7 +428,7 @@ onMounted(loadVideos);
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Status</label>
<select v-model="createForm.status" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="createForm.status" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="status in statusOptions" :key="status" :value="status">{{ status }}</option>
</select>
</div>
@@ -442,7 +442,7 @@ onMounted(loadVideos);
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
<textarea v-model="createForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Format</label>
@@ -480,7 +480,7 @@ onMounted(loadVideos);
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Status</label>
<select v-model="editForm.status" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<select v-model="editForm.status" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30">
<option v-for="status in statusOptions" :key="status" :value="status">{{ status }}</option>
</select>
</div>
@@ -494,7 +494,7 @@ onMounted(loadVideos);
</div>
<div class="space-y-2 md:col-span-2">
<label class="text-sm font-medium text-gray-700">Description</label>
<textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
<textarea v-model="editForm.description" rows="3" class="w-full rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground focus:border-primary/50 focus:outline-none focus:ring-2 focus:ring-primary/30" placeholder="Optional" />
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700">Format</label>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useTranslation } from 'i18next-vue';
import { computed } from 'vue';
const { t } = useTranslation();
@@ -237,7 +237,7 @@ const isScalePack = (tag: string) => tag === scaleTag.value;
/>
<path
d="M170 26c14-15 36-15 50 0l18 18c15 14 15 36 0 50l-18 18c-14 15-36 15-50 0l-18-18c-15-14-15-36 0-50l18-18zm35 41c5-5 5-14 0-19-6-5-14-5-20 0l-11 12c-5 5-5 13 0 19 5 5 14 5 19 0l12-12zm204 342c21-21 55-21 76 0l18 18c21 21 21 55 0 76l-18 18c-21 21-55 21-76 0l-18-18c-21-21-21-55 0-76l18-18zm38 38c5-5 5-14 0-19s-14-5-19 0l-18 18c-5 5-5 14 0 19s14 5 19 0l18-18zM113 170c-15-15-37-15-51 0l-18 18c-14 14-14 36 0 50l18 18c14 15 37 15 51 0l18-18c14-14 14-36 0-50l-18-18zm-16 41-12 12c-5 5-14 5-19 0-5-6-5-14 0-20l11-11c6-5 14-5 20 0 5 5 5 14 0 19zM485 31c-21-21-55-21-76 0l-39 39c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l39-39c21-21 21-55 0-76l-54-54zm-38 38-39 39c-5 5-14 5-19 0s-5-14 0-19l39-39c5-5 14-5 19 0s5 14 0 19zm-49 233c21-21 21-55 0-76l-54-54c-21-21-55-21-76 0L31 409c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l237-237zm-92-92L69 447c-5 5-14 5-19 0s-5-14 0-19l237-237c5-5 14-5 19 0s5 14 0 19z"
fill="#1e3050"
fill="currentColor"
/>
</svg>
</div>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useTranslation } from 'i18next-vue';
import Chart from '@/components/icons/Chart.vue';
import Credit from '@/components/icons/Credit.vue';
import Upload from '@/components/icons/Upload.vue';
import Video from '@/components/icons/Video.vue';
import { useUIState } from '@/stores/uiState';
import { useTranslation } from 'i18next-vue';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import Referral from './Referral.vue';
@@ -71,12 +71,12 @@ const quickActions = computed(() => [
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<button v-for="action in quickActions" :key="action.title" @click="action.onClick" :class="[
'p-6 rounded-xl text-left transition-all duration-200 flex flex-col bg-surface',
'p-6 rounded-xl text-left transition-all duration-200 flex flex-col bg-header',
'border border-gray-300 hover:border-primary hover:shadow-lg',
'group press-animated',
]">
<div
class="w-12 h-12 rounded-lg flex items-center justify-center mb-4 bg-muted group-hover:bg-primary/10">
class="w-12 h-12 rounded-lg flex items-center justify-center mb-4 bg-muted-dark group-hover:bg-primary/10">
<component filled :is="action.icon" class="w-6 h-6" />
</div>
<h3 class="font-semibold mb-1 group-hover:text-primary transition-colors">{{ action.title }}</h3>

View File

@@ -1,5 +1,5 @@
<template>
<div class="rounded-xl border border-gray-300 hover:border-primary hover:shadow-lg text-card-foreground bg-surface">
<div class="rounded-xl border border-gray-300 hover:border-primary hover:shadow-lg text-card-foreground bg-header">
<div class="flex flex-col space-y-1.5 p-6">
<h3 class="text-lg font-semibold leading-none tracking-tight">{{ t('overview.referral.title') }}</h3>
</div>
@@ -27,8 +27,8 @@
</template>
<script lang="ts" setup>
import { useAuthStore } from '@/stores/auth';
import { computed, ref } from 'vue';
import { useTranslation } from 'i18next-vue';
import { computed, ref } from 'vue';
const auth = useAuthStore();
const isCopied = ref(false);

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useTranslation } from 'i18next-vue';
import StatsCard from '@/components/dashboard/StatsCard.vue';
import { formatBytes } from '@/lib/utils';
import { useTranslation } from 'i18next-vue';
import { computed } from 'vue';
interface Props {
loading: boolean;
@@ -21,7 +21,7 @@ const localeTag = computed(() => i18next.resolvedLanguage === 'vi' ? 'vi-VN' : '
<template>
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<div v-for="i in 3" :key="i" class="bg-surface rounded-xl border border-gray-200 p-6">
<div v-for="i in 3" :key="i" class="bg-header rounded-xl border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<div class="space-y-2">
<div class="w-20 h-4 bg-gray-200 rounded animate-pulse mb-2" />

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { client as rpcClient } from '@/api/rpcclient';
import type { PaymentHistoryItem as PaymentHistoryApiItem, Plan as ModelPlan } from '@/server/gen/proto/app/v1/common';
import AppButton from '@/components/app/AppButton.vue';
import AppDialog from '@/components/app/AppDialog.vue';
import AppInput from '@/components/app/AppInput.vue';
@@ -12,6 +11,7 @@ import BillingPlansSection from '@/routes/settings/components/billing/BillingPla
import BillingTopupDialog from '@/routes/settings/components/billing/BillingTopupDialog.vue';
import BillingUsageSection from '@/routes/settings/components/billing/BillingUsageSection.vue';
import BillingWalletRow from '@/routes/settings/components/billing/BillingWalletRow.vue';
import type { Plan as ModelPlan, PaymentHistoryItem as PaymentHistoryApiItem } from '@/server/gen/proto/app/v1/common';
import { useAuthStore } from '@/stores/auth';
import { useQuery } from '@pinia/colada';
import { useTranslation } from 'i18next-vue';
@@ -634,7 +634,7 @@ const selectPreset = (amount: number) => {
'rounded-lg border px-4 py-3 text-left transition-all',
selectedTermMonths === months
? 'border-primary bg-primary/5 text-primary'
: 'border-border bg-surface text-foreground hover:border-primary/30 hover:bg-muted/30',
: 'border-border bg-header text-foreground hover:border-primary/30 hover:bg-muted/30',
]"
@click="selectedTermMonths = months"
>
@@ -645,11 +645,11 @@ const selectPreset = (amount: number) => {
</div>
<div class="grid gap-3 md:grid-cols-3">
<div class="rounded-lg border border-border bg-surface p-4">
<div class="rounded-lg border border-border bg-header p-4">
<p class="text-xs uppercase tracking-wide text-foreground/50">{{ t('settings.billing.upgradeDialog.totalLabel') }}</p>
<p class="mt-2 text-xl font-semibold text-foreground">{{ formatMoney(selectedTotalAmount) }}</p>
</div>
<div class="rounded-lg border border-border bg-surface p-4">
<div class="rounded-lg border border-border bg-header p-4">
<p class="text-xs uppercase tracking-wide text-foreground/50">{{ t('settings.billing.upgradeDialog.walletBalanceLabel') }}</p>
<p class="mt-2 text-xl font-semibold text-foreground">{{ formatMoney(walletBalance) }}</p>
</div>
@@ -679,7 +679,7 @@ const selectPreset = (amount: number) => {
'rounded-lg border p-4 text-left transition-all',
selectedPaymentMethod === 'wallet'
? 'border-primary bg-primary/5'
: 'border-border bg-surface hover:border-primary/30 hover:bg-muted/30',
: 'border-border bg-header hover:border-primary/30 hover:bg-muted/30',
]"
@click="selectUpgradePaymentMethod('wallet')"
>
@@ -695,7 +695,7 @@ const selectPreset = (amount: number) => {
'rounded-lg border p-4 text-left transition-all',
selectedPaymentMethod === 'topup'
? 'border-primary bg-primary/5'
: 'border-border bg-surface hover:border-primary/30 hover:bg-muted/30',
: 'border-border bg-header hover:border-primary/30 hover:bg-muted/30',
]"
@click="selectUpgradePaymentMethod('topup')"
>

View File

@@ -13,8 +13,8 @@ import SettingsNotice from '@/routes/settings/components/SettingsNotice.vue';
import SettingsSectionCard from '@/routes/settings/components/SettingsSectionCard.vue';
import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue';
import { useQuery } from '@pinia/colada';
import { computed, ref, watch } from 'vue';
import { useTranslation } from 'i18next-vue';
import { computed, ref, watch } from 'vue';
const toast = useAppToast();
const confirm = useAppConfirm();
@@ -304,7 +304,7 @@ const copyIframeCode = async () => {
<p class="text-xs text-foreground/60 mb-2">
{{ t('settings.domainsDns.embedCodeHint') }}
</p>
<pre class="bg-surface border border-border rounded-md p-3 text-xs text-foreground/70 overflow-x-auto"><code>{{ iframeCode }}</code></pre>
<pre class="bg-header border border-border rounded-md p-3 text-xs text-foreground/70 overflow-x-auto"><code>{{ iframeCode }}</code></pre>
</div>
<AppDialog

View File

@@ -2,7 +2,6 @@
import AppButton from '@/components/app/AppButton.vue';
import AppDialog from '@/components/app/AppDialog.vue';
import AppInput from '@/components/app/AppInput.vue';
import AppSwitch from '@/components/app/AppSwitch.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue';
import LockIcon from '@/components/icons/LockIcon.vue';
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
@@ -122,41 +121,6 @@ const saveLanguage = async () => {
}
};
const handleToggle2FA = async () => {
if (!twoFactorEnabled.value) {
try {
await new Promise(resolve => setTimeout(resolve, 500));
twoFactorDialogVisible.value = true;
} catch (e) {
toast.add({
severity: 'error',
summary: t('settings.securityConnected.toast.twoFactorEnableFailedSummary'),
detail: t('settings.securityConnected.toast.twoFactorEnableFailedDetail'),
life: 5000
});
twoFactorEnabled.value = false;
}
} else {
try {
await new Promise(resolve => setTimeout(resolve, 500));
toast.add({
severity: 'success',
summary: t('settings.securityConnected.toast.twoFactorDisabledSummary'),
detail: t('settings.securityConnected.toast.twoFactorDisabledDetail'),
life: 3000
});
} catch (e) {
toast.add({
severity: 'error',
summary: t('settings.securityConnected.toast.twoFactorDisableFailedSummary'),
detail: t('settings.securityConnected.toast.twoFactorDisableFailedDetail'),
life: 5000
});
twoFactorEnabled.value = true;
}
}
};
const confirmTwoFactor = async () => {
try {
await new Promise(resolve => setTimeout(resolve, 500));
@@ -228,10 +192,9 @@ const disconnectTelegram = async () => {
<SettingsRow
:title="t('settings.securityConnected.accountStatus.label')"
:description="t('settings.securityConnected.accountStatus.detail')"
iconBoxClass="bg-success/10"
>
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
@@ -245,11 +208,10 @@ const disconnectTelegram = async () => {
<SettingsRow
:title="t('settings.securityConnected.language.label')"
:description="t('settings.securityConnected.language.detail')"
iconBoxClass="bg-info/10"
actionsClass="flex items-center gap-2"
>
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20" />
<path d="M12 2a15 15 0 0 1 0 20" />
@@ -261,7 +223,7 @@ const disconnectTelegram = async () => {
<select
v-model="selectedLanguage"
:disabled="languageSaving"
class="rounded-md border border-border bg-surface px-3 py-2 text-sm text-foreground disabled:opacity-60"
class="rounded-md border border-border bg-header px-3 py-2 text-sm text-foreground disabled:opacity-60"
>
<option
v-for="option in languageOptions"
@@ -273,6 +235,7 @@ const disconnectTelegram = async () => {
</select>
<AppButton
size="sm"
variant="secondary"
:loading="languageSaving"
:disabled="languageSaving"
@click="saveLanguage"
@@ -282,33 +245,18 @@ const disconnectTelegram = async () => {
</template>
</SettingsRow>
<SettingsRow
:title="t('settings.securityConnected.twoFactor.label')"
:description="twoFactorEnabled ? t('settings.securityConnected.twoFactor.enabled') : t('settings.securityConnected.twoFactor.disabled')"
iconBoxClass="bg-primary/10"
>
<template #icon>
<LockIcon class="w-5 h-5 text-primary" />
</template>
<template #actions>
<AppSwitch v-model="twoFactorEnabled" @change="handleToggle2FA" />
</template>
</SettingsRow>
<SettingsRow
:title="t('settings.securityConnected.changePassword.label')"
:description="t('settings.securityConnected.changePassword.detail')"
iconBoxClass="bg-primary/10"
>
<template #icon>
<svg aria-hidden="true" class="fill-primary" height="24" viewBox="0 0 24 24" version="1.1" width="24" data-view-component="true">
<svg aria-hidden="true" class="fill-primary w-6 h-6" height="24" viewBox="0 0 24 24" version="1.1" width="24" data-view-component="true">
<path d="M22 9.75v5.5A1.75 1.75 0 0 1 20.25 17H3.75A1.75 1.75 0 0 1 2 15.25v-5.5C2 8.784 2.784 8 3.75 8h16.5c.966 0 1.75.784 1.75 1.75Zm-8.75 2.75a1.25 1.25 0 1 0-2.5 0 1.25 1.25 0 0 0 2.5 0Zm-6.5 1.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm10.5 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"></path>
</svg>
</template>
<template #actions>
<AppButton size="sm" @click="openChangePassword">
<AppButton variant="secondary" size="sm" @click="openChangePassword">
{{ t('settings.securityConnected.changePassword.button') }}
</AppButton>
</template>
@@ -317,10 +265,9 @@ const disconnectTelegram = async () => {
<SettingsRow
:title="t('settings.securityConnected.email.label')"
:description="emailConnected ? t('settings.securityConnected.email.connected') : t('settings.securityConnected.email.disconnected')"
iconBoxClass="bg-info/10"
>
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" class="text-info w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="20" height="16" x="2" y="4" rx="2"/>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
</svg>
@@ -336,10 +283,9 @@ const disconnectTelegram = async () => {
<SettingsRow
:title="t('settings.securityConnected.telegram.label')"
:description="telegramConnected ? (telegramUsername || t('settings.securityConnected.telegram.connectedFallback')) : t('settings.securityConnected.telegram.detailDisconnected')"
iconBoxClass="bg-[#0088cc]/10"
>
<template #icon>
<TelegramIcon class="w-5 h-5 text-[#0088cc]" />
<TelegramIcon class="w-6 h-6 text-[#0088cc]" />
</template>
<template #actions>
@@ -354,6 +300,7 @@ const disconnectTelegram = async () => {
<AppButton
v-else
size="sm"
variant="secondary"
@click="connectTelegram"
>
{{ t('settings.securityConnected.telegram.connect') }}
@@ -364,11 +311,10 @@ const disconnectTelegram = async () => {
<SettingsRow
:title="t('settings.securityConnected.logout.label')"
:description="t('settings.securityConnected.logout.detail')"
iconBoxClass="bg-danger/10"
hoverClass="hover:bg-danger/5"
>
<template #icon>
<XCircleIcon class="w-5 h-5 text-danger" />
<XCircleIcon class="w-6 h-6 text-danger" />
</template>
<template #actions>

View File

@@ -34,7 +34,7 @@
? 'bg-primary/10 text-primary font-semibold'
: item.danger
? 'text-danger hover:bg-danger/10'
: 'text-foreground/70 hover:bg-muted hover:text-foreground'
: 'text-foreground/70 hover:bg-header hover:text-foreground'
]"
>
<component :is="item.icon" class="w-5 h-5 shrink-0" :filled="currentTab === item.value" />
@@ -62,21 +62,21 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useTranslation } from 'i18next-vue';
import { useRoute } from 'vue-router';
import PageHeader from '@/components/dashboard/PageHeader.vue';
import AppConfirmHost from '@/components/app/AppConfirmHost.vue';
import AppToastHost from '@/components/app/AppToastHost.vue';
import ClientOnly from '@/components/ClientOnly';
import UserIcon from '@/components/icons/UserIcon.vue';
import GlobeIcon from '@/components/icons/Globe.vue';
import AlertTriangle from '@/components/icons/AlertTriangle.vue';
import { useAuthStore } from '@/stores/auth';
import CreditCardIcon from '@/components/icons/CreditCardIcon.vue';
import Bell from '@/components/icons/Bell.vue';
import PageHeader from '@/components/dashboard/PageHeader.vue';
import AdvertisementIcon from '@/components/icons/AdvertisementIcon.vue';
import AlertTriangle from '@/components/icons/AlertTriangle.vue';
import Bell from '@/components/icons/Bell.vue';
import CreditCardIcon from '@/components/icons/CreditCardIcon.vue';
import GlobeIcon from '@/components/icons/Globe.vue';
import UserIcon from '@/components/icons/UserIcon.vue';
import VideoPlayIcon from '@/components/icons/VideoPlayIcon.vue';
import { useAuthStore } from '@/stores/auth';
import { useTranslation } from 'i18next-vue';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const auth = useAuthStore();
@@ -94,7 +94,7 @@ const tabPaths: Record<string, string> = {
};
// Menu items grouped by category (GitHub-style)
const menuSections = computed(() => [
const menuSections = computed<{ title: string; items: { value: string; label: string; icon: any, danger?: boolean }[] }[]>(() => [
{
title: t('settings.menu.securityGroup'),
items: [

View File

@@ -15,8 +15,8 @@ const props = withDefaults(defineProps<{
rowClass?: string;
}>(), {
description: '',
iconBoxClass: '',
hoverClass: 'hover:bg-muted/30',
iconBoxClass: 'bg-muted text-foreground/70',
hoverClass: 'hover:bg-header',
titleClass: 'text-sm font-medium text-foreground',
descriptionClass: 'text-xs text-foreground/60 mt-0.5',
actionsClass: '',
@@ -43,7 +43,7 @@ const actionsWrapperClass = computed(() => cn('shrink-0', props.actionsClass));
<div v-bind="attrs" :class="rootClass">
<div class="flex min-w-0 items-center gap-4">
<div :class="iconClass">
<slot name="icon" />
<slot name="icon" class="h-6 w-6" />
</div>
<div class="min-w-0">

View File

@@ -23,7 +23,7 @@ const props = withDefaults(defineProps<{
const attrs = useAttrs();
const rootClass = computed(() => cn(
'bg-surface border border-border rounded-lg',
'bg-white border border-border rounded-lg',
));
</script>
@@ -32,7 +32,7 @@ const rootClass = computed(() => cn(
<div
v-if="title || description || $slots['header-actions']"
:class="cn(
'px-6 py-4 border-b border-border',
'px-6 py-4 border-b border-border bg-header rounded-tl-lg rounded-tr-lg',
$slots['header-actions'] ? 'flex items-center justify-between gap-4' : '',
headerClass,
)"

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import UploadQueueItem from './UploadQueueItem.vue';
import type { QueueItem } from '@/composables/useUploadQueue';
import { useTranslation } from 'i18next-vue';
@@ -24,7 +23,7 @@ const { t } = useTranslation();
<aside
class=":uno: w-full flex flex-col h-[calc(100svh-64px)] sticky top-16 before:(content-[''] absolute pointer-events-none inset-[-1px] rounded-[calc(var(--radius-2xl)+1px)] bg-[linear-gradient(-45deg,var(--capra-ramp-5)_0,var(--capra-ramp-4)_8%,var(--capra-ramp-3)_17%,var(--capra-ramp-2)_25%,var(--capra-ramp-1)_33%,#292929_34%,#292929_40%,#e1dfdf_45%,#e1dfdf_100%)] bg-[length:400%_200%] bg-[position:0_0] transition-[background-position] duration-[1000ms] ease-in-out delay-[500ms] z-0)"
:class="{ 'before:bg-[position:100%_100%]': pendingCount && pendingCount > 0 }">
<div class="bg-surface z-1 relative flex flex-col h-full rounded-2xl overflow-hidden">
<div class="bg-header z-1 relative flex flex-col h-full rounded-2xl overflow-hidden">
<div class="flex-1 overflow-y-auto scrollbar-thin p-6 space-y-5 relative" id="queue-list">
<div v-if="!items?.length" id="empty-queue"

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import type { Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
import LinkIcon from '@/components/icons/LinkIcon.vue';
import PencilIcon from '@/components/icons/PencilIcon.vue';
import TrashIcon from '@/components/icons/TrashIcon.vue';
import VideoIcon from '@/components/icons/VideoIcon.vue';
import { formatBytes, formatDate, getStatusSeverity } from '@/lib/utils';
import type { Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
import { useTranslation } from 'i18next-vue';
const props = defineProps<{
@@ -75,7 +75,7 @@ const isSelected = (video: ModelVideo) =>
</div>
<table v-else class="w-full min-w-[50rem]">
<thead>
<tr class="border-b border-gray-200 bg-gray-50">
<tr class="border-b border-gray-200 bg-header">
<th class="w-12 px-4 py-3">
<input type="checkbox" :checked="isAllSelected" @change="toggleAll"
class="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary" />