Initial ui redo progress #1

Merged
breadone merged 23 commits from ui-redo into main 2026-02-17 18:29:59 +13:00
27 changed files with 746 additions and 321 deletions
Showing only changes of commit a61b342c4c - Show all commits

View File

@@ -17,13 +17,15 @@
"db:studio": "drizzle-kit studio"
},
"devDependencies": {
"@lucide/svelte": "^0.563.1",
"@internationalized/date": "^3.11.0",
"@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-node": "^5.5.2",
"@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^22",
"bits-ui": "^2.15.5",
"clsx": "^2.1.1",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",

105
pnpm-lock.yaml generated
View File

@@ -45,9 +45,12 @@ importers:
specifier: ^1.32.2
version: 1.32.2(vue-template-compiler@2.7.16)(vue@2.7.16)
devDependencies:
'@internationalized/date':
specifier: ^3.11.0
version: 3.11.0
'@lucide/svelte':
specifier: ^0.563.1
version: 0.563.1(svelte@5.49.0)
specifier: ^0.561.0
version: 0.561.0(svelte@5.49.0)
'@sveltejs/adapter-node':
specifier: ^5.5.2
version: 5.5.2(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))
@@ -66,6 +69,9 @@ importers:
'@types/node':
specifier: ^22
version: 22.19.7
bits-ui:
specifier: ^2.15.5
version: 2.15.5(@internationalized/date@3.11.0)(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)
clsx:
specifier: ^2.1.1
version: 2.1.1
@@ -596,6 +602,9 @@ packages:
peerDependencies:
'@fullcalendar/core': ~6.1.20
'@internationalized/date@3.11.0':
resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -612,8 +621,8 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@lucide/svelte@0.563.1':
resolution: {integrity: sha512-Kt+MbnE5D9RsuI/csmf7M+HWxALe57x3A0DhQ8pPnnUpneh7zuldrYjlT+veWtk+tVnp5doQtaAAxLujzIlhBw==}
'@lucide/svelte@0.561.0':
resolution: {integrity: sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A==}
peerDependencies:
svelte: ^5
@@ -828,6 +837,9 @@ packages:
svelte: ^5.0.0
vite: ^6.3.0 || ^7.0.0
'@swc/helpers@0.5.18':
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
'@tailwindcss/forms@0.5.11':
resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==}
peerDependencies:
@@ -1108,6 +1120,13 @@ packages:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
bits-ui@2.15.5:
resolution: {integrity: sha512-WhS+P+E//ClLfKU6KqjKC17nGDRLnz+vkwoP6ClFUPd5m1fFVDxTElPX8QVsduLj5V1KFDxlnv6sW2G5Lqk+vw==}
engines: {node: '>=20'}
peerDependencies:
'@internationalized/date': ^3.8.1
svelte: ^5.33.0
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -1148,6 +1167,10 @@ packages:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -1430,6 +1453,10 @@ packages:
locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1594,6 +1621,15 @@ packages:
peerDependencies:
svelte: ^5.7.0
runed@0.35.1:
resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==}
peerDependencies:
'@sveltejs/kit': ^2.21.0
svelte: ^5.7.0
peerDependenciesMeta:
'@sveltejs/kit':
optional: true
sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@@ -1631,6 +1667,12 @@ packages:
svelte: ^4.0.0 || ^5.0.0-next.0
typescript: '>=5.0.0'
svelte-toolbelt@0.10.6:
resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==}
engines: {node: '>=18', pnpm: '>=8.7.0'}
peerDependencies:
svelte: ^5.30.2
svelte-toolbelt@0.7.1:
resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==}
engines: {node: '>=18', pnpm: '>=8.7.0'}
@@ -1641,6 +1683,9 @@ packages:
resolution: {integrity: sha512-Fn2mCc3XX0gnnbBYzWOTrZHi5WnF9KvqmB1+KGlUWoJkdioPmFYtg2ALBr6xl2dcnFTz3Vi7/mHpbKSVg/imVg==}
engines: {node: '>=18'}
tabbable@6.4.0:
resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
@@ -1681,6 +1726,9 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
@@ -2045,6 +2093,10 @@ snapshots:
'@fullcalendar/core': 6.1.20
'@fullcalendar/daygrid': 6.1.20(@fullcalendar/core@6.1.20)
'@internationalized/date@3.11.0':
dependencies:
'@swc/helpers': 0.5.18
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -2064,7 +2116,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@lucide/svelte@0.563.1(svelte@5.49.0)':
'@lucide/svelte@0.561.0(svelte@5.49.0)':
dependencies:
svelte: 5.49.0
@@ -2235,6 +2287,10 @@ snapshots:
vite: 7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)
vitefu: 1.1.1(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2))
'@swc/helpers@0.5.18':
dependencies:
tslib: 2.8.1
'@tailwindcss/forms@0.5.11(tailwindcss@4.1.18)':
dependencies:
mini-svg-data-uri: 1.4.4
@@ -2507,6 +2563,19 @@ snapshots:
axobject-query@4.1.0: {}
bits-ui@2.15.5(@internationalized/date@3.11.0)(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0):
dependencies:
'@floating-ui/core': 1.7.4
'@floating-ui/dom': 1.7.5
'@internationalized/date': 3.11.0
esm-env: 1.2.2
runed: 0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)
svelte: 5.49.0
svelte-toolbelt: 0.10.6(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)
tabbable: 6.4.0
transitivePeerDependencies:
- '@sveltejs/kit'
buffer-from@1.1.2: {}
chokidar@4.0.3:
@@ -2531,6 +2600,8 @@ snapshots:
deepmerge@4.3.1: {}
dequal@2.0.3: {}
detect-libc@2.1.2: {}
devalue@5.6.2: {}
@@ -2765,6 +2836,8 @@ snapshots:
locate-character@3.0.0: {}
lz-string@1.5.0: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -2979,6 +3052,15 @@ snapshots:
esm-env: 1.2.2
svelte: 5.49.0
runed@0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0):
dependencies:
dequal: 2.0.3
esm-env: 1.2.2
lz-string: 1.5.0
svelte: 5.49.0
optionalDependencies:
'@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2))
sade@1.8.1:
dependencies:
mri: 1.2.0
@@ -3018,6 +3100,15 @@ snapshots:
transitivePeerDependencies:
- picomatch
svelte-toolbelt@0.10.6(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0):
dependencies:
clsx: 2.1.1
runed: 0.35.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.49.0)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.49.0)
style-to-object: 1.0.14
svelte: 5.49.0
transitivePeerDependencies:
- '@sveltejs/kit'
svelte-toolbelt@0.7.1(svelte@5.49.0):
dependencies:
clsx: 2.1.1
@@ -3043,6 +3134,8 @@ snapshots:
magic-string: 0.30.21
zimmerframe: 1.1.4
tabbable@6.4.0: {}
tailwind-merge@3.4.0: {}
tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18):
@@ -3093,6 +3186,8 @@ snapshots:
totalist@3.0.1: {}
tslib@2.8.1: {}
tw-animate-css@1.4.0: {}
typescript@5.9.3: {}

View File

@@ -1,309 +1,29 @@
<script lang="ts">
import Calendar from "$lib/components/ui/calendar/calendar.svelte";
import CalendarDay from "$lib/components/ui/calendar/calendar-day.svelte";
import { CalendarDate } from "@internationalized/date";
let {
year = $bindable("2026"),
month = $bindable("0"),
dayClickCallback = $bindable(() => {}),
entries,
} = $props();
value = $bindable<CalendarDate | undefined>(new CalendarDate(2026, 2, 1)),
} = $props()
const headers = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
let getImageForDate = async (dateObject) => {
const date = `${dateObject.year}-${dateObject.month}-${dateObject.day}`;
const res = await fetch(`/api/entry?date=${date}`);
const data = await res.json();
let days = $state([]);
let expanded = $state(false);
let currentYear = $state(parseInt(year));
let currentMonth = $state(parseInt(month));
console.log(date)
function toISODate(year: number, month: number, date: number) {
return `${year}-${String(month + 1).padStart(2, '0')}-${String(date).padStart(2, '0')}`;
return data.image ? data.image : "noi"
}
function getEntryForDay(year: number, month: number, date: number) {
if (!entries) return undefined;
const dateStr = toISODate(year, month, date);
return entries.find((e) => e.date.split("T")[0] === dateStr);
}
function createDayObject(date: Date, isCurrentMonth: boolean) {
const today = new Date();
const entry = getEntryForDay(
date.getFullYear(),
date.getMonth(),
date.getDate(),
);
return {
date: date.getDate(),
month: date.getMonth(),
year: date.getFullYear(),
iso: toISODate(date.getFullYear(), date.getMonth(), date.getDate()),
dayName: headers[date.getDay() === 0 ? 6 : date.getDay() - 1],
isCurrentMonth,
isToday: date.toDateString() === today.toDateString(),
entry,
};
}
function getCurrentWeek() {
const today = new Date();
const diff = today.getDay() === 0 ? -6 : 1 - today.getDay();
const monday = new Date(today);
monday.setDate(today.getDate() + diff);
return Array.from({ length: 7 }, (_, i) => {
const day = new Date(monday);
day.setDate(monday.getDate() + i);
return createDayObject(day, day.getMonth() === today.getMonth());
});
}
function generateMonth(year: number, month: number) {
const firstDay = new Date(year, month, 1);
const startDay = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const prevMonthDays = new Date(year, month, 0).getDate();
const days = [];
// Previous month
for (let i = startDay - 1; i >= 0; i--) {
const date = new Date(year, month, 0);
date.setDate(prevMonthDays - i);
days.push(createDayObject(date, false));
}
// Current month
for (let i = 1; i <= daysInMonth; i++) {
days.push(createDayObject(new Date(year, month, i), true));
}
// Next month
const remaining = 42 - days.length;
for (let i = 1; i <= remaining; i++) {
days.push(createDayObject(new Date(year, month + 1, i), false));
}
return days;
}
$effect(() => {
days = expanded
? generateMonth(currentYear, currentMonth)
: getCurrentWeek();
});
function toggleExpanded() {
expanded = !expanded;
if (expanded) {
const today = new Date();
currentYear = today.getFullYear();
currentMonth = today.getMonth();
}
}
function previousMonth() {
if (!expanded) return;
currentMonth === 0
? ((currentMonth = 11), currentYear--)
: currentMonth--;
}
function nextMonth() {
if (!expanded) return;
currentMonth === 11
? ((currentMonth = 0), currentYear++)
: currentMonth++;
}
</script>
<div class="calendar-container">
<div class="calendar-header">
<button
class="expand-btn"
onclick={toggleExpanded}
title={expanded ? "Show week view" : "Show month view"}
>
{expanded ? "" : "+"}
</button>
{#if expanded}
<button class="nav-btn" onclick={previousMonth}></button>
<span class="month-year"
>{monthNames[currentMonth]} {currentYear}</span
>
<button class="nav-btn" onclick={nextMonth}></button>
{:else}
<span class="month-year">Current Week</span>
{/if}
</div>
<div class="calendar">
{#each days as day, index}
<button
class="day hover:brightness-80"
class:other-month={!day.isCurrentMonth}
class:today={day.isToday}
class:has-image={day.entry?.image}
onclick={day.entry?.id
? () => dayClickCallback(true, day.entry.id)
: () => dayClickCallback(false, day.iso)}
style={day.entry?.image
? `background-image: url(${day.entry.image}); background-size: cover; background-position: center;`
: ""}
>
{#if day.entry?.image}
<div class="image-overlay"></div>
{/if}
<div class="day-header">
{#if index < 7}
<span class="day-name">{day.dayName}</span>
{/if}
<span class="date">{day.date}</span>
{#if day.entry && !day.entry.image}
<span class="entry-indicator"></span>
{/if}
</div>
</button>
{/each}
</div>
</div>
<style>
.calendar-container {
width: 100%;
}
.calendar-header {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: rgba(166, 168, 179, 0.05);
border-bottom: 1px solid rgba(166, 168, 179, 0.12);
}
.expand-btn,
.nav-btn {
border: 1px solid rgba(166, 168, 179, 0.2);
background: transparent;
color: #009fb7;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.expand-btn {
width: 36px;
height: 36px;
font-size: 24px;
}
.nav-btn {
width: 32px;
height: 32px;
font-size: 20px;
}
.expand-btn:hover,
.nav-btn:hover {
background: rgba(0, 159, 183, 0.1);
border-color: #009fb7;
}
.month-year {
font-size: 18px;
font-weight: 600;
color: #009fb7;
flex: 1;
text-align: center;
}
.calendar {
display: grid;
width: 100%;
grid-template-columns: repeat(7, minmax(120px, 1fr));
grid-auto-rows: 120px;
overflow: auto;
}
.day {
border-bottom: 1px solid rgba(166, 168, 179, 0.12);
border-right: 1px solid rgba(166, 168, 179, 0.12);
padding: 14px 20px;
box-sizing: border-box;
color: #98a0a6;
position: relative;
}
.day.has-image {
background-color: transparent;
}
.image-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 1;
}
.day-header {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 2;
}
.day-name {
font-size: 12px;
text-transform: uppercase;
color: #009fb7;
font-weight: 500;
}
.date {
font-size: 18px;
font-weight: 600;
letter-spacing: 1px;
}
.entry-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #4a9eff;
margin-left: 8px;
}
.day.has-image .day-name,
.day.has-image .date {
color: white;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
.day.other-month {
opacity: 0.3;
}
.day.today {
background: rgba(0, 159, 183, 0.1);
}
.day.today .date {
color: #009fb7;
font-weight: 700;
}
</style>
<Calendar type="single" bind:value class="rounded-lg border shadow-sm [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]">
{#snippet day({ day })}
<CalendarDay>
{day.day}
<img src={getImageForDate(day)} alt="Image for {day}" />
</CalendarDay>
{/snippet}
</Calendar>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import ThemeModeButton from "$lib/components/ui/ThemeModeButton.svelte";
import ThemeModeButton from "$lib/components/ThemeModeButton.svelte";
</script>
<div class="flex w-full h-14 px-2 text-2xl rounded-xl mb-4 bg-[#009FB7] place-items-center">

View File

@@ -0,0 +1,76 @@
<script lang="ts">
import type { ComponentProps } from "svelte";
import type Calendar from "./calendar.svelte";
import CalendarMonthSelect from "./calendar-month-select.svelte";
import CalendarYearSelect from "./calendar-year-select.svelte";
import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date";
let {
captionLayout,
months,
monthFormat,
years,
yearFormat,
month,
locale,
placeholder = $bindable(),
monthIndex = 0,
}: {
captionLayout: ComponentProps<typeof Calendar>["captionLayout"];
months: ComponentProps<typeof CalendarMonthSelect>["months"];
monthFormat: ComponentProps<typeof CalendarMonthSelect>["monthFormat"];
years: ComponentProps<typeof CalendarYearSelect>["years"];
yearFormat: ComponentProps<typeof CalendarYearSelect>["yearFormat"];
month: DateValue;
placeholder: DateValue | undefined;
locale: string;
monthIndex: number;
} = $props();
function formatYear(date: DateValue) {
const dateObj = date.toDate(getLocalTimeZone());
if (typeof yearFormat === "function") return yearFormat(dateObj.getFullYear());
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
}
function formatMonth(date: DateValue) {
const dateObj = date.toDate(getLocalTimeZone());
if (typeof monthFormat === "function") return monthFormat(dateObj.getMonth() + 1);
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
}
</script>
{#snippet MonthSelect()}
<CalendarMonthSelect
{months}
{monthFormat}
value={month.month}
onchange={(e) => {
if (!placeholder) return;
const v = Number.parseInt(e.currentTarget.value);
const newPlaceholder = placeholder.set({ month: v });
placeholder = newPlaceholder.subtract({ months: monthIndex });
}}
/>
{/snippet}
{#snippet YearSelect()}
<CalendarYearSelect {years} {yearFormat} value={month.year} />
{/snippet}
{#if captionLayout === "dropdown"}
{@render MonthSelect()}
{@render YearSelect()}
{:else if captionLayout === "dropdown-months"}
{@render MonthSelect()}
{#if placeholder}
{formatYear(placeholder)}
{/if}
{:else if captionLayout === "dropdown-years"}
{#if placeholder}
{formatMonth(placeholder)}
{/if}
{@render YearSelect()}
{:else}
{formatMonth(month)} {formatYear(month)}
{/if}

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.CellProps = $props();
</script>
<CalendarPrimitive.Cell
bind:ref
class={cn(
"relative size-(--cell-size) p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-s-md [&:last-child[data-selected]_[data-bits-day]]:rounded-e-md",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
import { Calendar as CalendarPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.DayProps = $props();
</script>
<CalendarPrimitive.Day
bind:ref
class={cn(
buttonVariants({ variant: "ghost" }),
"flex size-(--cell-size) flex-col items-center justify-center gap-1 p-0 leading-none font-normal whitespace-nowrap select-none",
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground",
"data-[selected]:bg-primary dark:data-[selected]:hover:bg-accent/50 data-[selected]:text-primary-foreground",
// Outside months
"[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground",
// Disabled
"data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
// Unavailable
"data-[unavailable]:text-muted-foreground data-[unavailable]:line-through",
// hover
"dark:hover:text-accent-foreground",
// focus
"focus:border-ring focus:ring-ring/50 focus:relative",
// inner spans
"[&>span]:text-xs [&>span]:opacity-70",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridBodyProps = $props();
</script>
<CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} />

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridHeadProps = $props();
</script>
<CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} />

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridRowProps = $props();
</script>
<CalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridProps = $props();
</script>
<CalendarPrimitive.Grid
bind:ref
class={cn("mt-4 flex w-full border-collapse flex-col gap-1", className)}
{...restProps}
/>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeadCellProps = $props();
</script>
<CalendarPrimitive.HeadCell
bind:ref
class={cn(
"text-muted-foreground w-(--cell-size) rounded-md text-[0.8rem] font-normal",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeaderProps = $props();
</script>
<CalendarPrimitive.Header
bind:ref
class={cn(
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeadingProps = $props();
</script>
<CalendarPrimitive.Heading
bind:ref
class={cn("px-(--cell-size) text-sm font-medium", className)}
{...restProps}
/>

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
let {
ref = $bindable(null),
class: className,
value,
onchange,
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.MonthSelectProps> = $props();
</script>
<span
class={cn(
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
className
)}
>
<CalendarPrimitive.MonthSelect
bind:ref
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
{...restProps}
>
{#snippet child({ props, monthItems, selectedMonthItem })}
<select {...props} {value} {onchange}>
{#each monthItems as monthItem (monthItem.value)}
<option
value={monthItem.value}
selected={value !== undefined
? monthItem.value === value
: monthItem.value === selectedMonthItem.value}
>
{monthItem.label}
</option>
{/each}
</select>
<span
class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
aria-hidden="true"
>
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
<ChevronDownIcon class="size-4" />
</span>
{/snippet}
</CalendarPrimitive.MonthSelect>
</span>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import { type WithElementRef, cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div {...restProps} bind:this={ref} class={cn("flex flex-col", className)}>
{@render children?.()}
</div>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("relative flex flex-col gap-4 md:flex-row", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<nav
{...restProps}
bind:this={ref}
class={cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", className)}
>
{@render children?.()}
</nav>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
variant = "ghost",
...restProps
}: CalendarPrimitive.NextButtonProps & {
variant?: ButtonVariant;
} = $props();
</script>
{#snippet Fallback()}
<ChevronRightIcon class="size-4" />
{/snippet}
<CalendarPrimitive.NextButton
bind:ref
class={cn(
buttonVariants({ variant }),
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
className
)}
children={children || Fallback}
{...restProps}
/>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
variant = "ghost",
...restProps
}: CalendarPrimitive.PrevButtonProps & {
variant?: ButtonVariant;
} = $props();
</script>
{#snippet Fallback()}
<ChevronLeftIcon class="size-4" />
{/snippet}
<CalendarPrimitive.PrevButton
bind:ref
class={cn(
buttonVariants({ variant }),
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
className
)}
children={children || Fallback}
{...restProps}
/>

View File

@@ -0,0 +1,47 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
let {
ref = $bindable(null),
class: className,
value,
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.YearSelectProps> = $props();
</script>
<span
class={cn(
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
className
)}
>
<CalendarPrimitive.YearSelect
bind:ref
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
{...restProps}
>
{#snippet child({ props, yearItems, selectedYearItem })}
<select {...props} {value}>
{#each yearItems as yearItem (yearItem.value)}
<option
value={yearItem.value}
selected={value !== undefined
? yearItem.value === value
: yearItem.value === selectedYearItem.value}
>
{yearItem.label}
</option>
{/each}
</select>
<span
class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
aria-hidden="true"
>
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
<ChevronDownIcon class="size-4" />
</span>
{/snippet}
</CalendarPrimitive.YearSelect>
</span>

View File

@@ -0,0 +1,115 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import * as Calendar from "./index.js";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ButtonVariant } from "../button/button.svelte";
import { isEqualMonth, type DateValue } from "@internationalized/date";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
value = $bindable(),
placeholder = $bindable(),
class: className,
weekdayFormat = "short",
buttonVariant = "ghost",
captionLayout = "label",
locale = "en-NZ",
months: monthsProp,
years,
monthFormat: monthFormatProp,
yearFormat = "numeric",
day,
disableDaysOutsideMonth = false,
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
buttonVariant?: ButtonVariant;
captionLayout?: "dropdown" | "dropdown-months" | "dropdown-years" | "label";
months?: CalendarPrimitive.MonthSelectProps["months"];
years?: CalendarPrimitive.YearSelectProps["years"];
monthFormat?: CalendarPrimitive.MonthSelectProps["monthFormat"];
yearFormat?: CalendarPrimitive.YearSelectProps["yearFormat"];
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
} = $props();
const monthFormat = $derived.by(() => {
if (monthFormatProp) return monthFormatProp;
if (captionLayout.startsWith("dropdown")) return "short";
return "long";
});
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<CalendarPrimitive.Root
bind:value={value as never}
bind:ref
bind:placeholder
{weekdayFormat}
{disableDaysOutsideMonth}
class={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
className
)}
{locale}
{monthFormat}
{yearFormat}
{...restProps}
>
{#snippet children({ months, weekdays })}
<Calendar.Months>
<Calendar.Nav>
<Calendar.PrevButton variant={buttonVariant} />
<Calendar.NextButton variant={buttonVariant} />
</Calendar.Nav>
{#each months as month, monthIndex (month)}
<Calendar.Month>
<Calendar.Header>
<Calendar.Caption
{captionLayout}
months={monthsProp}
{monthFormat}
{years}
{yearFormat}
month={month.value}
bind:placeholder
{locale}
{monthIndex}
/>
</Calendar.Header>
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow class="select-none">
{#each weekdays as weekday (weekday)}
<Calendar.HeadCell>
{weekday.slice(0, 2)}
</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as weekDates (weekDates)}
<Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date (date)}
<Calendar.Cell {date} month={month.value}>
{#if day}
{@render day({
day: date,
outsideMonth: !isEqualMonth(date, month.value),
})}
{:else}
<Calendar.Day />
{/if}
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
</Calendar.Month>
{/each}
</Calendar.Months>
{/snippet}
</CalendarPrimitive.Root>

View File

@@ -0,0 +1,40 @@
import Root from "./calendar.svelte";
import Cell from "./calendar-cell.svelte";
import Day from "./calendar-day.svelte";
import Grid from "./calendar-grid.svelte";
import Header from "./calendar-header.svelte";
import Months from "./calendar-months.svelte";
import GridRow from "./calendar-grid-row.svelte";
import Heading from "./calendar-heading.svelte";
import GridBody from "./calendar-grid-body.svelte";
import GridHead from "./calendar-grid-head.svelte";
import HeadCell from "./calendar-head-cell.svelte";
import NextButton from "./calendar-next-button.svelte";
import PrevButton from "./calendar-prev-button.svelte";
import MonthSelect from "./calendar-month-select.svelte";
import YearSelect from "./calendar-year-select.svelte";
import Month from "./calendar-month.svelte";
import Nav from "./calendar-nav.svelte";
import Caption from "./calendar-caption.svelte";
export {
Day,
Cell,
Grid,
Header,
Months,
GridRow,
Heading,
GridBody,
GridHead,
HeadCell,
NextButton,
PrevButton,
Nav,
Month,
YearSelect,
MonthSelect,
Caption,
//
Root as Calendar,
};

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
import Header from '$lib/components/header.svelte';
import { ModeWatcher } from "mode-watcher"
let { children } = $props();
@@ -9,8 +8,7 @@
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
<div class="md:max-w-2/3 mx-auto">
<Header />
<div class="md:max-w-3/4 mx-auto">
<ModeWatcher />
{@render children()}
</div>

View File

@@ -1,16 +1,23 @@
<script lang="ts">
import { CalendarDate } from "@internationalized/date";
const { data } = $props();
import Header from '$lib/components/header.svelte';
import Calendar from "$lib/components/calendar.svelte";
import Editor from "$lib/components/editor/index.svelte";
import EntrySummaryView from "$lib/components/entrySummaryView.svelte";
import AddEntryCover from "$lib/components/addEntryCover.svelte";
import { today } from "$lib/date";
let edit = $state(false);
let selectedEntry = $state("");
let selectedDate = $state(today());
let dateValue = $state(new CalendarDate(2026, 1, 1));
let selectEntry = async (hasEntry: boolean, data: string | null) => {
// data should be the entryID if hasEntry == true, or the date if there is no entry
if (hasEntry) {
@@ -26,10 +33,11 @@
// $effect(() => {selectedEntry = ''})
</script>
<Calendar entries={data.all} dayClickCallback={selectEntry} />
<div class="flex flex-col md:flex-row space-y-4 w-full mt-6">
<div class="md:w-1/2 md:order-2">
<Header />
{#if selectedEntry}
{#key selectedEntry.id}
<Editor bind:entry={selectedEntry} />
@@ -41,7 +49,8 @@
{/if}
</div>
<div class="md:w-1/2 md:mr-4">
<div class="md:w-1/2 space-y-4 md:mr-4">
<Calendar bind:value={dateValue} />
<ul>
{#each data.all as entry}
<EntrySummaryView