This commit is contained in:
parent
33624d3b02
commit
896a3b77d7
5 changed files with 293 additions and 85 deletions
|
|
@ -3,6 +3,7 @@
|
|||
import { fly, fade, slide } from 'svelte/transition';
|
||||
import { browser } from '$app/environment';
|
||||
import { isAuthenticated } from '$lib/stores/auth';
|
||||
import { getHolidaysForMonth } from '$lib/data/holidays.js';
|
||||
import { connectionStatus } from '$lib/chat/chatStore';
|
||||
import TerminalTabBar from '$lib/components/terminal/TerminalTabBar.svelte';
|
||||
import TerminalCore from '$lib/components/terminal/TerminalCore.svelte';
|
||||
|
|
@ -231,8 +232,20 @@
|
|||
calendarDate.getFullYear() === today.getFullYear();
|
||||
}
|
||||
|
||||
function isHoliday(day) {
|
||||
if (!day) return false;
|
||||
return calendarHolidays.has(day);
|
||||
}
|
||||
|
||||
function getHolidayName(day) {
|
||||
if (!day) return null;
|
||||
const info = calendarHolidays.get(day);
|
||||
return info ? info.name : null;
|
||||
}
|
||||
|
||||
$: calendarDays = getCalendarDays(calendarDate);
|
||||
$: calendarMonthYear = calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||
$: calendarHolidays = getHolidaysForMonth(calendarDate.getFullYear(), calendarDate.getMonth());
|
||||
|
||||
// Timezone definitions
|
||||
const timezones = [
|
||||
|
|
@ -330,7 +343,13 @@
|
|||
</div>
|
||||
<div class="calendar-days">
|
||||
{#each calendarDays as day}
|
||||
<span class="calendar-day" class:today={isToday(day)} class:empty={!day}>
|
||||
<span
|
||||
class="calendar-day"
|
||||
class:today={isToday(day)}
|
||||
class:holiday={isHoliday(day)}
|
||||
class:empty={!day}
|
||||
title={getHolidayName(day) || ''}
|
||||
>
|
||||
{day || ''}
|
||||
</span>
|
||||
{/each}
|
||||
|
|
@ -725,6 +744,24 @@
|
|||
background: #9eeea1;
|
||||
}
|
||||
|
||||
.calendar-day.holiday {
|
||||
background: #ff9800;
|
||||
color: #0d1117;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.calendar-day.holiday:not(.today):hover {
|
||||
background: #ffb74d;
|
||||
}
|
||||
|
||||
.calendar-day.today.holiday {
|
||||
background: linear-gradient(135deg, #7ee787 50%, #ff9800 50%);
|
||||
}
|
||||
|
||||
.calendar-day.today.holiday:hover {
|
||||
background: linear-gradient(135deg, #9eeea1 50%, #ffb74d 50%);
|
||||
}
|
||||
|
||||
.timezone-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@
|
|||
let lastSeekTime = 0; // Track last seek time for rate-limiting
|
||||
let leadInEndedAt = 0; // Track when lead-in ended for grace period
|
||||
let wasInLeadIn = false; // Track previous lead-in state
|
||||
let restartInProgress = false; // Track when video restart is in progress (prevents drift detection)
|
||||
let restartStartedAt = 0; // When restart started
|
||||
|
||||
const SYNC_CHECK_INTERVAL = 1000; // Check sync every second (matches server sync interval)
|
||||
const RESTART_GRACE_PERIOD = 3000; // 3 seconds grace period during video restart
|
||||
const DRIFT_THRESHOLD = 2; // Seek if drift > 2 seconds (CyTube-style tight sync)
|
||||
const MIN_SYNC_INTERVAL = 1000; // Minimum 1 second between sync checks (server pushes every 1s)
|
||||
const CONTROLLER_SEEK_DEBOUNCE = 2000; // Debounce controller seeks by 2 seconds
|
||||
|
|
@ -122,6 +125,9 @@
|
|||
// YT.PlayerState: UNSTARTED (-1), ENDED (0), PLAYING (1), PAUSED (2), BUFFERING (3), CUED (5)
|
||||
// Always handle ENDED state - crucial for playlist advancement
|
||||
if (state === window.YT.PlayerState.ENDED) {
|
||||
// Set restart flag to prevent drift detection during transition
|
||||
restartInProgress = true;
|
||||
restartStartedAt = Date.now();
|
||||
dispatch('ended');
|
||||
// Also notify server directly - this is a fallback for when duration-based detection fails
|
||||
watchSync.videoEnded();
|
||||
|
|
@ -187,10 +193,29 @@
|
|||
const shouldBePlaying = storeState.playbackState === 'playing';
|
||||
const hasVideoEnded = playerState === window.YT.PlayerState.ENDED;
|
||||
|
||||
// During restart transition (video ended, waiting for server response), skip drift detection
|
||||
// This prevents false "Controller seek detected" when video loops/restarts
|
||||
if (restartInProgress) {
|
||||
if (now - restartStartedAt >= RESTART_GRACE_PERIOD) {
|
||||
// Grace period expired, reset flag
|
||||
restartInProgress = false;
|
||||
} else {
|
||||
// Still in grace period - only ensure video starts playing if needed, no sync
|
||||
if (shouldBePlaying && !isPlayerPlaying && !isBuffering && !hasVideoEnded) {
|
||||
ignoreStateChange = true;
|
||||
player.playVideo();
|
||||
setTimeout(() => { ignoreStateChange = false; }, 1500);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// During lead-in period, let video buffer without seeking
|
||||
// Server sends leadIn=true for 3 seconds after play starts
|
||||
if (storeState.leadIn) {
|
||||
wasInLeadIn = true;
|
||||
// Lead-in started, clear restart flag since server has responded
|
||||
restartInProgress = false;
|
||||
// During lead-in, just ensure video is loading/buffering
|
||||
// Don't seek or sync position - wait for lead-in to complete
|
||||
if (shouldBePlaying && !isPlayerPlaying && !isBuffering && !hasVideoEnded) {
|
||||
|
|
@ -266,6 +291,7 @@
|
|||
$: if (playerReady && $watchSync.currentVideo?.id !== currentPlaylistItemId) {
|
||||
currentPlaylistItemId = $watchSync.currentVideo?.id;
|
||||
durationReportedForItemId = null; // Reset so we report duration for new video
|
||||
restartInProgress = false; // Video changed, clear restart flag
|
||||
const newVideoId = $watchSync.currentVideo?.youtubeVideoId;
|
||||
if (newVideoId && player) {
|
||||
videoId = newVideoId;
|
||||
|
|
@ -302,13 +328,18 @@
|
|||
} else if (action === 'seek' && currentTime !== undefined) {
|
||||
player.seekTo(currentTime, true);
|
||||
} else if (action === 'skip' || action === 'video_changed') {
|
||||
// Server has responded, clear restart flag
|
||||
restartInProgress = false;
|
||||
// Video change will be handled by the reactive statement above
|
||||
setTimeout(() => checkAndSync(true), 1000);
|
||||
} else if (action === 'locked_restart' || action === 'repeat') {
|
||||
// Server has responded with restart, clear the restart flag
|
||||
restartInProgress = false;
|
||||
// Locked video loop - seek to beginning and play
|
||||
player.seekTo(0, true);
|
||||
player.playVideo();
|
||||
setTimeout(() => checkAndSync(true), 1000);
|
||||
// Delay sync check longer to allow player to stabilize
|
||||
setTimeout(() => checkAndSync(true), 2000);
|
||||
}
|
||||
|
||||
setTimeout(() => { ignoreStateChange = false; }, 1000);
|
||||
|
|
|
|||
110
frontend/src/lib/data/holidays.js
Normal file
110
frontend/src/lib/data/holidays.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Holiday data for terminal calendar
|
||||
* Includes both Indian national holidays and international holidays
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fixed holidays - same date every year
|
||||
* Format: { month: 0-11, day: 1-31, name: string, type: 'indian' | 'international' }
|
||||
*/
|
||||
export const fixedHolidays = [
|
||||
// Indian National Holidays (Fixed)
|
||||
{ month: 0, day: 26, name: 'Republic Day', type: 'indian' },
|
||||
{ month: 7, day: 15, name: 'Independence Day', type: 'indian' },
|
||||
{ month: 9, day: 2, name: 'Gandhi Jayanti', type: 'indian' },
|
||||
|
||||
// International Holidays (Fixed)
|
||||
{ month: 0, day: 1, name: "New Year's Day", type: 'international' },
|
||||
{ month: 1, day: 14, name: "Valentine's Day", type: 'international' },
|
||||
{ month: 9, day: 31, name: 'Halloween', type: 'international' },
|
||||
{ month: 11, day: 25, name: 'Christmas', type: 'international' },
|
||||
{ month: 11, day: 31, name: "New Year's Eve", type: 'international' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Variable holidays - different date each year (lunar calendar based)
|
||||
* Format: { [year]: Array<{ month: 0-11, day: 1-31, name: string, type: string }> }
|
||||
*/
|
||||
export const variableHolidays = {
|
||||
2024: [
|
||||
{ month: 2, day: 25, name: 'Holi', type: 'indian' },
|
||||
{ month: 3, day: 10, name: 'Eid ul-Fitr', type: 'indian' },
|
||||
{ month: 5, day: 17, name: 'Eid ul-Adha', type: 'indian' },
|
||||
{ month: 7, day: 19, name: 'Raksha Bandhan', type: 'indian' },
|
||||
{ month: 7, day: 26, name: 'Janmashtami', type: 'indian' },
|
||||
{ month: 8, day: 7, name: 'Ganesh Chaturthi', type: 'indian' },
|
||||
{ month: 9, day: 12, name: 'Dussehra', type: 'indian' },
|
||||
{ month: 10, day: 1, name: 'Diwali', type: 'indian' },
|
||||
],
|
||||
2025: [
|
||||
{ month: 2, day: 14, name: 'Holi', type: 'indian' },
|
||||
{ month: 2, day: 31, name: 'Eid ul-Fitr', type: 'indian' },
|
||||
{ month: 5, day: 7, name: 'Eid ul-Adha', type: 'indian' },
|
||||
{ month: 7, day: 9, name: 'Raksha Bandhan', type: 'indian' },
|
||||
{ month: 7, day: 16, name: 'Janmashtami', type: 'indian' },
|
||||
{ month: 7, day: 27, name: 'Ganesh Chaturthi', type: 'indian' },
|
||||
{ month: 9, day: 2, name: 'Dussehra', type: 'indian' },
|
||||
{ month: 9, day: 20, name: 'Diwali', type: 'indian' },
|
||||
],
|
||||
2026: [
|
||||
{ month: 2, day: 4, name: 'Holi', type: 'indian' },
|
||||
{ month: 2, day: 20, name: 'Eid ul-Fitr', type: 'indian' },
|
||||
{ month: 4, day: 27, name: 'Eid ul-Adha', type: 'indian' },
|
||||
{ month: 7, day: 28, name: 'Raksha Bandhan', type: 'indian' },
|
||||
{ month: 8, day: 4, name: 'Janmashtami', type: 'indian' },
|
||||
{ month: 8, day: 17, name: 'Ganesh Chaturthi', type: 'indian' },
|
||||
{ month: 8, day: 20, name: 'Dussehra', type: 'indian' },
|
||||
{ month: 10, day: 8, name: 'Diwali', type: 'indian' },
|
||||
],
|
||||
2027: [
|
||||
{ month: 2, day: 22, name: 'Holi', type: 'indian' },
|
||||
{ month: 2, day: 10, name: 'Eid ul-Fitr', type: 'indian' },
|
||||
{ month: 4, day: 17, name: 'Eid ul-Adha', type: 'indian' },
|
||||
{ month: 7, day: 17, name: 'Raksha Bandhan', type: 'indian' },
|
||||
{ month: 7, day: 25, name: 'Janmashtami', type: 'indian' },
|
||||
{ month: 8, day: 6, name: 'Ganesh Chaturthi', type: 'indian' },
|
||||
{ month: 9, day: 9, name: 'Dussehra', type: 'indian' },
|
||||
{ month: 9, day: 28, name: 'Diwali', type: 'indian' },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all holidays for a specific month and year
|
||||
* @param {number} year - Full year (e.g., 2024)
|
||||
* @param {number} month - Month (0-11)
|
||||
* @returns {Map<number, { name: string, type: string }>} - Map of day -> holiday info
|
||||
*/
|
||||
export function getHolidaysForMonth(year, month) {
|
||||
const holidayMap = new Map();
|
||||
|
||||
// Add fixed holidays for this month
|
||||
for (const holiday of fixedHolidays) {
|
||||
if (holiday.month === month) {
|
||||
holidayMap.set(holiday.day, { name: holiday.name, type: holiday.type });
|
||||
}
|
||||
}
|
||||
|
||||
// Add variable holidays for this year and month
|
||||
const yearHolidays = variableHolidays[year];
|
||||
if (yearHolidays) {
|
||||
for (const holiday of yearHolidays) {
|
||||
if (holiday.month === month) {
|
||||
holidayMap.set(holiday.day, { name: holiday.name, type: holiday.type });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return holidayMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific date is a holiday
|
||||
* @param {number} year - Full year
|
||||
* @param {number} month - Month (0-11)
|
||||
* @param {number} day - Day of month (1-31)
|
||||
* @returns {{ name: string, type: string } | null}
|
||||
*/
|
||||
export function getHolidayInfo(year, month, day) {
|
||||
const monthHolidays = getHolidaysForMonth(year, month);
|
||||
return monthHolidays.get(day) || null;
|
||||
}
|
||||
|
|
@ -203,33 +203,31 @@
|
|||
</svelte:head>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 4rem 0;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(135deg, var(--primary), #8b3a92);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.25rem;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.stream-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.stream-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.stream-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.stream-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.stream-card {
|
||||
background: #111;
|
||||
border: 1px solid var(--border);
|
||||
|
|
@ -979,11 +977,6 @@
|
|||
</style>
|
||||
|
||||
<div class="container">
|
||||
<div class="hero">
|
||||
<h1>Live Streams</h1>
|
||||
<p>Watch your favorite streamers live</p>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div style="text-align: center; padding: 2rem;">
|
||||
<p>Loading streams...</p>
|
||||
|
|
@ -1036,6 +1029,61 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Watch Rooms Section - Only show rooms with a video currently playing -->
|
||||
{#if !watchRoomsLoading && watchRooms.filter(r => r.currentVideoTitle).length > 0}
|
||||
<div class="watch-rooms-section">
|
||||
<div class="section-header">
|
||||
<h2>Watch Together</h2>
|
||||
</div>
|
||||
<div class="watch-rooms-grid">
|
||||
{#each watchRooms.filter(r => r.currentVideoTitle) as room}
|
||||
<a href={`/${room.name}/watch`} class="watch-room-card">
|
||||
<div class="watch-room-thumbnail">
|
||||
{#if room.currentThumbnail}
|
||||
<img src={room.currentThumbnail} alt={room.currentVideoTitle || 'Now playing'} />
|
||||
{:else}
|
||||
<div class="watch-room-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="48" height="48">
|
||||
<path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
{#if room.playbackState === 'playing'}
|
||||
<span class="watch-room-live-badge">PLAYING</span>
|
||||
{/if}
|
||||
{#if room.queuedCount > 0}
|
||||
<span class="watch-room-queue-badge">{room.queuedCount} in queue</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="watch-room-info">
|
||||
<h3>{room.name}</h3>
|
||||
{#if room.currentVideoTitle}
|
||||
<div class="watch-room-current" title={room.currentVideoTitle}>
|
||||
{room.currentVideoTitle}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="watch-room-meta">
|
||||
<div class="watch-room-owner">
|
||||
{#if room.avatarUrl}
|
||||
<img src={room.avatarUrl} alt={room.username} class="watch-room-avatar" />
|
||||
{:else}
|
||||
<div class="watch-room-avatar" style="background: {room.colorCode}">
|
||||
{room.username?.charAt(0).toUpperCase() || '?'}
|
||||
</div>
|
||||
{/if}
|
||||
<span style="color: {room.colorCode}">{room.username}</span>
|
||||
</div>
|
||||
{#if room.viewerCount > 0}
|
||||
<span class="watch-room-viewers">{room.viewerCount} watching</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Latest Videos Section -->
|
||||
{#if !videosLoading && videos.length > 0}
|
||||
<div class="videos-section">
|
||||
|
|
@ -1132,61 +1180,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Watch Rooms Section - Only show rooms with a video currently playing -->
|
||||
{#if !watchRoomsLoading && watchRooms.filter(r => r.currentVideoTitle).length > 0}
|
||||
<div class="watch-rooms-section">
|
||||
<div class="section-header">
|
||||
<h2>Watch Together</h2>
|
||||
</div>
|
||||
<div class="watch-rooms-grid">
|
||||
{#each watchRooms.filter(r => r.currentVideoTitle) as room}
|
||||
<a href={`/${room.name}/watch`} class="watch-room-card">
|
||||
<div class="watch-room-thumbnail">
|
||||
{#if room.currentThumbnail}
|
||||
<img src={room.currentThumbnail} alt={room.currentVideoTitle || 'Now playing'} />
|
||||
{:else}
|
||||
<div class="watch-room-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="48" height="48">
|
||||
<path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
{#if room.playbackState === 'playing'}
|
||||
<span class="watch-room-live-badge">PLAYING</span>
|
||||
{/if}
|
||||
{#if room.queuedCount > 0}
|
||||
<span class="watch-room-queue-badge">{room.queuedCount} in queue</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="watch-room-info">
|
||||
<h3>{room.name}</h3>
|
||||
{#if room.currentVideoTitle}
|
||||
<div class="watch-room-current" title={room.currentVideoTitle}>
|
||||
{room.currentVideoTitle}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="watch-room-meta">
|
||||
<div class="watch-room-owner">
|
||||
{#if room.avatarUrl}
|
||||
<img src={room.avatarUrl} alt={room.username} class="watch-room-avatar" />
|
||||
{:else}
|
||||
<div class="watch-room-avatar" style="background: {room.colorCode}">
|
||||
{room.username?.charAt(0).toUpperCase() || '?'}
|
||||
</div>
|
||||
{/if}
|
||||
<span style="color: {room.colorCode}">{room.username}</span>
|
||||
</div>
|
||||
{#if room.viewerCount > 0}
|
||||
<span class="watch-room-viewers">{room.viewerCount} watching</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Latest Ebooks Section -->
|
||||
{#if !ebooksLoading && ebookFiles.length > 0}
|
||||
<div class="ebooks-section">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { connectionStatus } from '$lib/chat/chatStore';
|
||||
import { auth, isAuthenticated } from '$lib/stores/auth';
|
||||
import { getHolidaysForMonth } from '$lib/data/holidays.js';
|
||||
import { siteSettings } from '$lib/stores/siteSettings';
|
||||
import TerminalTabBar from '$lib/components/terminal/TerminalTabBar.svelte';
|
||||
import TerminalCore from '$lib/components/terminal/TerminalCore.svelte';
|
||||
|
|
@ -129,8 +130,20 @@
|
|||
calendarDate.getFullYear() === today.getFullYear();
|
||||
}
|
||||
|
||||
function isHoliday(day) {
|
||||
if (!day) return false;
|
||||
return calendarHolidays.has(day);
|
||||
}
|
||||
|
||||
function getHolidayName(day) {
|
||||
if (!day) return null;
|
||||
const info = calendarHolidays.get(day);
|
||||
return info ? info.name : null;
|
||||
}
|
||||
|
||||
$: calendarDays = getCalendarDays(calendarDate);
|
||||
$: calendarMonthYear = calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||
$: calendarHolidays = getHolidaysForMonth(calendarDate.getFullYear(), calendarDate.getMonth());
|
||||
|
||||
// Timezone definitions
|
||||
const timezones = [
|
||||
|
|
@ -224,7 +237,13 @@
|
|||
</div>
|
||||
<div class="calendar-days">
|
||||
{#each calendarDays as day}
|
||||
<span class="calendar-day" class:today={isToday(day)} class:empty={!day}>
|
||||
<span
|
||||
class="calendar-day"
|
||||
class:today={isToday(day)}
|
||||
class:holiday={isHoliday(day)}
|
||||
class:empty={!day}
|
||||
title={getHolidayName(day) || ''}
|
||||
>
|
||||
{day || ''}
|
||||
</span>
|
||||
{/each}
|
||||
|
|
@ -534,6 +553,24 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.calendar-day.holiday {
|
||||
background: #ff9800;
|
||||
color: #0d1117;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.calendar-day.holiday:not(.today):hover {
|
||||
background: #ffb74d;
|
||||
}
|
||||
|
||||
.calendar-day.today.holiday {
|
||||
background: linear-gradient(135deg, #4caf50 50%, #ff9800 50%);
|
||||
}
|
||||
|
||||
.calendar-day.today.holiday:hover {
|
||||
background: linear-gradient(135deg, #66bb6a 50%, #ffb74d 50%);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue