feat(calendar): add moon phases to DateStrip component

- Add suncalc library for accurate astronomical moon phase calculation
- Display moon phase emojis (🌑🌓🌕🌗) on significant dates
- Show new moon, first quarter, full moon, and last quarter
- Style moon indicators centered above each day
- Add hover effects to "Heute" button

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-12 03:05:53 +01:00
parent 7d03a524fc
commit 74e2dabf76
2 changed files with 90 additions and 62 deletions

View file

@ -19,6 +19,7 @@
"@tailwindcss/vite": "^4.1.7",
"@types/d3-force": "^3.0.0",
"@types/node": "^20.0.0",
"@types/suncalc": "^1.9.2",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^5.0.0",
@ -31,7 +32,6 @@
"dependencies": {
"@calendar/shared": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-types": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-feedback-service": "workspace:*",
@ -44,12 +44,14 @@
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",
"@manacore/shared-types": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"@manacore/shared-utils": "workspace:*",
"@neodrag/svelte": "^2.3.3",
"d3-force": "^3.0.0",
"date-fns": "^4.1.0",
"lucide-svelte": "^0.559.0",
"suncalc": "^1.9.0",
"svelte-dnd-action": "^0.9.68",
"svelte-i18n": "^4.0.1"
},

View file

@ -11,6 +11,42 @@
} from 'date-fns';
import { de } from 'date-fns/locale';
import { onMount, tick } from 'svelte';
import SunCalc from 'suncalc';
// Moon phase emojis (8 phases)
const MOON_EMOJIS = ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'];
// Get moon emoji for a date
function getMoonEmoji(date: Date): string {
const moonData = SunCalc.getMoonIllumination(date);
// phase: 0 = new moon, 0.25 = first quarter, 0.5 = full moon, 0.75 = last quarter
const phaseIndex = Math.floor(moonData.phase * 8) % 8;
return MOON_EMOJIS[phaseIndex];
}
// Check if this is a significant moon phase (new, first quarter, full, last quarter)
function isSignificantMoonPhase(date: Date): { significant: boolean; emoji: string } {
const moonData = SunCalc.getMoonIllumination(date);
const phase = moonData.phase;
// Lunar cycle is ~29.53 days, so 1 day = ~0.0339
// Use half a day tolerance (~0.017) to ensure only 1 day is marked
const tolerance = 0.017;
if (phase < tolerance || phase > 1 - tolerance) {
return { significant: true, emoji: '🌑' }; // New moon
}
if (Math.abs(phase - 0.25) < tolerance) {
return { significant: true, emoji: '🌓' }; // First quarter
}
if (Math.abs(phase - 0.5) < tolerance) {
return { significant: true, emoji: '🌕' }; // Full moon
}
if (Math.abs(phase - 0.75) < tolerance) {
return { significant: true, emoji: '🌗' }; // Last quarter
}
return { significant: false, emoji: '' };
}
// Reactive view range - needed to trigger re-renders
let viewRange = $derived(viewStore.viewRange);
@ -166,25 +202,14 @@
</script>
<div class="date-strip-wrapper">
{#if !isTodayVisible}
<button onclick={goToToday} title="Zum heutigen Tag" class="today-button"> Heute </button>
{/if}
<div class="date-strip-container">
<!-- Month label with today button -->
<!-- Month label -->
<div class="month-header">
<span class="month-label">{visibleMonth}</span>
{#if !isTodayVisible}
<button class="today-btn" onclick={goToToday} title="Zum heutigen Tag">
<svg
class="today-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 6v6l4 2" />
</svg>
Heute
</button>
{/if}
</div>
<!-- Days row -->
@ -197,6 +222,7 @@
{@const dayIsRangeStart = isSameDay(day, viewRange.start)}
{@const dayIsRangeEnd = isSameDay(day, viewRange.end)}
{@const isFirstOfMonth = day.getDate() === 1}
{@const moonPhase = isSignificantMoonPhase(day)}
{#if isFirstOfMonth}
<div class="month-divider"></div>
{/if}
@ -214,6 +240,9 @@
? 'background: #3b82f6; color: white; border-radius: 10px; font-weight: 700; box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);'
: ''}
>
{#if moonPhase.significant}
<span class="moon-indicator">{moonPhase.emoji}</span>
{/if}
<span class="day-weekday" style={dayIsToday ? 'opacity: 1; color: white;' : ''}
>{format(day, 'EE', { locale: de })}</span
>
@ -229,16 +258,38 @@
<style>
.date-strip-wrapper {
position: fixed;
bottom: calc(130px + env(safe-area-inset-bottom, 0px));
bottom: calc(200px + env(safe-area-inset-bottom, 0px));
left: 0;
right: 0;
z-index: 998;
z-index: 48;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
}
.today-button {
padding: 0.25rem 0.75rem;
background: transparent;
border: 1px solid #d1d5db;
border-radius: 9999px;
cursor: pointer;
color: #9ca3af;
font-size: 0.6875rem;
font-weight: 600;
margin-bottom: 0.375rem;
pointer-events: auto;
transition: all 0.2s ease;
}
.today-button:hover {
background: rgba(59, 130, 246, 0.1);
border-color: #3b82f6;
color: #3b82f6;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
}
.date-strip-container {
display: flex;
flex-direction: column;
@ -268,33 +319,6 @@
white-space: nowrap;
}
.today-btn {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.3125rem 0.625rem;
background: var(--color-muted, #f3f4f6);
border: 1.5px solid var(--color-border, #e5e7eb);
border-radius: 9999px;
cursor: pointer;
color: var(--color-muted-foreground, #6b7280);
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
transition: all 0.15s ease;
}
.today-btn:hover {
background: var(--color-surface-hover, #e5e7eb);
color: var(--color-foreground, #1f2937);
border-color: var(--color-border-strong, #d1d5db);
}
.today-icon {
width: 14px;
height: 14px;
}
.month-divider {
width: 1px;
height: 40px;
@ -308,10 +332,12 @@
align-items: center;
gap: 2px;
overflow-x: auto;
overflow-y: visible;
scrollbar-width: none;
-ms-overflow-style: none;
scroll-behavior: auto;
padding: 0.25rem;
padding: 1.25rem 0.25rem 0.25rem;
margin-top: -1rem;
}
.days-scroll::-webkit-scrollbar {
@ -333,6 +359,16 @@
color: var(--color-foreground, #1f2937);
transition: all 0.15s ease;
flex-shrink: 0;
position: relative;
}
.moon-indicator {
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
font-size: 1.125rem;
line-height: 1;
}
.day-item:hover {
@ -396,26 +432,16 @@
font-size: 1rem;
}
.month-header-side {
min-width: 60px;
}
.today-btn {
padding: 0.1875rem 0.5rem;
font-size: 0.6875rem;
gap: 0.25rem;
}
.today-icon {
width: 12px;
height: 12px;
}
.day-item {
min-width: 44px;
height: 52px;
}
.moon-indicator {
font-size: 1rem;
top: -14px;
}
.day-number {
font-size: 1rem;
}