diff --git a/locales/rn/ar.json b/locales/rn/ar.json
index 96ea46ea..3959d574 100644
--- a/locales/rn/ar.json
+++ b/locales/rn/ar.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "اضغط + لإنشاء حدث"
},
+ "fab": {
+ "event": "حدث",
+ "task": "مهمة"
+ },
"form": {
"description_placeholder": "ملاحظات، جدول أعمال…",
"location_placeholder": "المكان أو العنوان",
@@ -127,6 +131,10 @@
"not_found": "لم تعد هذه المهمة متاحة.",
"save_error": "فشل حفظ المهمة",
"update_error": "فشل تحديث المهمة"
+ },
+ "views": {
+ "schedule": "الجدول",
+ "three_days": "3 أيام"
}
},
"common": {
diff --git a/locales/rn/ca.json b/locales/rn/ca.json
index 1f24ae9c..56fee5a3 100644
--- a/locales/rn/ca.json
+++ b/locales/rn/ca.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Toqueu + per crear-ne un"
},
+ "fab": {
+ "event": "Esdeveniment",
+ "task": "Tasca"
+ },
"form": {
"description_placeholder": "Notes, ordre del dia…",
"location_placeholder": "Lloc o adreça",
@@ -127,6 +131,10 @@
"not_found": "Aquesta tasca ja no està disponible.",
"save_error": "No s'ha pogut desar la tasca",
"update_error": "No s'ha pogut actualitzar la tasca"
+ },
+ "views": {
+ "schedule": "Agenda",
+ "three_days": "3 dies"
}
},
"common": {
diff --git a/locales/rn/cs.json b/locales/rn/cs.json
index e33381f9..58583cbe 100644
--- a/locales/rn/cs.json
+++ b/locales/rn/cs.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Událost vytvoříte klepnutím na +"
},
+ "fab": {
+ "event": "Událost",
+ "task": "Úkol"
+ },
"form": {
"description_placeholder": "Poznámky, program…",
"location_placeholder": "Místo nebo adresa",
@@ -127,6 +131,10 @@
"not_found": "Tento úkol už není k dispozici.",
"save_error": "Úkol se nepodařilo uložit",
"update_error": "Úkol se nepodařilo aktualizovat"
+ },
+ "views": {
+ "schedule": "Program",
+ "three_days": "3 dny"
}
},
"common": {
diff --git a/locales/rn/da.json b/locales/rn/da.json
index 3e8ea243..662eb608 100644
--- a/locales/rn/da.json
+++ b/locales/rn/da.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Tryk på + for at oprette en"
},
+ "fab": {
+ "event": "Begivenhed",
+ "task": "Opgave"
+ },
"form": {
"description_placeholder": "Noter, dagsorden…",
"location_placeholder": "Sted eller adresse",
@@ -127,6 +131,10 @@
"not_found": "Denne opgave er ikke længere tilgængelig.",
"save_error": "Kunne ikke gemme opgaven",
"update_error": "Kunne ikke opdatere opgaven"
+ },
+ "views": {
+ "schedule": "Tidsplan",
+ "three_days": "3 dage"
}
},
"common": {
diff --git a/locales/rn/de.json b/locales/rn/de.json
index e738a8d6..58fcba37 100644
--- a/locales/rn/de.json
+++ b/locales/rn/de.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Tippen Sie auf +, um einen zu erstellen"
},
+ "fab": {
+ "event": "Termin",
+ "task": "Aufgabe"
+ },
"form": {
"description_placeholder": "Notizen, Tagesordnung…",
"location_placeholder": "Ort oder Adresse",
@@ -127,6 +131,10 @@
"not_found": "Diese Aufgabe ist nicht mehr verfügbar.",
"save_error": "Aufgabe konnte nicht gespeichert werden",
"update_error": "Aufgabe konnte nicht aktualisiert werden"
+ },
+ "views": {
+ "schedule": "Terminübersicht",
+ "three_days": "3 Tage"
}
},
"common": {
diff --git a/locales/rn/en.json b/locales/rn/en.json
index eedb5336..eebec322 100644
--- a/locales/rn/en.json
+++ b/locales/rn/en.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Tap + to create one"
},
+ "fab": {
+ "event": "Event",
+ "task": "Task"
+ },
"form": {
"description_placeholder": "Notes, agenda…",
"location_placeholder": "Place or address",
@@ -127,6 +131,10 @@
"not_found": "This task is no longer available.",
"save_error": "Failed to save task",
"update_error": "Failed to update task"
+ },
+ "views": {
+ "schedule": "Schedule",
+ "three_days": "3 days"
}
},
"common": {
diff --git a/locales/rn/es.json b/locales/rn/es.json
index ef29641e..5668fe0a 100644
--- a/locales/rn/es.json
+++ b/locales/rn/es.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Toca + para crear uno"
},
+ "fab": {
+ "event": "Evento",
+ "task": "Tarea"
+ },
"form": {
"description_placeholder": "Notas, orden del día…",
"location_placeholder": "Lugar o dirección",
@@ -127,6 +131,10 @@
"not_found": "Esta tarea ya no está disponible.",
"save_error": "No se pudo guardar la tarea",
"update_error": "No se pudo actualizar la tarea"
+ },
+ "views": {
+ "schedule": "Agenda",
+ "three_days": "3 días"
}
},
"common": {
diff --git a/locales/rn/fa.json b/locales/rn/fa.json
index 23d47354..ce458222 100644
--- a/locales/rn/fa.json
+++ b/locales/rn/fa.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "برای ایجاد، روی + ضربه بزنید"
},
+ "fab": {
+ "event": "رویداد",
+ "task": "کار"
+ },
"form": {
"description_placeholder": "یادداشتها، دستور جلسه…",
"location_placeholder": "مکان یا نشانی",
@@ -127,6 +131,10 @@
"not_found": "این وظیفه دیگر در دسترس نیست.",
"save_error": "ذخیره وظیفه ناموفق بود",
"update_error": "بهروزرسانی وظیفه ناموفق بود"
+ },
+ "views": {
+ "schedule": "برنامه",
+ "three_days": "۳ روز"
}
},
"common": {
diff --git a/locales/rn/fr.json b/locales/rn/fr.json
index 226ca38e..67e54653 100644
--- a/locales/rn/fr.json
+++ b/locales/rn/fr.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Appuyez sur + pour en créer un"
},
+ "fab": {
+ "event": "Événement",
+ "task": "Tâche"
+ },
"form": {
"description_placeholder": "Notes, ordre du jour…",
"location_placeholder": "Lieu ou adresse",
@@ -127,6 +131,10 @@
"not_found": "Cette tâche n'est plus disponible.",
"save_error": "Échec de l'enregistrement de la tâche",
"update_error": "Échec de la mise à jour de la tâche"
+ },
+ "views": {
+ "schedule": "Planning",
+ "three_days": "3 jours"
}
},
"common": {
diff --git a/locales/rn/he.json b/locales/rn/he.json
index 74fbf9ad..44a820e2 100644
--- a/locales/rn/he.json
+++ b/locales/rn/he.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "הקש על + כדי ליצור"
},
+ "fab": {
+ "event": "אירוע",
+ "task": "משימה"
+ },
"form": {
"description_placeholder": "הערות, סדר יום…",
"location_placeholder": "מקום או כתובת",
@@ -127,6 +131,10 @@
"not_found": "המשימה הזו אינה זמינה עוד.",
"save_error": "שמירת המשימה נכשלה",
"update_error": "עדכון המשימה נכשל"
+ },
+ "views": {
+ "schedule": "לוח זמנים",
+ "three_days": "3 ימים"
}
},
"common": {
diff --git a/locales/rn/hu.json b/locales/rn/hu.json
index cdf2a9ec..1b04ed44 100644
--- a/locales/rn/hu.json
+++ b/locales/rn/hu.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Esemény létrehozásához koppints a + gombra"
},
+ "fab": {
+ "event": "Esemény",
+ "task": "Feladat"
+ },
"form": {
"description_placeholder": "Jegyzetek, napirend…",
"location_placeholder": "Hely vagy cím",
@@ -127,6 +131,10 @@
"not_found": "Ez a feladat már nem érhető el.",
"save_error": "Nem sikerült menteni a feladatot",
"update_error": "Nem sikerült frissíteni a feladatot"
+ },
+ "views": {
+ "schedule": "Ütemezés",
+ "three_days": "3 nap"
}
},
"common": {
diff --git a/locales/rn/it.json b/locales/rn/it.json
index ac84ea94..785a68ee 100644
--- a/locales/rn/it.json
+++ b/locales/rn/it.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Tocca + per crearne uno"
},
+ "fab": {
+ "event": "Evento",
+ "task": "Attività"
+ },
"form": {
"description_placeholder": "Note, ordine del giorno…",
"location_placeholder": "Luogo o indirizzo",
@@ -127,6 +131,10 @@
"not_found": "Questa attività non è più disponibile.",
"save_error": "Impossibile salvare l'attività",
"update_error": "Impossibile aggiornare l'attività"
+ },
+ "views": {
+ "schedule": "Programma",
+ "three_days": "3 giorni"
}
},
"common": {
diff --git a/locales/rn/ja.json b/locales/rn/ja.json
index a3409565..6e6b7ac5 100644
--- a/locales/rn/ja.json
+++ b/locales/rn/ja.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "+ をタップして作成"
},
+ "fab": {
+ "event": "予定",
+ "task": "タスク"
+ },
"form": {
"description_placeholder": "メモ、議題…",
"location_placeholder": "場所または住所",
@@ -127,6 +131,10 @@
"not_found": "このタスクは利用できなくなりました。",
"save_error": "タスクの保存に失敗しました",
"update_error": "タスクの更新に失敗しました"
+ },
+ "views": {
+ "schedule": "スケジュール",
+ "three_days": "3 日"
}
},
"common": {
diff --git a/locales/rn/ko.json b/locales/rn/ko.json
index c4664b73..6125577b 100644
--- a/locales/rn/ko.json
+++ b/locales/rn/ko.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "+를 눌러 만들어 보세요"
},
+ "fab": {
+ "event": "일정",
+ "task": "할 일"
+ },
"form": {
"description_placeholder": "메모, 안건…",
"location_placeholder": "장소 또는 주소",
@@ -127,6 +131,10 @@
"not_found": "이 할 일을 더 이상 찾을 수 없어요.",
"save_error": "할 일을 저장하지 못했어요",
"update_error": "할 일을 업데이트하지 못했어요"
+ },
+ "views": {
+ "schedule": "일정",
+ "three_days": "3일"
}
},
"common": {
diff --git a/locales/rn/lv.json b/locales/rn/lv.json
index ef5fb809..1f256434 100644
--- a/locales/rn/lv.json
+++ b/locales/rn/lv.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Pieskarieties +, lai izveidotu notikumu"
},
+ "fab": {
+ "event": "Notikums",
+ "task": "Uzdevums"
+ },
"form": {
"description_placeholder": "Piezīmes, darba kārtība…",
"location_placeholder": "Vieta vai adrese",
@@ -127,6 +131,10 @@
"not_found": "Šis uzdevums vairs nav pieejams.",
"save_error": "Neizdevās saglabāt uzdevumu",
"update_error": "Neizdevās atjaunināt uzdevumu"
+ },
+ "views": {
+ "schedule": "Grafiks",
+ "three_days": "3 dienas"
}
},
"common": {
diff --git a/locales/rn/mn.json b/locales/rn/mn.json
index 73ba4774..162422e6 100644
--- a/locales/rn/mn.json
+++ b/locales/rn/mn.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Үүсгэхийн тулд + товшино уу"
},
+ "fab": {
+ "event": "Үйл явдал",
+ "task": "Даалгавар"
+ },
"form": {
"description_placeholder": "Тэмдэглэл, хэлэлцэх асуудал…",
"location_placeholder": "Газар эсвэл хаяг",
@@ -127,6 +131,10 @@
"not_found": "Энэ даалгавар байхгүй болсон.",
"save_error": "Даалгаврыг хадгалж чадсангүй",
"update_error": "Даалгаврыг шинэчилж чадсангүй"
+ },
+ "views": {
+ "schedule": "Хуваарь",
+ "three_days": "3 өдөр"
}
},
"common": {
diff --git a/locales/rn/nb.json b/locales/rn/nb.json
index 16443226..a3754623 100644
--- a/locales/rn/nb.json
+++ b/locales/rn/nb.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Trykk på + for å opprette en"
},
+ "fab": {
+ "event": "Hendelse",
+ "task": "Oppgave"
+ },
"form": {
"description_placeholder": "Notater, agenda…",
"location_placeholder": "Sted eller adresse",
@@ -127,6 +131,10 @@
"not_found": "Denne oppgaven er ikke lenger tilgjengelig.",
"save_error": "Kunne ikke lagre oppgaven",
"update_error": "Kunne ikke oppdatere oppgaven"
+ },
+ "views": {
+ "schedule": "Timeplan",
+ "three_days": "3 dager"
}
},
"common": {
diff --git a/locales/rn/nl.json b/locales/rn/nl.json
index efbca3fd..03d47732 100644
--- a/locales/rn/nl.json
+++ b/locales/rn/nl.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Tik op + om er een te maken"
},
+ "fab": {
+ "event": "Afspraak",
+ "task": "Taak"
+ },
"form": {
"description_placeholder": "Notities, agendapunten…",
"location_placeholder": "Plaats of adres",
@@ -127,6 +131,10 @@
"not_found": "Deze taak is niet meer beschikbaar.",
"save_error": "Taak opslaan mislukt",
"update_error": "Taak bijwerken mislukt"
+ },
+ "views": {
+ "schedule": "Planning",
+ "three_days": "3 dagen"
}
},
"common": {
diff --git a/locales/rn/pl.json b/locales/rn/pl.json
index 42bf12a8..58e8dbf0 100644
--- a/locales/rn/pl.json
+++ b/locales/rn/pl.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Stuknij +, aby utworzyć wydarzenie"
},
+ "fab": {
+ "event": "Wydarzenie",
+ "task": "Zadanie"
+ },
"form": {
"description_placeholder": "Notatki, agenda…",
"location_placeholder": "Miejsce lub adres",
@@ -127,6 +131,10 @@
"not_found": "To zadanie nie jest już dostępne.",
"save_error": "Nie udało się zapisać zadania",
"update_error": "Nie udało się zaktualizować zadania"
+ },
+ "views": {
+ "schedule": "Harmonogram",
+ "three_days": "3 dni"
}
},
"common": {
diff --git a/locales/rn/pt.json b/locales/rn/pt.json
index 84f7918a..ac3bd7bd 100644
--- a/locales/rn/pt.json
+++ b/locales/rn/pt.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Toque em + para criar um"
},
+ "fab": {
+ "event": "Evento",
+ "task": "Tarefa"
+ },
"form": {
"description_placeholder": "Notas, pauta…",
"location_placeholder": "Local ou endereço",
@@ -127,6 +131,10 @@
"not_found": "Esta tarefa não está mais disponível.",
"save_error": "Falha ao salvar a tarefa",
"update_error": "Falha ao atualizar a tarefa"
+ },
+ "views": {
+ "schedule": "Programação",
+ "three_days": "3 dias"
}
},
"common": {
diff --git a/locales/rn/ro.json b/locales/rn/ro.json
index 1cab3248..064a8c8d 100644
--- a/locales/rn/ro.json
+++ b/locales/rn/ro.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Atingeți + pentru a crea unul"
},
+ "fab": {
+ "event": "Eveniment",
+ "task": "Sarcină"
+ },
"form": {
"description_placeholder": "Note, agendă…",
"location_placeholder": "Loc sau adresă",
@@ -127,6 +131,10 @@
"not_found": "Această sarcină nu mai este disponibilă.",
"save_error": "Sarcina nu a putut fi salvată",
"update_error": "Sarcina nu a putut fi actualizată"
+ },
+ "views": {
+ "schedule": "Program",
+ "three_days": "3 zile"
}
},
"common": {
diff --git a/locales/rn/ru.json b/locales/rn/ru.json
index 9e88464e..c2bea6d7 100644
--- a/locales/rn/ru.json
+++ b/locales/rn/ru.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Нажмите +, чтобы создать"
},
+ "fab": {
+ "event": "Мероприятие",
+ "task": "Задача"
+ },
"form": {
"description_placeholder": "Заметки, повестка…",
"location_placeholder": "Место или адрес",
@@ -127,6 +131,10 @@
"not_found": "Эта задача больше недоступна.",
"save_error": "Не удалось сохранить задачу",
"update_error": "Не удалось обновить задачу"
+ },
+ "views": {
+ "schedule": "Расписание",
+ "three_days": "3 дня"
}
},
"common": {
diff --git a/locales/rn/sk.json b/locales/rn/sk.json
index b06666e8..089de996 100644
--- a/locales/rn/sk.json
+++ b/locales/rn/sk.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Udalosť vytvoríte klepnutím na +"
},
+ "fab": {
+ "event": "Udalosť",
+ "task": "Úloha"
+ },
"form": {
"description_placeholder": "Poznámky, program…",
"location_placeholder": "Miesto alebo adresa",
@@ -127,6 +131,10 @@
"not_found": "Táto úloha už nie je k dispozícii.",
"save_error": "Úlohu sa nepodarilo uložiť",
"update_error": "Úlohu sa nepodarilo aktualizovať"
+ },
+ "views": {
+ "schedule": "Program",
+ "three_days": "3 dni"
}
},
"common": {
diff --git a/locales/rn/tr.json b/locales/rn/tr.json
index e14810c6..4fcba8b5 100644
--- a/locales/rn/tr.json
+++ b/locales/rn/tr.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Etkinlik oluşturmak için + simgesine dokunun"
},
+ "fab": {
+ "event": "Etkinlik",
+ "task": "Görev"
+ },
"form": {
"description_placeholder": "Notlar, gündem…",
"location_placeholder": "Yer veya adres",
@@ -127,6 +131,10 @@
"not_found": "Bu görev artık kullanılamıyor.",
"save_error": "Görev kaydedilemedi",
"update_error": "Görev güncellenemedi"
+ },
+ "views": {
+ "schedule": "Program",
+ "three_days": "3 gün"
}
},
"common": {
diff --git a/locales/rn/uk.json b/locales/rn/uk.json
index 0e0d71a3..cfe4df86 100644
--- a/locales/rn/uk.json
+++ b/locales/rn/uk.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "Натисніть +, щоб створити"
},
+ "fab": {
+ "event": "Подія",
+ "task": "Завдання"
+ },
"form": {
"description_placeholder": "Нотатки, порядок денний…",
"location_placeholder": "Місце або адреса",
@@ -127,6 +131,10 @@
"not_found": "Це завдання більше недоступне.",
"save_error": "Не вдалося зберегти завдання",
"update_error": "Не вдалося оновити завдання"
+ },
+ "views": {
+ "schedule": "Розклад",
+ "three_days": "3 дні"
}
},
"common": {
diff --git a/locales/rn/zh-TW.json b/locales/rn/zh-TW.json
index a93623de..05981e80 100644
--- a/locales/rn/zh-TW.json
+++ b/locales/rn/zh-TW.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "點選 + 即可建立"
},
+ "fab": {
+ "event": "活動",
+ "task": "工作"
+ },
"form": {
"description_placeholder": "備註、議程…",
"location_placeholder": "地點或地址",
@@ -127,6 +131,10 @@
"not_found": "此任務已不存在。",
"save_error": "儲存任務失敗",
"update_error": "更新任務失敗"
+ },
+ "views": {
+ "schedule": "行程",
+ "three_days": "3 天"
}
},
"common": {
diff --git a/locales/rn/zh.json b/locales/rn/zh.json
index 5cdb8d6a..3f275e9a 100644
--- a/locales/rn/zh.json
+++ b/locales/rn/zh.json
@@ -27,6 +27,10 @@
"export_ics_short": ".ics",
"tap_to_create": "点按 + 创建"
},
+ "fab": {
+ "event": "活动",
+ "task": "任务"
+ },
"form": {
"description_placeholder": "备注、议程…",
"location_placeholder": "地点或地址",
@@ -127,6 +131,10 @@
"not_found": "此任务已不存在。",
"save_error": "保存任务失败",
"update_error": "更新任务失败"
+ },
+ "views": {
+ "schedule": "日程",
+ "three_days": "3 天"
}
},
"common": {
diff --git a/src/components/calendar/CalendarFab.tsx b/src/components/calendar/CalendarFab.tsx
new file mode 100644
index 00000000..c6783d5b
--- /dev/null
+++ b/src/components/calendar/CalendarFab.tsx
@@ -0,0 +1,153 @@
+import React from 'react';
+import { Animated, Easing, Pressable, StyleSheet, Text, View } from 'react-native';
+import { CalendarPlus, CircleCheck, Plus } from 'lucide-react-native';
+import { componentSizes, radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
+import { useColors } from '../../theme/colors';
+import { useAnimDuration } from '../../theme/dynamic';
+import { useLocaleStore } from '../../stores/locale-store';
+
+interface CalendarFabProps {
+ onNewEvent: () => void;
+ /** With tasks turned on, the button first offers "Event" and "Task". */
+ onNewTask?: () => void;
+}
+
+/**
+ * The floating create button in the corner of the calendar, styled like the
+ * mail list's compose button. Without tasks it creates an event at once;
+ * with tasks it opens a small menu over a scrim.
+ */
+export function CalendarFab({ onNewEvent, onNewTask }: CalendarFabProps) {
+ const c = useColors();
+ const styles = React.useMemo(() => makeStyles(c), [c]);
+ const t = useLocaleStore((s) => s.t);
+ const [open, setOpen] = React.useState(false);
+ const progress = React.useRef(new Animated.Value(0)).current;
+ const duration = useAnimDuration(160);
+
+ React.useEffect(() => {
+ Animated.timing(progress, {
+ toValue: open ? 1 : 0,
+ duration,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }).start();
+ }, [open, progress, duration]);
+
+ const pick = (action: () => void) => {
+ setOpen(false);
+ action();
+ };
+
+ const onMainPress = () => {
+ if (!onNewTask) {
+ onNewEvent();
+ return;
+ }
+ if (open) pick(onNewEvent);
+ else setOpen(true);
+ };
+
+ const rotate = progress.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '90deg'] });
+ const lift = progress.interpolate({ inputRange: [0, 1], outputRange: [12, 0] });
+
+ return (
+ <>
+ {open && (
+
+ setOpen(false)}
+ accessibilityRole="button"
+ accessibilityLabel={t('common.close', 'Close')}
+ />
+
+ )}
+
+ {open && onNewTask && (
+
+ {t('calendar.fab.task', 'Task')}
+ pick(onNewTask)}
+ accessibilityRole="button"
+ accessibilityLabel={t('calendar.events.new_task', 'New task')}
+ >
+
+
+
+ )}
+
+ {open && (
+
+ {t('calendar.fab.event', 'Event')}
+
+ )}
+ [styles.fab, pressed && styles.fabPressed]}
+ onPress={onMainPress}
+ accessibilityRole="button"
+ accessibilityLabel={t('calendar.events.new_event', 'New event')}
+ >
+ {open ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ >
+ );
+}
+
+function makeStyles(c: ThemePalette) {
+ return StyleSheet.create({
+ scrim: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0,0,0,0.55)',
+ zIndex: 20,
+ },
+ anchor: {
+ position: 'absolute',
+ right: spacing.lg,
+ bottom: spacing.lg,
+ alignItems: 'flex-end',
+ gap: spacing.lg,
+ zIndex: 21,
+ },
+ option: { flexDirection: 'row', alignItems: 'center', gap: spacing.md, marginRight: 8 },
+ mainRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.md },
+ optionLabel: { ...typography.baseMedium, color: '#ffffff' },
+ miniFab: {
+ width: 40,
+ height: 40,
+ borderRadius: radius.full,
+ backgroundColor: c.surface,
+ alignItems: 'center',
+ justifyContent: 'center',
+ elevation: 3,
+ shadowColor: '#000',
+ shadowOpacity: 0.2,
+ shadowRadius: 4,
+ shadowOffset: { width: 0, height: 2 },
+ },
+ // Same as the compose button on the mail list (EmailListScreen).
+ fab: {
+ width: componentSizes.fab,
+ height: componentSizes.fab,
+ borderRadius: radius.full,
+ backgroundColor: c.text,
+ alignItems: 'center',
+ justifyContent: 'center',
+ elevation: 6,
+ shadowColor: '#000',
+ shadowOpacity: 0.25,
+ shadowRadius: 6,
+ shadowOffset: { width: 0, height: 3 },
+ },
+ fabPressed: { opacity: 0.85 },
+ });
+}
diff --git a/src/components/calendar/CalendarSidebarDrawer.tsx b/src/components/calendar/CalendarSidebarDrawer.tsx
index c332b42f..0f43d235 100644
--- a/src/components/calendar/CalendarSidebarDrawer.tsx
+++ b/src/components/calendar/CalendarSidebarDrawer.tsx
@@ -12,7 +12,8 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
- Check, X, Upload, Rss, Shuffle, Star, Plus, Pencil, Share2, Eraser, Trash2,
+ Check, X, Upload, Rss, Shuffle, Star, Plus, Pencil, Share2, Eraser, Trash2, RefreshCw,
+ type LucideIcon,
} from 'lucide-react-native';
import type { Calendar } from '../../api/types';
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
@@ -23,8 +24,19 @@ import { BIRTHDAY_CALENDAR_ID } from '../../lib/birthday-calendar';
import { isWritableCalendar } from '../../lib/calendar-editability';
import { useLocaleStore } from '../../stores/locale-store';
+/** A calendar view listed at the top of the drawer. */
+export interface DrawerViewOption {
+ key: string;
+ label: string;
+ Icon: LucideIcon;
+}
+
interface CalendarSidebarDrawerProps {
visible: boolean;
+ views?: DrawerViewOption[];
+ activeView?: string;
+ onSelectView?: (key: string) => void;
+ onRefresh?: () => void;
calendars: Calendar[];
hiddenCalendarIds: string[];
onToggle: (id: string) => void;
@@ -48,6 +60,10 @@ interface CalendarSidebarDrawerProps {
export function CalendarSidebarDrawer({
visible,
+ views,
+ activeView,
+ onSelectView,
+ onRefresh,
calendars,
hiddenCalendarIds,
onToggle,
@@ -161,10 +177,44 @@ export function CalendarSidebarDrawer({
>
- {t('calendar.my_calendars', 'Calendars')}
+ {t('calendar.title', 'Calendar')}
+ {views && views.length > 0 && (
+
+ {views.map(({ key, label, Icon }) => {
+ const active = key === activeView;
+ return (
+ onSelectView?.(key)}
+ style={({ pressed }) => [
+ styles.viewRow,
+ active && styles.viewRowActive,
+ pressed && !active && styles.rowPressed,
+ ]}
+ accessibilityRole="button"
+ accessibilityState={{ selected: active }}
+ >
+
+ {label}
+
+ );
+ })}
+ {onRefresh && (
+ [styles.viewRow, styles.refreshRow, pressed && styles.rowPressed]}
+ accessibilityRole="button"
+ >
+
+ {t('common.refresh', 'Refresh')}
+
+ )}
+
+ )}
+
{calendars.length === 0 && (
{t('calendar.drawer.no_calendars', 'No calendars yet.')}
)}
@@ -431,6 +481,27 @@ function makeStyles(c: ThemePalette) {
headerTitle: { ...typography.h3, color: c.text },
scroll: { flex: 1 },
+
+ viewsSection: {
+ paddingTop: spacing.sm,
+ paddingBottom: spacing.sm,
+ borderBottomWidth: 1,
+ borderBottomColor: c.border,
+ },
+ // Same rows as the mail drawer's folders (SidebarDrawer).
+ viewRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: spacing.md,
+ paddingHorizontal: spacing.lg,
+ minHeight: 44,
+ borderLeftWidth: 2,
+ borderLeftColor: 'transparent',
+ },
+ viewRowActive: { backgroundColor: c.accent, borderLeftColor: c.primary },
+ refreshRow: { marginTop: spacing.xs },
+ viewText: { ...typography.body, color: c.text },
+ viewTextActive: { ...typography.bodySemibold },
scrollContent: { paddingBottom: spacing.lg },
empty: {
diff --git a/src/components/calendar/MonthDropdown.tsx b/src/components/calendar/MonthDropdown.tsx
new file mode 100644
index 00000000..fda002cf
--- /dev/null
+++ b/src/components/calendar/MonthDropdown.tsx
@@ -0,0 +1,125 @@
+import React from 'react';
+import {
+ FlatList,
+ StyleSheet,
+ View,
+ useWindowDimensions,
+ type ListRenderItemInfo,
+ type NativeScrollEvent,
+ type NativeSyntheticEvent,
+} from 'react-native';
+import { addMonths, startOfMonth } from 'date-fns';
+import type { Calendar } from '../../api/types';
+import type { ThemePalette } from '../../theme/tokens';
+import { useColors } from '../../theme/colors';
+import type { EventDayIndex } from '../../lib/calendar-utils';
+import { MONTH_ROW_HEIGHT, MonthView } from './MonthView';
+
+type WeekStart = 0 | 1 | 6;
+
+/** Months either side of the one the panel opens on. */
+const MONTHS_EACH_SIDE = 24;
+/** Six week rows plus the weekday initials. */
+const PANEL_HEIGHT = MONTH_ROW_HEIGHT * 6 + 22;
+
+interface MonthDropdownProps {
+ /** The month the panel opens on. */
+ month: Date;
+ selectedDate: Date;
+ eventsByDay: EventDayIndex;
+ calendars: Calendar[];
+ weekStartsOn: WeekStart;
+ /** Reports the month swiped to, so the title can follow it. */
+ onMonthChange: (month: Date) => void;
+ onSelectDate: (date: Date) => void;
+}
+
+/**
+ * The month that drops down under the calendar title: swipe sideways
+ * between months, tap a day to go there.
+ */
+export function MonthDropdown({
+ month,
+ selectedDate,
+ eventsByDay,
+ calendars,
+ weekStartsOn,
+ onMonthChange,
+ onSelectDate,
+}: MonthDropdownProps) {
+ const c = useColors();
+ const styles = React.useMemo(() => makeStyles(c), [c]);
+ const { width } = useWindowDimensions();
+ // The months are fixed while the panel is open; it remounts on each open.
+ const [months] = React.useState(() => {
+ const first = addMonths(startOfMonth(month), -MONTHS_EACH_SIDE);
+ return Array.from({ length: MONTHS_EACH_SIDE * 2 + 1 }, (_, i) => addMonths(first, i));
+ });
+ const lastIndexRef = React.useRef(MONTHS_EACH_SIDE);
+
+ const handleMomentumEnd = React.useCallback(
+ (e: NativeSyntheticEvent) => {
+ const index = Math.round(e.nativeEvent.contentOffset.x / width);
+ if (index === lastIndexRef.current || !months[index]) return;
+ lastIndexRef.current = index;
+ onMonthChange(months[index]);
+ },
+ [width, months, onMonthChange],
+ );
+
+ const renderItem = React.useCallback(
+ ({ item }: ListRenderItemInfo) => (
+
+
+
+ ),
+ [width, selectedDate, eventsByDay, calendars, weekStartsOn, onSelectDate],
+ );
+
+ const getItemLayout = React.useCallback(
+ (_: ArrayLike | null | undefined, i: number) => ({ length: width, offset: width * i, index: i }),
+ [width],
+ );
+
+ return (
+
+
+
+ );
+}
+
+function monthKey(month: Date): string {
+ return `${month.getFullYear()}-${month.getMonth()}`;
+}
+
+function makeStyles(c: ThemePalette) {
+ return StyleSheet.create({
+ panel: {
+ backgroundColor: c.background,
+ borderBottomWidth: 1,
+ borderBottomColor: c.border,
+ paddingTop: 4,
+ },
+ });
+}
diff --git a/src/components/calendar/TasksSheet.tsx b/src/components/calendar/TasksSheet.tsx
index 55d96dd7..ad8864a2 100644
--- a/src/components/calendar/TasksSheet.tsx
+++ b/src/components/calendar/TasksSheet.tsx
@@ -52,6 +52,8 @@ interface TasksSheetProps {
timeFormat?: TimeFormat;
// Task to open in the editor when the sheet appears (from a calendar chip).
initialTaskId?: string | null;
+ // Open straight on an empty editor (the calendar's "new task" button).
+ startNew?: boolean;
onClose: () => void;
onCreate: (task: Partial, calendarId: string) => Promise | void;
onUpdate?: (id: string, changes: Partial) => Promise | void;
@@ -94,6 +96,7 @@ export function TasksSheet({
calendars,
timeFormat,
initialTaskId,
+ startNew = false,
onClose,
onCreate,
onUpdate,
@@ -132,10 +135,10 @@ export function TasksSheet({
setExpanded(true);
} else {
setEditor(emptyTaskEditor(defaultCalendarId));
- setExpanded(false);
+ setExpanded(startNew);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [visible, initialTaskId]);
+ }, [visible, initialTaskId, startNew]);
React.useEffect(() => {
if (visible && !editor.calendarId && defaultCalendarId) {
diff --git a/src/components/calendar/TimeGridScrollView.tsx b/src/components/calendar/TimeGridScrollView.tsx
index 8ef195be..ef3d3972 100644
--- a/src/components/calendar/TimeGridScrollView.tsx
+++ b/src/components/calendar/TimeGridScrollView.tsx
@@ -37,6 +37,7 @@ import { isInactiveEvent } from '../../lib/calendar-participants';
import { eventBlockColors } from '../../lib/event-colors';
import {
buildAllDaySegments,
+ columnsPerScreen,
headerColumnRange,
hourAtOffset,
timeGridFocusColumn,
@@ -119,7 +120,7 @@ function TimeGridScrollViewInner({
[eventsByDay, events],
);
- const perScreen = mode === 'week' ? 7 : 1;
+ const perScreen = columnsPerScreen(mode);
const { width: screenWidth } = useWindowDimensions();
const [rootWidth, setRootWidth] = React.useState(screenWidth);
const listWidth = Math.max(perScreen, rootWidth - GUTTER_WIDTH);
diff --git a/src/lib/calendar-time-grid.ts b/src/lib/calendar-time-grid.ts
index 5a1800df..3fff4fb1 100644
--- a/src/lib/calendar-time-grid.ts
+++ b/src/lib/calendar-time-grid.ts
@@ -16,7 +16,12 @@ import type { DayRange, WeekStartsOn } from './calendar-scroll-window';
* window, scrolling sideways and snapping to whole days.
*/
-export type TimeGridMode = 'week' | 'day';
+export type TimeGridMode = 'week' | '3day' | 'day';
+
+/** Day columns on one screen. */
+export function columnsPerScreen(mode: TimeGridMode): number {
+ return mode === 'week' ? 7 : mode === '3day' ? 3 : 1;
+}
/** Every day of the window, in order. */
export function windowDays(window: DayRange): Date[] {
@@ -28,7 +33,8 @@ export function windowDays(window: DayRange): Date[] {
/**
* The column navigation aligns with the start of the viewport: the focused
- * week's first day in the week view, the focused day in the day view.
+ * week's first day in the week view, the focused day in the day and
+ * three-day views.
*/
export function timeGridFocusColumn(
window: DayRange,
diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx
index 3b7bb051..51408188 100644
--- a/src/screens/CalendarScreen.tsx
+++ b/src/screens/CalendarScreen.tsx
@@ -13,20 +13,21 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
+ Calendar as CalendarIcon,
+ CalendarDays,
+ ChevronDown,
ChevronLeft,
ChevronRight,
- Plus,
- Calendar1,
- CalendarDays,
- LayoutGrid,
- List as ListIcon,
+ Columns3,
+ Columns4,
+ Grid3x3,
ListChecks,
Menu,
+ Rows2,
+ Square,
} from 'lucide-react-native';
import {
format,
- startOfWeek,
- endOfWeek,
addDays,
subDays,
addMonths,
@@ -39,7 +40,6 @@ import { useCalendarLocale } from '../lib/calendar-locale';
import { displayNow, isDisplayToday } from '../lib/calendar-timezone';
import { spacing, radius, typography, type ThemePalette } from '../theme/tokens';
import { useColors } from '../theme/colors';
-import { Button } from '../components';
import { useCalendarStore } from '../stores/calendar-store';
import { useSettingsStore } from '../stores/settings-store';
import { MonthView } from '../components/calendar/MonthView';
@@ -54,7 +54,9 @@ import {
RecurrenceScopeDialog,
type RecurrenceEditScope,
} from '../components/calendar/RecurrenceScopeDialog';
-import { CalendarSidebarDrawer } from '../components/calendar/CalendarSidebarDrawer';
+import { CalendarSidebarDrawer, type DrawerViewOption } from '../components/calendar/CalendarSidebarDrawer';
+import { CalendarFab } from '../components/calendar/CalendarFab';
+import { MonthDropdown } from '../components/calendar/MonthDropdown';
import { TasksSheet } from '../components/calendar/TasksSheet';
import { ICalImportSheet } from '../components/calendar/ICalImportSheet';
import { ICalSubscriptionSheet } from '../components/calendar/ICalSubscriptionSheet';
@@ -71,6 +73,7 @@ import {
scrollWindowLoadRange,
windowStateForJump,
type CalendarFocus,
+ type ScrollViewMode,
type ScrollWindowOptions,
type ScrollWindowState,
} from '../lib/calendar-scroll-window';
@@ -107,7 +110,18 @@ import { shareEventICS } from '../lib/calendar-ics-export';
import * as Clipboard from 'expo-clipboard';
import type { Calendar, CalendarEvent, RecurrenceRule } from '../api/types';
-type ViewMode = 'month' | 'week' | 'day' | 'agenda';
+type ViewMode = 'month' | 'week' | '3day' | 'day' | 'agenda';
+
+// The three-day grid scrolls through the same window of days as the day view.
+function windowMode(mode: ViewMode): ScrollViewMode {
+ return mode === '3day' ? 'day' : mode;
+}
+
+// A window without free scrolling: one period, three days for the three-day grid.
+function fixedWindowFor(mode: ViewMode, anchor: Date): ScrollWindowState {
+ const state = fixedScrollWindowState(windowMode(mode), anchor);
+ return mode === '3day' ? { ...state, after: 2 } : state;
+}
type PendingAction =
| {
kind: 'edit';
@@ -129,28 +143,10 @@ type PendingAction =
// the next arrow press or edge usually finds them there already.
const RANGE_MARGIN_DAYS = 14;
-type WeekStart = 0 | 1 | 6;
-
-// `firstDay`: the scrolled week grid reports the first column in view; the
-// title then spans the seven days from there instead of the focused week.
-function headerTitle(
- viewMode: ViewMode,
- currentDate: Date,
- weekStartsOn: WeekStart,
- locale: Locale,
- firstDay?: Date | null,
-): string {
- if (viewMode === 'month') return format(currentDate, 'MMMM yyyy', { locale });
- if (viewMode === 'day') return format(currentDate, 'EEE, MMM d, yyyy', { locale });
- if (viewMode === 'week') {
- const start = firstDay ?? startOfWeek(currentDate, { weekStartsOn });
- const end = firstDay ? addDays(firstDay, 6) : endOfWeek(currentDate, { weekStartsOn });
- if (start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear()) {
- return `${format(start, 'MMM d', { locale })} – ${format(end, 'd, yyyy', { locale })}`;
- }
- return `${format(start, 'MMM d', { locale })} – ${format(end, 'MMM d, yyyy', { locale })}`;
- }
- return format(currentDate, 'MMMM yyyy', { locale });
+// The month on screen; the year only when it isn't this year.
+function headerTitle(date: Date, locale: Locale): string {
+ const sameYear = date.getFullYear() === displayNow().getFullYear();
+ return format(date, sameYear ? 'LLLL' : 'LLLL yyyy', { locale });
}
export default function CalendarScreen() {
@@ -201,13 +197,14 @@ export default function CalendarScreen() {
// The sideways-scrolling week and day grids assume a left-to-right strip;
// right-to-left layouts keep them paged.
const freeScroll =
- calendarFreeScroll && ((viewMode !== 'week' && viewMode !== 'day') || !I18nManager.isRTL);
+ calendarFreeScroll
+ && ((viewMode !== 'week' && viewMode !== '3day' && viewMode !== 'day') || !I18nManager.isRTL);
const focusKey = dayKey(focus.date);
const activeWindowState = React.useMemo(
() =>
freeScroll
- ? normalizeScrollWindowState(windowState, viewMode, parseDayKey(focusKey))
- : fixedScrollWindowState(viewMode, parseDayKey(focusKey)),
+ ? normalizeScrollWindowState(windowState, windowMode(viewMode), parseDayKey(focusKey))
+ : fixedWindowFor(viewMode, parseDayKey(focusKey)),
[freeScroll, windowState, viewMode, focusKey],
);
React.useEffect(() => {
@@ -218,7 +215,7 @@ export default function CalendarScreen() {
[activeWindowState, windowOptions],
);
// Changes whenever a fresh window starts; the views remount on it.
- const windowKey = `${activeWindowState.mode}:${activeWindowState.anchorKey}`;
+ const windowKey = `${viewMode}:${activeWindowState.anchorKey}`;
const loadRange = React.useMemo(() => {
const { after, before } = scrollWindowLoadRange(scrollWindow, RANGE_MARGIN_DAYS);
return { after: after.toISOString(), before: before.toISOString() };
@@ -467,7 +464,7 @@ export default function CalendarScreen() {
setSelectedDate(date);
setVisibleDate(null);
setFocus((prev) => ({ date, nonce: prev.nonce + 1 }));
- setWindowState((prev) => windowStateForJump(prev, viewMode, date, windowOptions));
+ setWindowState((prev) => windowStateForJump(prev, windowMode(viewMode), date, windowOptions));
},
[viewMode, windowOptions],
);
@@ -479,7 +476,7 @@ export default function CalendarScreen() {
(side: 'before' | 'after') => {
setLoadingEdge(side === 'before' ? 'start' : 'end');
setWindowState((prev) =>
- growScrollWindow(normalizeScrollWindowState(prev, viewMode, focus.date), side),
+ growScrollWindow(normalizeScrollWindowState(prev, windowMode(viewMode), focus.date), side),
);
},
[viewMode, focus.date],
@@ -493,6 +490,7 @@ export default function CalendarScreen() {
const base = visibleDate ?? focus.date;
jumpTo(
viewMode === 'week' ? subWeeks(base, 1)
+ : viewMode === '3day' ? subDays(base, 3)
: viewMode === 'day' ? subDays(base, 1)
: subMonths(base, 1),
);
@@ -502,6 +500,7 @@ export default function CalendarScreen() {
const base = visibleDate ?? focus.date;
jumpTo(
viewMode === 'week' ? addWeeks(base, 1)
+ : viewMode === '3day' ? addDays(base, 3)
: viewMode === 'day' ? addDays(base, 1)
: addMonths(base, 1),
);
@@ -519,11 +518,48 @@ export default function CalendarScreen() {
setViewMode(mode);
setVisibleDate(null);
setFocus((prev) => ({ date, nonce: prev.nonce + 1 }));
- setWindowState(freshScrollWindowState(mode, date));
+ setWindowState(freshScrollWindowState(windowMode(mode), date));
},
[viewMode, visibleDate, focus.date],
);
+ // The views, listed at the top of the drawer.
+ const viewOptions = React.useMemo(
+ () => [
+ { key: 'agenda', label: t('calendar.views.schedule', 'Schedule'), Icon: Rows2 },
+ { key: 'day', label: t('calendar.views.day', 'Day'), Icon: Square },
+ { key: '3day', label: t('calendar.views.three_days', '3 days'), Icon: Columns3 },
+ { key: 'week', label: t('calendar.views.week', 'Week'), Icon: Columns4 },
+ { key: 'month', label: t('calendar.views.month', 'Month'), Icon: Grid3x3 },
+ ],
+ [t],
+ );
+
+ // The month that drops down under the title; the title follows the month
+ // swiped to while it is open.
+ const [monthPanelOpen, setMonthPanelOpen] = React.useState(false);
+ const [panelMonth, setPanelMonth] = React.useState(null);
+ const toggleMonthPanel = React.useCallback(() => {
+ setPanelMonth(null);
+ setMonthPanelOpen((open) => !open);
+ }, []);
+ const handlePanelSelect = React.useCallback(
+ (date: Date) => {
+ setMonthPanelOpen(false);
+ setPanelMonth(null);
+ jumpTo(date);
+ },
+ [jumpTo],
+ );
+
+ // New tasks open the tasks sheet on an empty editor.
+ const [newTask, setNewTask] = React.useState(false);
+ const openNewTask = React.useCallback(() => {
+ setTasksInitialId(null);
+ setNewTask(true);
+ setTasksVisible(true);
+ }, []);
+
// Picking a day only moves the selection; the view stays where it is.
const handleSelectDate = React.useCallback((date: Date) => {
setSelectedDate(date);
@@ -923,6 +959,7 @@ export default function CalendarScreen() {
);
const isSelectedToday = isDisplayToday(selectedDate);
+ const titleDate = (monthPanelOpen && panelMonth) || visibleDate || focus.date;
return (
@@ -936,96 +973,83 @@ export default function CalendarScreen() {
>
-
- {/* Four view buttons leave less room: shrink a long title instead of wrapping it. */}
-
- {headerTitle(
- viewMode,
- visibleDate ?? focus.date,
- calendarFirstDayOfWeek,
- locale,
- viewMode === 'week' && freeScroll ? visibleDate : null,
- )}
+
+
+ {headerTitle(titleDate, locale)}
-
- {isSelectedToday
- ? t('calendar.views.today', 'Today')
- : format(selectedDate, 'EEE, MMM d', { locale })}
-
-
+
+
+
+
+ {/* Without free scrolling the views don't swipe: keep arrows. */}
+ {!freeScroll && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+ {
+ setMonthPanelOpen(false);
+ goToday();
+ }}
+ hitSlop={4}
+ accessibilityRole="button"
+ accessibilityLabel={t('calendar.events.go_to_today', 'Go to today')}
+ >
+
+ {format(displayNow(), 'd')}
+
{enableCalendarTasks && (
setTasksVisible(true)}
- hitSlop={6}
+ hitSlop={4}
accessibilityRole="button"
accessibilityLabel={t('calendar.views.tasks', 'Tasks')}
>
-
+
)}
-
- {(['month', 'week', 'day', 'agenda'] as ViewMode[]).map((mode) => {
- const Icon =
- mode === 'month' ? LayoutGrid
- : mode === 'week' ? CalendarDays
- : mode === 'day' ? Calendar1
- : ListIcon;
- const active = viewMode === mode;
- const label =
- mode === 'month' ? t('calendar.views.month', 'Month')
- : mode === 'week' ? t('calendar.views.week', 'Week')
- : mode === 'day' ? t('calendar.views.day', 'Day')
- : t('calendar.views.agenda', 'Agenda');
- return (
- changeViewMode(mode)}
- accessibilityRole="button"
- accessibilityLabel={label}
- accessibilityState={{ selected: active }}
- >
-
-
- );
- })}
-
- openCreate(selectedDate)}
- accessibilityRole="button"
- accessibilityLabel={t('calendar.events.new_event', 'New event')}
- >
-
-
-
-
-
-
-
-
-
-
-
+ {monthPanelOpen && (
+
+ )}
{error && (
@@ -1071,10 +1095,10 @@ export default function CalendarScreen() {
onLongPressDate={openCreate}
/>
)}
- {(viewMode === 'day' || (viewMode === 'week' && freeScroll)) && (
+ {(viewMode === 'day' || viewMode === '3day' || (viewMode === 'week' && freeScroll)) && (
+ openCreate(selectedDate)}
+ onNewTask={enableCalendarTasks ? openNewTask : undefined}
+ />
+
{
+ setSidebarVisible(false);
+ setMonthPanelOpen(false);
+ changeViewMode(key as ViewMode);
+ }}
+ onRefresh={() => {
+ setSidebarVisible(false);
+ void onRefresh();
+ }}
calendars={eventCalendars}
hiddenCalendarIds={hiddenCalendarIds}
onToggle={toggleCalendarVisibility}
@@ -1222,7 +1262,8 @@ export default function CalendarScreen() {
calendars={taskSheetCalendars}
timeFormat={calendarTimeFormat}
initialTaskId={tasksInitialId}
- onClose={() => { setTasksVisible(false); setTasksInitialId(null); }}
+ startNew={newTask}
+ onClose={() => { setTasksVisible(false); setTasksInitialId(null); setNewTask(false); }}
onCreate={createTask}
onUpdate={updateTask}
onToggle={handleToggleTask}
@@ -1330,57 +1371,38 @@ function makeStyles(c: ThemePalette) {
header: {
flexDirection: 'row',
alignItems: 'center',
- paddingHorizontal: spacing.md,
+ paddingHorizontal: spacing.sm,
paddingVertical: spacing.sm,
gap: spacing.sm,
},
headerBtn: {
- width: 36,
- height: 36,
+ width: 40,
+ height: 40,
alignItems: 'center',
justifyContent: 'center',
borderRadius: radius.full,
},
- headerLeft: { flex: 1 },
- headerTitle: { ...typography.h3, color: c.text },
- headerSubtitle: { ...typography.caption, color: c.textMuted, marginTop: 2 },
- headerActions: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
- viewToggle: {
- flexDirection: 'row',
- backgroundColor: c.surface,
- borderRadius: radius.md,
- padding: 2,
- },
- viewToggleBtn: {
- width: 30,
- height: 32,
- alignItems: 'center',
- justifyContent: 'center',
- borderRadius: radius.sm,
- },
- viewToggleBtnActive: { backgroundColor: c.background },
- fab: {
- width: 36,
- height: 36,
- borderRadius: radius.full,
- backgroundColor: c.primary,
- alignItems: 'center',
- justifyContent: 'center',
- },
-
- nav: {
+ titleBtn: {
+ flex: 1,
flexDirection: 'row',
alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: spacing.xs,
- gap: spacing.lg,
+ gap: 2,
+ minHeight: 40,
},
- navBtn: {
- width: 36,
- height: 36,
- alignItems: 'center',
- justifyContent: 'center',
- borderRadius: radius.full,
+ headerTitle: { ...typography.h3, color: c.text, flexShrink: 1 },
+ chevronOpen: { transform: [{ rotate: '180deg' }] },
+ headerActions: { flexDirection: 'row', alignItems: 'center', gap: 2 },
+ // Today's date inside the calendar icon, below its rings.
+ todayIconText: {
+ position: 'absolute',
+ top: 15,
+ left: 0,
+ right: 0,
+ textAlign: 'center',
+ fontSize: 10,
+ lineHeight: 12,
+ fontWeight: '700',
+ color: c.text,
},
errorBanner: {