added dark mode support across components and improved accessibility styling
This commit is contained in:
@@ -1,95 +1,105 @@
|
||||
<template>
|
||||
<div id="app" class="min-h-screen bg-gray-50">
|
||||
<!-- Header -->
|
||||
<header class="bg-white shadow-sm border-b sticky top-0 z-40">
|
||||
<div id="app" class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<header
|
||||
class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 sticky top-0 z-40">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<h1 class="text-xl sm:text-2xl font-bold text-gray-900">Owly News</h1>
|
||||
<h1 class="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white">Owly News</h1>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
@click="showMobileMenu = !showMobileMenu"
|
||||
class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100"
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
@click="toggleDarkMode"
|
||||
class="p-2 rounded-md text-gray-400 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
||||
>
|
||||
<svg v-if="darkMode.isDarkMode" class="h-6 w-6" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
|
||||
</svg>
|
||||
<svg v-else class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="showMobileMenu = !showMobileMenu"
|
||||
class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 cursor-pointer"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<nav class="hidden lg:flex space-x-4">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
:class="[
|
||||
'px-3 py-2 rounded-md text-sm font-medium transition-colors cursor-pointer',
|
||||
activeTab === tab.id
|
||||
? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
]"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="{ 'hidden': !showMobileMenu }"
|
||||
class="lg:hidden py-2 border-t"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<nav class="hidden lg:flex space-x-4">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
:class="[
|
||||
'px-3 py-2 rounded-md text-sm font-medium transition-colors',
|
||||
<div class="flex flex-col space-y-1">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id; showMobileMenu = false"
|
||||
:class="[
|
||||
'px-3 py-2 rounded-md text-left text-base font-medium transition-colors cursor-pointer',
|
||||
activeTab === tab.id
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
|
||||
? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
]"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<div
|
||||
:class="{ 'hidden': !showMobileMenu }"
|
||||
class="lg:hidden py-2 border-t"
|
||||
>
|
||||
<div class="flex flex-col space-y-1">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id; showMobileMenu = false"
|
||||
:class="[
|
||||
'px-3 py-2 rounded-md text-left text-base font-medium transition-colors',
|
||||
activeTab === tab.id
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
|
||||
]"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8">
|
||||
<!-- Management Tab -->
|
||||
<div v-if="activeTab === 'management'" class="space-y-6">
|
||||
<!-- Control Panel - Responsive Grid -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<SyncButton />
|
||||
<NewsRefreshButton />
|
||||
<ModelStatus />
|
||||
<SyncButton/>
|
||||
<NewsRefreshButton/>
|
||||
<ModelStatus/>
|
||||
</div>
|
||||
|
||||
<!-- Management Tools -->
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<FeedManager />
|
||||
<CronSlider />
|
||||
<FeedManager/>
|
||||
<CronSlider/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- News Tab -->
|
||||
<div v-if="activeTab === 'news'" class="space-y-6">
|
||||
<!-- News Filters -->
|
||||
<NewsFilters />
|
||||
<NewsFilters/>
|
||||
|
||||
<!-- News List -->
|
||||
<NewsList />
|
||||
<NewsList/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useNews } from './stores/useNews';
|
||||
import {ref, onMounted} from 'vue';
|
||||
import {useNews} from './stores/useNews';
|
||||
import {useDarkMode} from './stores/useDarkMode';
|
||||
import SyncButton from './components/SyncButton.vue';
|
||||
import NewsRefreshButton from './components/NewsRefreshButton.vue';
|
||||
import ModelStatus from './components/ModelStatus.vue';
|
||||
@@ -99,19 +109,27 @@ import NewsFilters from './components/NewsFilters.vue';
|
||||
import NewsList from './components/NewsList.vue';
|
||||
|
||||
const news = useNews();
|
||||
const darkMode = useDarkMode();
|
||||
const activeTab = ref('news');
|
||||
const showMobileMenu = ref(false);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
darkMode.toggleDarkMode();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
darkMode.initDarkMode();
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ id: 'news', label: 'News' },
|
||||
{ id: 'management', label: 'Management' }
|
||||
{id: 'news', label: 'News'},
|
||||
{id: 'management', label: 'Management'}
|
||||
];
|
||||
|
||||
onMounted(async () => {
|
||||
await news.loadLastSync();
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (showMobileMenu.value && !(e.target as Element).closest('header')) {
|
||||
showMobileMenu.value = false;
|
||||
@@ -120,7 +138,6 @@ document.addEventListener('click', (e) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Component-scoped styles to prevent global conflicts */
|
||||
#app {
|
||||
display: block;
|
||||
grid-template-columns: none;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
@@ -12,24 +11,26 @@
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-md bg-white/20"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-md bg-white/20 dark:bg-black/20"
|
||||
@click="closeModal"
|
||||
>
|
||||
<div
|
||||
class="bg-white/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 max-w-2xl w-full max-h-[90vh] overflow-hidden modal-content"
|
||||
class="bg-white/95 dark:bg-gray-800/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-gray-700/20 max-w-2xl w-full max-h-[90vh] overflow-hidden modal-content"
|
||||
@click.stop
|
||||
>
|
||||
<!-- Modal Header with glass effect -->
|
||||
<div class="sticky top-0 bg-white/80 backdrop-blur-sm border-b border-white/30 px-6 py-4">
|
||||
<div
|
||||
class="sticky top-0 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border-b border-white/30 dark:border-gray-700/30 px-6 py-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<!-- Country badge with glass effect -->
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-white/60 backdrop-blur-sm text-gray-800 border border-white/30 mb-2">
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm text-gray-800 dark:text-gray-200 border border-white/30 dark:border-gray-600/30 mb-2">
|
||||
{{ article?.country }}
|
||||
</span>
|
||||
|
||||
<!-- Title -->
|
||||
<h3 class="text-lg font-semibold text-gray-900 leading-6 pr-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white leading-6 pr-4">
|
||||
{{ article?.title }}
|
||||
</h3>
|
||||
|
||||
@@ -37,7 +38,7 @@
|
||||
<time
|
||||
v-if="article"
|
||||
:datetime="new Date(article.published * 1000).toISOString()"
|
||||
class="text-sm text-gray-600 block mt-2"
|
||||
class="text-sm text-gray-600 dark:text-gray-400 block mt-2"
|
||||
>
|
||||
{{ formatDate(article.published) }}
|
||||
</time>
|
||||
@@ -46,11 +47,12 @@
|
||||
<!-- Close button with glass effect -->
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="flex-shrink-0 bg-white/60 backdrop-blur-sm rounded-full p-2 text-gray-500 hover:text-gray-700 hover:bg-white/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 border border-white/30 transition-all duration-200"
|
||||
class="flex-shrink-0 bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm rounded-full p-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-white/80 dark:hover:bg-gray-600/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 dark:focus:ring-green-600 border border-white/30 dark:border-gray-600/30 transition-all duration-200"
|
||||
>
|
||||
<span class="sr-only">Close</span>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -60,23 +62,28 @@
|
||||
<div class="px-6 py-4 overflow-y-auto custom-scrollbar">
|
||||
<!-- Full Summary -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<h4
|
||||
class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Summary
|
||||
</h4>
|
||||
<div class="prose prose-sm max-w-none">
|
||||
<p class="text-gray-800 leading-relaxed whitespace-pre-wrap">{{ article?.summary }}</p>
|
||||
<p class="text-gray-800 dark:text-gray-200 leading-relaxed whitespace-pre-wrap">
|
||||
{{ article?.summary }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer with glass effect -->
|
||||
<div class="sticky bottom-0 bg-white/80 backdrop-blur-sm border-t border-white/30 px-6 py-4 flex flex-col sm:flex-row gap-3 sm:justify-between">
|
||||
<div
|
||||
class="sticky bottom-0 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border-t border-white/30 dark:border-gray-700/30 px-6 py-4 flex flex-col sm:flex-row gap-3 sm:justify-between">
|
||||
<button
|
||||
@click="closeModal"
|
||||
class="inline-flex justify-center px-4 py-2 text-sm font-medium text-gray-700 bg-white/60 backdrop-blur-sm border border-white/40 rounded-lg shadow-sm hover:bg-white/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200"
|
||||
class="inline-flex justify-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm border border-white/40 dark:border-gray-600/40 rounded-lg shadow-sm hover:bg-white/80 dark:hover:bg-gray-600/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 dark:focus:ring-green-600 transition-all duration-200"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
@@ -90,7 +97,8 @@
|
||||
>
|
||||
Read Full Article
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
@@ -101,7 +109,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, onUnmounted } from 'vue';
|
||||
import {watch, onUnmounted} from 'vue';
|
||||
|
||||
interface Article {
|
||||
id: number;
|
||||
@@ -188,12 +196,20 @@ function formatDate(timestamp: number): string {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:root.dark .custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(16, 185, 129, 0.3);
|
||||
border-radius: 3px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
:root.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
@@ -20,10 +20,10 @@ async function update() {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="p-4 bg-white rounded shadow">
|
||||
<div class="p-4 bg-white dark:bg-gray-800 rounded shadow dark:shadow-gray-700 dark:text-gray-200">
|
||||
<label class="block font-semibold mb-2">Fetch Interval (Stunden)</label>
|
||||
<input type="range" min="0.5" max="24" step="0.5" v-model="hours" @change="update"
|
||||
class="w-full"/>
|
||||
class="w-full accent-green-600 dark:accent-green-500"/>
|
||||
<p class="text-sm mt-2">{{ hours }} h <span v-if="saving" class="animate-pulse">…</span></p>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -15,23 +15,30 @@ async function add() {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="bg-white rounded p-4 shadow space-y-2">
|
||||
<h2 class="font-semibold text-lg">Feeds verwalten</h2>
|
||||
<div class="bg-white dark:bg-gray-800 rounded p-4 shadow space-y-2">
|
||||
<h2 class="font-semibold text-lg dark:text-gray-200">Feeds verwalten</h2>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<select v-model="country" class="border rounded p-1 flex-1">
|
||||
<select v-model="country"
|
||||
class="border dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded p-1 flex-1">
|
||||
<option>DE</option>
|
||||
<option>FR</option>
|
||||
<option>EU</option>
|
||||
</select>
|
||||
<input v-model="url" placeholder="https://…" class="border rounded p-1 flex-[3]"/>
|
||||
<button @click="add" class="bg-green-600 text-white px-3 rounded">+</button>
|
||||
<input v-model="url" placeholder="https://…"
|
||||
class="border dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded p-1 flex-[3]"/>
|
||||
<button @click="add"
|
||||
class="bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-800 text-white px-3 rounded">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="list-disc pl-5">
|
||||
<li v-for="(f, index) in feeds.list" :key="index" class="flex justify-between items-center">
|
||||
<span>{{ f.country }} — {{ f.url }}</span>
|
||||
<button @click="feeds.remove(f.url)" class="text-red-600">✕</button>
|
||||
<span class="dark:text-gray-200">{{ f.country }} — {{ f.url }}</span>
|
||||
<button @click="feeds.remove(f.url)"
|
||||
class="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-500">✕
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { useModel } from '../stores/useModel';
|
||||
import {onMounted} from 'vue';
|
||||
import {useModel} from '../stores/useModel';
|
||||
|
||||
const model = useModel();
|
||||
|
||||
@@ -10,29 +10,29 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white rounded p-4 shadow">
|
||||
<h3 class="font-semibold mb-2">Model Status</h3>
|
||||
<div class="bg-white dark:bg-gray-800 rounded p-4 shadow">
|
||||
<h3 class="font-semibold mb-2 text-gray-900 dark:text-white">Model Status</h3>
|
||||
|
||||
<div v-if="model.status === 'loading'" class="text-gray-600">
|
||||
<div v-if="model.status === 'loading'" class="text-gray-600 dark:text-gray-400">
|
||||
Loading model information...
|
||||
</div>
|
||||
|
||||
<div v-else-if="model.status === 'error'" class="text-red-600">
|
||||
<div v-else-if="model.status === 'error'" class="text-red-600 dark:text-red-400">
|
||||
Error: {{ model.error }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="flex items-center mb-2">
|
||||
<span class="font-medium mr-2">Name:</span>
|
||||
<span>{{ model.name }}</span>
|
||||
<span class="font-medium mr-2 text-gray-900 dark:text-white">Name:</span>
|
||||
<span class="dark:text-gray-200">{{ model.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<span class="font-medium mr-2">Status:</span>
|
||||
<span class="font-medium mr-2 text-gray-900 dark:text-white">Status:</span>
|
||||
<span
|
||||
:class="{
|
||||
'text-green-600': model.status === 'ready',
|
||||
'text-red-600': model.status === 'not available'
|
||||
'text-green-600 dark:text-green-400': model.status === 'ready',
|
||||
'text-red-600 dark:text-red-400': model.status === 'not available'
|
||||
}"
|
||||
>
|
||||
{{ model.status }}
|
||||
|
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div class="bg-white rounded-lg shadow-sm border overflow-hidden">
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border dark:border-gray-700 overflow-hidden">
|
||||
<!-- Mobile Header with Toggle -->
|
||||
<div class="lg:hidden">
|
||||
<button
|
||||
@click="showFilters = !showFilters"
|
||||
class="w-full px-4 py-3 flex items-center justify-between bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span class="font-medium text-gray-900">Filters</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">Filters</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-500">{{ articleCount }} articles</span>
|
||||
<svg
|
||||
@@ -16,16 +17,17 @@
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Header -->
|
||||
<div class="hidden lg:block px-6 py-4 border-b">
|
||||
<div class="hidden lg:block px-6 py-4 border-b dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900">News Filters</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">News Filters</h3>
|
||||
<span class="text-sm text-gray-500">{{ articleCount }} articles found</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,9 +40,9 @@
|
||||
<!-- Mobile/Tablet: Stacked Layout -->
|
||||
<div class="lg:hidden space-y-6">
|
||||
<!-- Country Filter Mobile -->
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-gray-900 mb-3">Countries</h4>
|
||||
<div class="space-y-3">
|
||||
<div class="bg-gray-50 dark:bg-gray-800 dark:text-gray-200 rounded-lg p-4">
|
||||
<h4 class="font-medium text-gray-900 dark:text-white mb-3">Countries</h4>
|
||||
<div class="space-y-3 ">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -48,7 +50,7 @@
|
||||
@change="onAllCountriesChange"
|
||||
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||
/>
|
||||
<span class="ml-3 text-sm font-medium">All Countries</span>
|
||||
<span class="ml-3 text-sm dark:text-gray-200">All Countries</span>
|
||||
</label>
|
||||
<div v-if="!filters.allCountries" class="grid grid-cols-2 gap-2 pl-6">
|
||||
<label v-for="country in availableCountries" :key="country" class="flex items-center">
|
||||
@@ -65,8 +67,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Date Filter Mobile -->
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-gray-900 mb-3">Date Range</h4>
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
||||
<h4 class="font-medium text-gray-900 dark:text-white mb-3">Date Range</h4>
|
||||
<div class="space-y-3">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
@@ -75,16 +77,16 @@
|
||||
@change="onAllDatesChange"
|
||||
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||
/>
|
||||
<span class="ml-3 text-sm font-medium">All Dates</span>
|
||||
<span class="ml-3 text-sm font-medium dark:text-gray-200">All Dates</span>
|
||||
</label>
|
||||
<div v-if="!filters.allDates" class="space-y-3 pl-6">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">From</label>
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-200 mb-1">From</label>
|
||||
<input
|
||||
type="date"
|
||||
v-model="filters.fromDate"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500 dark:text-gray-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -92,20 +94,20 @@
|
||||
<input
|
||||
type="date"
|
||||
v-model="filters.toDate"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500 dark:text-gray-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Presets Mobile -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-2">Quick Presets</label>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white mb-3">Quick Presets</h4>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="preset in datePresets"
|
||||
:key="preset.label"
|
||||
@click="applyDatePreset(preset)"
|
||||
class="px-3 py-2 text-sm text-green-600 bg-green-50 hover:bg-green-100 rounded-md transition-colors"
|
||||
class="px-3 py-2 text-sm text-green-600 bg-green-50 hover:bg-green-100 dark:hover:bg-green-700 rounded-md transition-colors"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</button>
|
||||
@@ -123,9 +125,12 @@
|
||||
class="flex-1 px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-medium transition-colors"
|
||||
>
|
||||
<span v-if="isLoading" class="flex items-center justify-center">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Loading...
|
||||
</span>
|
||||
@@ -145,7 +150,8 @@
|
||||
<div class="hidden lg:grid lg:grid-cols-4 gap-6">
|
||||
<!-- Country Filter Desktop -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-3">Countries</label>
|
||||
<label
|
||||
class="block text-sm font-medium text-gray-700 dark:text-white mb-3">Countries</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
@@ -154,7 +160,7 @@
|
||||
@change="onAllCountriesChange"
|
||||
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||
/>
|
||||
<span class="ml-2 text-sm font-medium">All Countries</span>
|
||||
<span class="ml-2 text-sm font-medium dark:text-white">All Countries</span>
|
||||
</label>
|
||||
<div v-if="!filters.allCountries" class="space-y-1 max-h-40 overflow-y-auto">
|
||||
<label v-for="country in availableCountries" :key="country" class="flex items-center">
|
||||
@@ -164,7 +170,7 @@
|
||||
v-model="filters.selectedCountries"
|
||||
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||
/>
|
||||
<span class="ml-2 text-sm">{{ country }}</span>
|
||||
<span class="ml-2 text-sm dark:text-gray-200">{{ country }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,7 +178,8 @@
|
||||
|
||||
<!-- Date Range Filter Desktop -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-3">Date Range</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-white mb-3">Date
|
||||
Range</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
@@ -181,23 +188,23 @@
|
||||
@change="onAllDatesChange"
|
||||
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
||||
/>
|
||||
<span class="ml-2 text-sm font-medium">All Dates</span>
|
||||
<span class="ml-2 text-sm font-medium dark:text-white">All Dates</span>
|
||||
</label>
|
||||
<div v-if="!filters.allDates" class="space-y-2">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">From</label>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">From</label>
|
||||
<input
|
||||
type="date"
|
||||
v-model="filters.fromDate"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">To</label>
|
||||
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">To</label>
|
||||
<input
|
||||
type="date"
|
||||
v-model="filters.toDate"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,13 +213,14 @@
|
||||
|
||||
<!-- Quick Date Presets Desktop -->
|
||||
<div v-if="!filters.allDates">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-3">Quick Presets</label>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-white mb-3">Quick
|
||||
Presets</label>
|
||||
<div class="space-y-1">
|
||||
<button
|
||||
v-for="preset in datePresets"
|
||||
:key="preset.label"
|
||||
@click="applyDatePreset(preset)"
|
||||
class="block w-full text-left px-3 py-2 text-sm text-green-600 hover:bg-green-50 rounded-md transition-colors"
|
||||
class="block w-full text-left px-3 py-2 text-sm text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900 rounded-md transition-colors"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</button>
|
||||
@@ -224,12 +232,15 @@
|
||||
<button
|
||||
@click="applyFilters"
|
||||
:disabled="isLoading"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-medium transition-colors"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:bg-gray-400 dark:disabled:bg-gray-600 disabled:cursor-not-allowed font-medium transition-colors"
|
||||
>
|
||||
<span v-if="isLoading" class="flex items-center justify-center">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Loading...
|
||||
</span>
|
||||
@@ -249,9 +260,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useNews } from '../stores/useNews';
|
||||
import { useFeeds } from '../stores/useFeeds';
|
||||
import {ref, onMounted, computed} from 'vue';
|
||||
import {useNews} from '../stores/useNews';
|
||||
import {useFeeds} from '../stores/useFeeds';
|
||||
|
||||
const news = useNews();
|
||||
const feeds = useFeeds();
|
||||
|
@@ -2,11 +2,14 @@
|
||||
<div>
|
||||
<!-- Empty State -->
|
||||
<div v-if="news.articles.length === 0" class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h4" />
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h4"/>
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">No articles found</h3>
|
||||
<p class="mt-2 text-gray-500">Try adjusting your filters to see more results.</p>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No articles found</h3>
|
||||
<p class="mt-2 text-gray-500 dark:text-gray-400">Try adjusting your filters to see more
|
||||
results.</p>
|
||||
</div>
|
||||
|
||||
<!-- Articles Grid -->
|
||||
@@ -14,12 +17,13 @@
|
||||
<article
|
||||
v-for="article in news.articles"
|
||||
:key="article.id"
|
||||
class="bg-white rounded-lg shadow-sm border hover:shadow-md transition-all duration-200 overflow-hidden group"
|
||||
class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md dark:hover:shadow-lg dark:hover:shadow-gray-800/50 transition-all duration-200 overflow-hidden group"
|
||||
>
|
||||
<!-- Article Header -->
|
||||
<div class="p-4 sm:p-6">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">
|
||||
{{ article.country }}
|
||||
</span>
|
||||
<time
|
||||
@@ -31,14 +35,16 @@
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h3 class="text-base sm:text-lg font-semibold text-gray-900 mb-3 line-clamp-2 group-hover:text-green-600 transition-colors">
|
||||
<h3
|
||||
class="text-base sm:text-lg font-semibold text-gray-900 dark:text-white mb-3 line-clamp-2 group-hover:text-green-600 dark:group-hover:text-green-400 transition-colors">
|
||||
<a :href="article.url" target="_blank" rel="noopener noreferrer">
|
||||
{{ article.title }}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<!-- Summary -->
|
||||
<p class="text-sm sm:text-base text-gray-700 line-clamp-3 mb-4 leading-relaxed">
|
||||
<p
|
||||
class="text-sm sm:text-base text-gray-700 dark:text-gray-300 line-clamp-3 mb-4 leading-relaxed">
|
||||
{{ article.summary }}
|
||||
</p>
|
||||
|
||||
@@ -49,8 +55,10 @@
|
||||
>
|
||||
View full summary
|
||||
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -65,7 +73,8 @@
|
||||
>
|
||||
Read full article
|
||||
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
@@ -89,8 +98,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useNews } from '../stores/useNews';
|
||||
import {ref} from 'vue';
|
||||
import {useNews} from '../stores/useNews';
|
||||
import ArticleModal from './ArticleModal.vue';
|
||||
|
||||
const news = useNews();
|
||||
|
@@ -1,12 +1,18 @@
|
||||
import './assets/main.css';
|
||||
import './style.css';
|
||||
|
||||
import {createPinia} from 'pinia';
|
||||
import {createApp} from 'vue';
|
||||
import App from "@/App.vue";
|
||||
|
||||
import {useDarkMode} from './stores/useDarkMode';
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(pinia);
|
||||
|
||||
// Initialize dark mode before mounting the app
|
||||
const darkModeStore = useDarkMode(pinia);
|
||||
darkModeStore.initDarkMode();
|
||||
|
||||
app.mount('#app');
|
||||
|
56
frontend/src/stores/useDarkMode.ts
Normal file
56
frontend/src/stores/useDarkMode.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {defineStore} from 'pinia';
|
||||
|
||||
export const useDarkMode = defineStore('darkMode', {
|
||||
state: () => ({
|
||||
isDarkMode: localStorage.theme === "dark" ||
|
||||
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
}),
|
||||
actions: {
|
||||
// Explicitly choose light mode
|
||||
setLightMode() {
|
||||
localStorage.theme = "light";
|
||||
this.isDarkMode = false;
|
||||
this.applyDarkMode();
|
||||
},
|
||||
|
||||
// Explicitly choose dark mode
|
||||
setDarkMode() {
|
||||
localStorage.theme = "dark";
|
||||
this.isDarkMode = true;
|
||||
this.applyDarkMode();
|
||||
},
|
||||
|
||||
// Respect OS preference (remove explicit theme choice)
|
||||
setSystemMode() {
|
||||
localStorage.removeItem("theme");
|
||||
this.isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
this.applyDarkMode();
|
||||
},
|
||||
|
||||
// Toggle between light and dark (keeping system as fallback)
|
||||
toggleDarkMode() {
|
||||
if (this.isDarkMode) {
|
||||
this.setLightMode();
|
||||
} else {
|
||||
this.setDarkMode();
|
||||
}
|
||||
},
|
||||
|
||||
applyDarkMode() {
|
||||
document.documentElement.classList.toggle('dark', this.isDarkMode);
|
||||
},
|
||||
|
||||
initDarkMode() {
|
||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||
document.documentElement.classList.toggle(
|
||||
"dark",
|
||||
localStorage.theme === "dark" ||
|
||||
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
);
|
||||
|
||||
// Update state to match what was applied
|
||||
this.isDarkMode = localStorage.theme === "dark" ||
|
||||
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
}
|
||||
}
|
||||
});
|
@@ -2,6 +2,8 @@
|
||||
|
||||
@import "./assets/main.css";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* Custom scrollbar for webkit browsers */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
Reference in New Issue
Block a user