From 1af62006550d47c35797b67b2afe1359361b4d3c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 4 Dec 2025 13:24:47 +0200 Subject: [PATCH 01/83] chore(react/launch_bar): get launch bar to render React widgets --- .../src/widgets/containers/{launcher.ts => launcher.tsx} | 8 ++++---- apps/client/src/widgets/launch_bar/BookmarkButtons.tsx | 5 +++++ .../client/src/widgets/launch_bar/RightDropdownButton.tsx | 3 +++ apps/client/src/widgets/launch_bar/launch_bar_widget.ts | 3 +++ 4 files changed, 15 insertions(+), 4 deletions(-) rename apps/client/src/widgets/containers/{launcher.ts => launcher.tsx} (94%) create mode 100644 apps/client/src/widgets/launch_bar/BookmarkButtons.tsx create mode 100644 apps/client/src/widgets/launch_bar/RightDropdownButton.tsx create mode 100644 apps/client/src/widgets/launch_bar/launch_bar_widget.ts diff --git a/apps/client/src/widgets/containers/launcher.ts b/apps/client/src/widgets/containers/launcher.tsx similarity index 94% rename from apps/client/src/widgets/containers/launcher.ts rename to apps/client/src/widgets/containers/launcher.tsx index e1bfc5a8b..41d7fee9d 100644 --- a/apps/client/src/widgets/containers/launcher.ts +++ b/apps/client/src/widgets/containers/launcher.tsx @@ -1,9 +1,8 @@ import CalendarWidget from "../buttons/calendar.js"; import SpacerWidget from "../spacer.js"; -import BookmarkButtons from "../bookmark_buttons.js"; import ProtectedSessionStatusWidget from "../buttons/protected_session_status.js"; import SyncStatusWidget from "../sync_status.js"; -import BasicWidget from "../basic_widget.js"; +import BasicWidget, { wrapReactWidgets } from "../basic_widget.js"; import NoteLauncher from "../buttons/launcher/note_launcher.js"; import ScriptLauncher from "../buttons/launcher/script_launcher.js"; import CommandButtonWidget from "../buttons/command_button.js"; @@ -14,6 +13,7 @@ import QuickSearchLauncherWidget from "../quick_search_launcher.js"; import type FNote from "../../entities/fnote.js"; import type { CommandNames } from "../../components/app_context.js"; import AiChatButton from "../buttons/ai_chat_button.js"; +import BookmarkButtons from "../launch_bar/BookmarkButtons.jsx"; interface InnerWidget extends BasicWidget { settings?: { @@ -64,7 +64,7 @@ export default class LauncherWidget extends BasicWidget { } else if (launcherType === "customWidget") { widget = await this.initCustomWidget(note); } else if (launcherType === "builtinWidget") { - widget = this.initBuiltinWidget(note); + widget = wrapReactWidgets([ this.initBuiltinWidget(note) ])[0]; } else { throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${note.noteId}' title '${note.title}'`); } @@ -111,7 +111,7 @@ export default class LauncherWidget extends BasicWidget { return new SpacerWidget(baseSize, growthFactor); case "bookmarks": - return new BookmarkButtons(this.isHorizontalLayout); + return case "protectedSession": return new ProtectedSessionStatusWidget(); case "syncStatus": diff --git a/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx b/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx new file mode 100644 index 000000000..4d6abec7d --- /dev/null +++ b/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx @@ -0,0 +1,5 @@ +import type { LaunchBarWidgetProps } from "./launch_bar_widget"; + +export default function BookmarkButtons({ }: LaunchBarWidgetProps) { + return

Bookmarks goes here.

; +} diff --git a/apps/client/src/widgets/launch_bar/RightDropdownButton.tsx b/apps/client/src/widgets/launch_bar/RightDropdownButton.tsx new file mode 100644 index 000000000..e47b5624c --- /dev/null +++ b/apps/client/src/widgets/launch_bar/RightDropdownButton.tsx @@ -0,0 +1,3 @@ +export default function RightDropdownButton() { + return

Button goes here.

; +} diff --git a/apps/client/src/widgets/launch_bar/launch_bar_widget.ts b/apps/client/src/widgets/launch_bar/launch_bar_widget.ts new file mode 100644 index 000000000..36284797d --- /dev/null +++ b/apps/client/src/widgets/launch_bar/launch_bar_widget.ts @@ -0,0 +1,3 @@ +export interface LaunchBarWidgetProps { + isHorizontalLayout: boolean; +} From 48cbb80e790b4032a53656ab12d2ed6ca621f389 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 4 Dec 2025 14:18:04 +0200 Subject: [PATCH 02/83] chore(react/launch_bar): port open_note_button_widget --- apps/client/src/services/utils.ts | 2 +- apps/client/src/widgets/bookmark_buttons.ts | 8 +-- .../buttons/open_note_button_widget.ts | 49 ------------- .../widgets/launch_bar/BookmarkButtons.tsx | 70 ++++++++++++++++++- .../client/src/widgets/react/ActionButton.tsx | 8 +-- apps/client/src/widgets/react/hooks.tsx | 13 ++++ 6 files changed, 87 insertions(+), 63 deletions(-) delete mode 100644 apps/client/src/widgets/buttons/open_note_button_widget.ts diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 2045cd4d7..e6da60ae5 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -150,7 +150,7 @@ export function isMac() { export const hasTouchBar = (isMac() && isElectron()); -function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent | JQueryEventObject) { +export function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent | JQueryEventObject) { return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); } diff --git a/apps/client/src/widgets/bookmark_buttons.ts b/apps/client/src/widgets/bookmark_buttons.ts index b32393c9a..88ff3c7e9 100644 --- a/apps/client/src/widgets/bookmark_buttons.ts +++ b/apps/client/src/widgets/bookmark_buttons.ts @@ -1,5 +1,4 @@ import FlexContainer from "./containers/flex_container.js"; -import OpenNoteButtonWidget from "./buttons/open_note_button_widget.js"; import BookmarkFolderWidget from "./buttons/bookmark_folder.js"; import froca from "../services/froca.js"; import utils from "../services/utils.js"; @@ -23,10 +22,6 @@ export default class BookmarkButtons extends FlexContainer { } async refresh(): Promise { - this.$widget.empty(); - this.children = []; - this.noteIds = []; - const bookmarkParentNote = await froca.getNote("_lbBookmarks"); if (!bookmarkParentNote) { @@ -37,8 +32,7 @@ export default class BookmarkButtons extends FlexContainer { this.noteIds.push(note.noteId); let buttonWidget: OpenNoteButtonWidget | BookmarkFolderWidget = note.isLabelTruthy("bookmarkFolder") - ? new BookmarkFolderWidget(note) - : new OpenNoteButtonWidget(note).class("launcher-button"); + ? new BookmarkFolderWidget(note); if (this.settings.titlePlacement) { if (!("settings" in buttonWidget)) { diff --git a/apps/client/src/widgets/buttons/open_note_button_widget.ts b/apps/client/src/widgets/buttons/open_note_button_widget.ts deleted file mode 100644 index c0a4c6334..000000000 --- a/apps/client/src/widgets/buttons/open_note_button_widget.ts +++ /dev/null @@ -1,49 +0,0 @@ -import OnClickButtonWidget from "./onclick_button.js"; -import linkContextMenuService from "../../menus/link_context_menu.js"; -import utils from "../../services/utils.js"; -import appContext from "../../components/app_context.js"; -import type FNote from "../../entities/fnote.js"; - -export default class OpenNoteButtonWidget extends OnClickButtonWidget { - - private noteToOpen: FNote; - - constructor(noteToOpen: FNote) { - super(); - - this.noteToOpen = noteToOpen; - - this.title(() => utils.escapeHtml(this.noteToOpen.title)) - .icon(() => this.noteToOpen.getIcon()) - .onClick((widget, evt) => this.launch(evt)) - .onAuxClick((widget, evt) => this.launch(evt)) - .onContextMenu((evt) => { - if (evt) { - linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt); - } - }); - } - - async launch(evt: JQuery.ClickEvent | JQuery.TriggeredEvent | JQuery.ContextMenuEvent) { - if (evt.which === 3) { - return; - } - const hoistedNoteId = this.getHoistedNoteId(); - const ctrlKey = utils.isCtrlKey(evt); - - if ((evt.which === 1 && ctrlKey) || evt.which === 2) { - const activate = evt.shiftKey ? true : false; - await appContext.tabManager.openInNewTab(this.noteToOpen.noteId, hoistedNoteId, activate); - } else { - await appContext.tabManager.openInSameTab(this.noteToOpen.noteId); - } - } - - getHoistedNoteId() { - return this.noteToOpen.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId; - } - - initialRenderCompleteEvent() { - // we trigger refresh above - } -} diff --git a/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx b/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx index 4d6abec7d..45941c699 100644 --- a/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx +++ b/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx @@ -1,5 +1,71 @@ +import { useMemo } from "preact/hooks"; import type { LaunchBarWidgetProps } from "./launch_bar_widget"; +import { CSSProperties } from "preact"; +import type FNote from "../../entities/fnote"; +import { useChildNotes, useNoteLabel, useNoteProperty } from "../react/hooks"; +import Dropdown from "../react/Dropdown"; +import ActionButton from "../react/ActionButton"; +import appContext from "../../components/app_context"; +import { escapeHtml, isCtrlKey } from "../../services/utils"; +import link_context_menu from "../../menus/link_context_menu"; -export default function BookmarkButtons({ }: LaunchBarWidgetProps) { - return

Bookmarks goes here.

; +const PARENT_NOTE_ID = "_lbBookmarks"; + +export default function BookmarkButtons({ isHorizontalLayout }: LaunchBarWidgetProps) { + const style = useMemo(() => ({ + display: "flex", + flexDirection: isHorizontalLayout ? "row" : "column", + contain: "none" + }), [ isHorizontalLayout ]); + const childNotes = useChildNotes(PARENT_NOTE_ID); + + return ( +
+ {childNotes?.map(childNote => )} +
+ ) +} + +function SingleBookmark({ note }: { note: FNote }) { + return +} + +function OpenNoteButtonWidget({ note }: { note: FNote }) { + const [ iconClass ] = useNoteLabel(note, "iconClass"); + const title = useNoteProperty(note, "title"); + + async function launch(evt: MouseEvent) { + if (evt.which === 3) { + return; + } + const hoistedNoteId = getHoistedNoteId(note); + const ctrlKey = isCtrlKey(evt); + + if ((evt.which === 1 && ctrlKey) || evt.which === 2) { + const activate = evt.shiftKey ? true : false; + await appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId, activate); + } else { + await appContext.tabManager.openInSameTab(note.noteId); + } + } + + return title && iconClass && ( + { + evt.preventDefault(); + link_context_menu.openContextMenu(note.noteId, evt); + }} + /> + ) +} + +function getHoistedNoteId(noteToOpen: FNote) { + return noteToOpen.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId; } diff --git a/apps/client/src/widgets/react/ActionButton.tsx b/apps/client/src/widgets/react/ActionButton.tsx index 28489005d..a37f34514 100644 --- a/apps/client/src/widgets/react/ActionButton.tsx +++ b/apps/client/src/widgets/react/ActionButton.tsx @@ -2,13 +2,13 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { CommandNames } from "../../components/app_context"; import { useStaticTooltip } from "./hooks"; import keyboard_actions from "../../services/keyboard_actions"; +import { HTMLAttributes } from "preact"; -export interface ActionButtonProps { +export interface ActionButtonProps extends Pick, "onClick" | "onAuxClick" | "onContextMenu"> { text: string; titlePosition?: "top" | "right" | "bottom" | "left"; icon: string; className?: string; - onClick?: (e: MouseEvent) => void; triggerCommand?: CommandNames; noIconActionClass?: boolean; frame?: boolean; @@ -16,7 +16,7 @@ export interface ActionButtonProps { disabled?: boolean; } -export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled }: ActionButtonProps) { +export default function ActionButton({ text, icon, className, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled, ...restProps }: ActionButtonProps) { const buttonRef = useRef(null); const [ keyboardShortcut, setKeyboardShortcut ] = useState(); @@ -35,8 +35,8 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo return @@ -61,7 +54,6 @@ const DROPDOWN_TPL = `
-
`; const DAYS_OF_WEEK = [ @@ -74,9 +66,7 @@ const DAYS_OF_WEEK = [ t("calendar.sat") ]; -interface DateNotesForMonth { - [date: string]: string; -} + interface WeekCalculationOptions { firstWeekType: number; @@ -228,9 +218,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget { // Store firstDayOfWeek as ISO (1–7) manageFirstDayOfWeek() { - const rawFirstDayOfWeek = options.getInt("firstDayOfWeek") || 0; - this.firstDayOfWeekISO = rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek; - let localeDaysOfWeek = [...DAYS_OF_WEEK]; const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek); localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted]; @@ -244,34 +231,13 @@ export default class CalendarWidget extends RightDropdownButtonWidget { }; } - getWeekStartDate(date: Dayjs): Dayjs { - const currentISO = date.isoWeekday(); - const diff = (currentISO - this.firstDayOfWeekISO + 7) % 7; - return date.clone().subtract(diff, "day").startOf("day"); - } - - getWeekNumber(date: Dayjs): number { - const weekStart = this.getWeekStartDate(date); - return weekStart.isoWeek(); - } - async dropdownShown() { await this.getWeekNoteEnable(); this.weekNotes = await server.get(`attribute-values/weekNote`); - this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null); + this.init( ?? null); } - init(activeDate: string | null) { - this.activeDate = activeDate ? dayjs(`${activeDate}T12:00:00`) : null; - this.todaysDate = dayjs(); - this.date = dayjs(this.activeDate || this.todaysDate).startOf('month'); - this.createMonth(); - } - - createDay(dateNotesForMonth: DateNotesForMonth, num: number) { - const $newDay = $("") - .addClass("calendar-date") - .attr("data-calendar-date", this.date.local().format('YYYY-MM-DD')); + createDay() { const $date = $("").html(String(num)); const dateNoteId = dateNotesForMonth[this.date.local().format('YYYY-MM-DD')]; @@ -304,105 +270,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget { return $newWeekNumber; } - // Use isoWeekday() consistently - private getPrevMonthDays(firstDayISO: number): { weekNumber: number, dates: Dayjs[] } { - const prevMonthLastDay = this.date.subtract(1, 'month').endOf('month'); - const daysToAdd = (firstDayISO - this.firstDayOfWeekISO + 7) % 7; - const dates: Dayjs[] = []; - - const firstDay = this.date.startOf('month'); - const weekNumber = this.getWeekNumber(firstDay); - - // Get dates from previous month - for (let i = daysToAdd - 1; i >= 0; i--) { - dates.push(prevMonthLastDay.subtract(i, 'day')); - } - - return { weekNumber, dates }; - } - - private getNextMonthDays(lastDayISO: number): Dayjs[] { - const nextMonthFirstDay = this.date.add(1, 'month').startOf('month'); - const dates: Dayjs[] = []; - - const lastDayOfUserWeek = ((this.firstDayOfWeekISO + 6 - 1) % 7) + 1; // ISO wrap - const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7; - - for (let i = 0; i < daysToAdd; i++) { - dates.push(nextMonthFirstDay.add(i, 'day')); - } - return dates; - } - - async createMonth() { - const month = this.date.format('YYYY-MM'); - const dateNotesForMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${month}`); - - this.$month.empty(); - - const firstDay = this.date.startOf('month'); - const firstDayISO = firstDay.isoWeekday(); - - // Previous month filler - if (firstDayISO !== this.firstDayOfWeekISO) { - const { weekNumber, dates } = this.getPrevMonthDays(firstDayISO); - const prevMonth = this.date.subtract(1, 'month').format('YYYY-MM'); - const dateNotesForPrevMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${prevMonth}`); - - const $weekNumber = this.createWeekNumber(weekNumber); - this.$month.append($weekNumber); - - dates.forEach(date => { - const tempDate = this.date; - this.date = date; - const $day = this.createDay(dateNotesForPrevMonth, date.date()); - $day.addClass('calendar-date-prev-month'); - this.$month.append($day); - this.date = tempDate; - }); - } - - const currentMonth = this.date.month(); - - // Main month - while (this.date.month() === currentMonth) { - const weekNumber = this.getWeekNumber(this.date); - if (this.date.isoWeekday() === this.firstDayOfWeekISO) { - const $weekNumber = this.createWeekNumber(weekNumber); - this.$month.append($weekNumber); - } - - const $day = this.createDay(dateNotesForMonth, this.date.date()); - this.$month.append($day); - this.date = this.date.add(1, 'day'); - } - // while loop trips over and day is at 30/31, bring it back - this.date = this.date.startOf('month').subtract(1, 'month'); - - // Add dates from next month - const lastDayOfMonth = this.date.endOf('month'); - const lastDayISO = lastDayOfMonth.isoWeekday(); - const lastDayOfUserWeek = ((this.firstDayOfWeekISO + 6 - 1) % 7) + 1; - - if (lastDayISO !== lastDayOfUserWeek) { - const dates = this.getNextMonthDays(lastDayISO); - const nextMonth = this.date.add(1, 'month').format('YYYY-MM'); - const dateNotesForNextMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${nextMonth}`); - - dates.forEach(date => { - const tempDate = this.date; - this.date = date; - const $day = this.createDay(dateNotesForNextMonth, date.date()); - $day.addClass('calendar-date-next-month'); - this.$month.append($day); - this.date = tempDate; - }); - } - - this.$monthSelect.text(MONTHS[this.date.month()]); - this.$yearSelect.val(this.date.year()); - } - async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { const WEEK_OPTIONS: (keyof OptionDefinitions)[] = [ "firstDayOfWeek", diff --git a/apps/client/src/widgets/containers/launcher.tsx b/apps/client/src/widgets/containers/launcher.tsx index c61717cf2..2e9b6693f 100644 --- a/apps/client/src/widgets/containers/launcher.tsx +++ b/apps/client/src/widgets/containers/launcher.tsx @@ -1,4 +1,3 @@ -import CalendarWidget from "../buttons/calendar.js"; import SyncStatusWidget from "../sync_status.js"; import BasicWidget, { wrapReactWidgets } from "../basic_widget.js"; import utils, { isMobile } from "../../services/utils.js"; @@ -16,6 +15,7 @@ import QuickSearchWidget from "../quick_search.js"; import { ParentComponent } from "../react/react_utils.jsx"; import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { LaunchBarActionButton, useLauncherIconAndTitle } from "../launch_bar/launch_bar_widgets.jsx"; +import CalendarWidget from "../launch_bar/CalendarWidget.jsx"; interface InnerWidget extends BasicWidget { settings?: { @@ -98,7 +98,7 @@ export default class LauncherWidget extends BasicWidget { const builtinWidget = note.getLabelValue("builtinWidget"); switch (builtinWidget) { case "calendar": - return new CalendarWidget(note.title, note.getIcon()); + return case "spacer": // || has to be inside since 0 is a valid value const baseSize = parseInt(note.getLabelValue("baseSize") || "40"); diff --git a/apps/client/src/stylesheets/calendar.css b/apps/client/src/widgets/launch_bar/CalendarWidget.css similarity index 100% rename from apps/client/src/stylesheets/calendar.css rename to apps/client/src/widgets/launch_bar/CalendarWidget.css diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx new file mode 100644 index 000000000..ca562bb38 --- /dev/null +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -0,0 +1,159 @@ +import { useEffect, useState } from "preact/hooks"; +import FNote from "../../entities/fnote"; +import { LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; +import { Dayjs, dayjs } from "@triliumnext/commons"; +import appContext from "../../components/app_context"; +import { useTriliumOptionInt } from "../react/hooks"; +import { VNode } from "preact"; +import clsx from "clsx"; +import "./CalendarWidget.css"; +import server from "../../services/server"; + +interface DateNotesForMonth { + [date: string]: string; +} + +export default function CalendarWidget({ launcherNote }: { launcherNote: FNote }) { + const { title, icon } = useLauncherIconAndTitle(launcherNote); + const [ date, setDate ] = useState(); + const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek") ?? 0; + const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek); + + useEffect(() => { + + }) + + return ( + { + const dateNote = appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote"); + const activeDate = dateNote ? dayjs(`${dateNote}T12:00:00`) : null; + const todaysDate = dayjs(); + const date = dayjs(activeDate || todaysDate).startOf('month'); + setDate(date); + }} + > + {date &&
+ +
} +
+ ) +} + +function Calendar({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) { + const month = date.format('YYYY-MM'); + const firstDay = date.startOf('month'); + const firstDayISO = firstDay.isoWeekday(); + + return ( +
+ {firstDayISO !== firstDayOfWeekISO && } + + +
+ ) +} + +function PreviousMonthDays({ date, firstDayISO, firstDayOfWeekISO }: { date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number }) { + const prevMonth = date.subtract(1, 'month').format('YYYY-MM'); + const { weekNumber, dates } = getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO); + const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState(); + + useEffect(() => { + server.get(`special-notes/notes-for-month/${prevMonth}`).then(setDateNotesForPrevMonth); + }, [ date ]); + + return dates.map(date => ( + + )); +} + +function CurrentMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) { + const dates = getCurMonthDays(date, firstDayOfWeekISO); + + return dates.map(date => ( + + )); +} + +function NextMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) { + const lastDayOfMonth = date.endOf('month'); + const lastDayISO = lastDayOfMonth.isoWeekday(); + const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1; + const nextMonth = date.add(1, 'month').format('YYYY-MM'); + const [ dateNotesForNextMonth, setDateNotesForNextMonth ] = useState(); + + useEffect(() => { + server.get(`special-notes/notes-for-month/${nextMonth}`).then(setDateNotesForNextMonth); + }, [ date ]); + + const dates = lastDayISO !== lastDayOfUserWeek ? getNextMonthDays(date, lastDayISO, firstDayOfWeekISO) : []; + return dates.map(date => ( + + )); +} + +function CalendarDay({ date, dateNotesForMonth, className }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string }) { + return ( +
+ + {date.date()} + + + ); +} + +function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): { weekNumber: number, dates: Dayjs[] } { + const prevMonthLastDay = date.subtract(1, 'month').endOf('month'); + const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7; + const dates: Dayjs[] = []; + + const firstDay = date.startOf('month'); + const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO); + + // Get dates from previous month + for (let i = daysToAdd - 1; i >= 0; i--) { + dates.push(prevMonthLastDay.subtract(i, 'day')); + } + + return { weekNumber, dates }; +} + +function getCurMonthDays(date: Dayjs, firstDayOfWeekISO: number) { + let dateCursor = date; + const currentMonth = date.month(); + const dates: Dayjs[] = []; + while (dateCursor.month() === currentMonth) { + dates.push(dateCursor); + dateCursor = dateCursor.add(1, "day"); + } + return dates; +} + +function getNextMonthDays(date: Dayjs, lastDayISO: number, firstDayOfWeekISO): Dayjs[] { + const nextMonthFirstDay = date.add(1, 'month').startOf('month'); + const dates: Dayjs[] = []; + + const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1; // ISO wrap + const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7; + + for (let i = 0; i < daysToAdd; i++) { + dates.push(nextMonthFirstDay.add(i, 'day')); + } + return dates; +} + +function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number { + const weekStart = getWeekStartDate(date, firstDayOfWeekISO); + return weekStart.isoWeek(); +} + +function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs { + const currentISO = date.isoWeekday(); + const diff = (currentISO - firstDayOfWeekISO + 7) % 7; + return date.clone().subtract(diff, "day").startOf("day"); +} diff --git a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx index 5a72e0f54..715b5dbe5 100644 --- a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx +++ b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx @@ -20,7 +20,7 @@ export function LaunchBarActionButton(props: Omit & { icon: string }) { +export function LaunchBarDropdownButton({ children, icon, ...props }: Pick & { icon: string }) { return ( Date: Thu, 4 Dec 2025 19:22:28 +0200 Subject: [PATCH 22/83] refactor(react/launch_bar): use different mechanism for gathering calendar info --- .../src/widgets/launch_bar/CalendarWidget.tsx | 71 ++--------------- .../widgets/launch_bar/CalendarWidgetUtils.ts | 76 +++++++++++++++++++ 2 files changed, 84 insertions(+), 63 deletions(-) create mode 100644 apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index ca562bb38..1e149041c 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -8,6 +8,7 @@ import { VNode } from "preact"; import clsx from "clsx"; import "./CalendarWidget.css"; import server from "../../services/server"; +import { getMonthInformation } from "./CalendarWidgetUtils"; interface DateNotesForMonth { [date: string]: string; @@ -45,19 +46,19 @@ function Calendar({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: const month = date.format('YYYY-MM'); const firstDay = date.startOf('month'); const firstDayISO = firstDay.isoWeekday(); + const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO); return (
- {firstDayISO !== firstDayOfWeekISO && } - - + {firstDayISO !== firstDayOfWeekISO && } + +
) } -function PreviousMonthDays({ date, firstDayISO, firstDayOfWeekISO }: { date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number }) { +function PreviousMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { const prevMonth = date.subtract(1, 'month').format('YYYY-MM'); - const { weekNumber, dates } = getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO); const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState(); useEffect(() => { @@ -69,18 +70,13 @@ function PreviousMonthDays({ date, firstDayISO, firstDayOfWeekISO }: { date: Day )); } -function CurrentMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) { - const dates = getCurMonthDays(date, firstDayOfWeekISO); - +function CurrentMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { return dates.map(date => ( )); } -function NextMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) { - const lastDayOfMonth = date.endOf('month'); - const lastDayISO = lastDayOfMonth.isoWeekday(); - const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1; +function NextMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { const nextMonth = date.add(1, 'month').format('YYYY-MM'); const [ dateNotesForNextMonth, setDateNotesForNextMonth ] = useState(); @@ -88,7 +84,6 @@ function NextMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWee server.get(`special-notes/notes-for-month/${nextMonth}`).then(setDateNotesForNextMonth); }, [ date ]); - const dates = lastDayISO !== lastDayOfUserWeek ? getNextMonthDays(date, lastDayISO, firstDayOfWeekISO) : []; return dates.map(date => ( )); @@ -107,53 +102,3 @@ function CalendarDay({ date, dateNotesForMonth, className }: { date: Dayjs, date ); } -function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): { weekNumber: number, dates: Dayjs[] } { - const prevMonthLastDay = date.subtract(1, 'month').endOf('month'); - const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7; - const dates: Dayjs[] = []; - - const firstDay = date.startOf('month'); - const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO); - - // Get dates from previous month - for (let i = daysToAdd - 1; i >= 0; i--) { - dates.push(prevMonthLastDay.subtract(i, 'day')); - } - - return { weekNumber, dates }; -} - -function getCurMonthDays(date: Dayjs, firstDayOfWeekISO: number) { - let dateCursor = date; - const currentMonth = date.month(); - const dates: Dayjs[] = []; - while (dateCursor.month() === currentMonth) { - dates.push(dateCursor); - dateCursor = dateCursor.add(1, "day"); - } - return dates; -} - -function getNextMonthDays(date: Dayjs, lastDayISO: number, firstDayOfWeekISO): Dayjs[] { - const nextMonthFirstDay = date.add(1, 'month').startOf('month'); - const dates: Dayjs[] = []; - - const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1; // ISO wrap - const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7; - - for (let i = 0; i < daysToAdd; i++) { - dates.push(nextMonthFirstDay.add(i, 'day')); - } - return dates; -} - -function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number { - const weekStart = getWeekStartDate(date, firstDayOfWeekISO); - return weekStart.isoWeek(); -} - -function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs { - const currentISO = date.isoWeekday(); - const diff = (currentISO - firstDayOfWeekISO + 7) % 7; - return date.clone().subtract(diff, "day").startOf("day"); -} diff --git a/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts b/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts new file mode 100644 index 000000000..16d057107 --- /dev/null +++ b/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts @@ -0,0 +1,76 @@ +import { Dayjs } from "@triliumnext/commons"; + +interface DateRangeInfo { + weekNumbers: number[]; + dates: Dayjs[]; +} + +export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) { + return { + prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO), + currentMonth: getCurMonthDays(date, firstDayOfWeekISO), + nextMonth: getNextMonthDays(date, firstDayOfWeekISO) + } +} + +function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): DateRangeInfo { + const prevMonthLastDay = date.subtract(1, 'month').endOf('month'); + const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7; + const dates: Dayjs[] = []; + + const firstDay = date.startOf('month'); + const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO); + + // Get dates from previous month + for (let i = daysToAdd - 1; i >= 0; i--) { + dates.push(prevMonthLastDay.subtract(i, 'day')); + } + + return { weekNumbers: [ weekNumber ], dates }; +} + +function getCurMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo { + let dateCursor = date; + const currentMonth = date.month(); + const dates: Dayjs[] = []; + const weekNumbers: number[] = []; + while (dateCursor.month() === currentMonth) { + const weekNumber = getWeekNumber(date, firstDayOfWeekISO); + if (date.isoWeekday() === firstDayOfWeekISO) { + weekNumbers.push(weekNumber); + } + + dates.push(dateCursor); + dateCursor = dateCursor.add(1, "day"); + + } + return { weekNumbers, dates }; +} + +function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo { + const lastDayOfMonth = date.endOf('month'); + const lastDayISO = lastDayOfMonth.isoWeekday(); + const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1; + const nextMonthFirstDay = date.add(1, 'month').startOf('month'); + const dates: Dayjs[] = []; + + if (lastDayISO !== lastDayOfUserWeek) { + const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7; + + for (let i = 0; i < daysToAdd; i++) { + dates.push(nextMonthFirstDay.add(i, 'day')); + } + } + return { weekNumbers: [], dates }; +} + +function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number { + const weekStart = getWeekStartDate(date, firstDayOfWeekISO); + return weekStart.isoWeek(); +} + +function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs { + const currentISO = date.isoWeekday(); + const diff = (currentISO - firstDayOfWeekISO + 7) % 7; + return date.clone().subtract(diff, "day").startOf("day"); +} From 0d8127140f0bc6aa3c70ac0eaee1aa5585175f8f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 4 Dec 2025 19:38:56 +0200 Subject: [PATCH 23/83] chore(react/launch_bar): get week numbers to render --- apps/client/src/widgets/buttons/calendar.ts | 5 +-- .../src/widgets/launch_bar/CalendarWidget.tsx | 43 +++++++++++++------ .../widgets/launch_bar/CalendarWidgetUtils.ts | 23 +--------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/apps/client/src/widgets/buttons/calendar.ts b/apps/client/src/widgets/buttons/calendar.ts index 75e01af1b..286b94a14 100644 --- a/apps/client/src/widgets/buttons/calendar.ts +++ b/apps/client/src/widgets/buttons/calendar.ts @@ -53,7 +53,7 @@ const DROPDOWN_TPL = ` -
+ `; const DAYS_OF_WEEK = [ @@ -262,11 +262,10 @@ export default class CalendarWidget extends RightDropdownButtonWidget { $newWeekNumber.addClass("calendar-date-exists").attr("data-href", `#root/${weekNoteId}`); } } else { - $newWeekNumber = $("").addClass("calendar-week-number-disabled"); + } $newWeekNumber.addClass("calendar-week-number").attr("data-calendar-week-number", weekNoteId); - $newWeekNumber.append($("").html(String(weekNumber))); return $newWeekNumber; } diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index 1e149041c..b8b94ba73 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -4,11 +4,11 @@ import { LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_w import { Dayjs, dayjs } from "@triliumnext/commons"; import appContext from "../../components/app_context"; import { useTriliumOptionInt } from "../react/hooks"; -import { VNode } from "preact"; import clsx from "clsx"; import "./CalendarWidget.css"; import server from "../../services/server"; -import { getMonthInformation } from "./CalendarWidgetUtils"; +import { DateRangeInfo, getMonthInformation, getWeekNumber } from "./CalendarWidgetUtils"; +import { VNode } from "preact"; interface DateNotesForMonth { [date: string]: string; @@ -50,14 +50,14 @@ function Calendar({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: return (
- {firstDayISO !== firstDayOfWeekISO && } - + {firstDayISO !== firstDayOfWeekISO && } +
) } -function PreviousMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { +function PreviousMonthDays({ date, info: { dates, weekNumbers } }: { date: Dayjs, info: DateRangeInfo }) { const prevMonth = date.subtract(1, 'month').format('YYYY-MM'); const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState(); @@ -65,15 +65,29 @@ function PreviousMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { server.get(`special-notes/notes-for-month/${prevMonth}`).then(setDateNotesForPrevMonth); }, [ date ]); - return dates.map(date => ( - - )); + return ( + <> + + {dates.map(date => )} + + ) } -function CurrentMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { - return dates.map(date => ( - - )); +function CurrentMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) { + let dateCursor = date; + const currentMonth = date.month(); + const items: VNode[] = []; + while (dateCursor.month() === currentMonth) { + const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO); + if (dateCursor.isoWeekday() === firstDayOfWeekISO) { + items.push() + } + + items.push() + dateCursor = dateCursor.add(1, "day"); + } + + return items; } function NextMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { @@ -102,3 +116,8 @@ function CalendarDay({ date, dateNotesForMonth, className }: { date: Dayjs, date ); } +function CalendarWeek({ weekNumber }: { weekNumber: number }) { + return ( + {weekNumber} + ) +} diff --git a/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts b/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts index 16d057107..e7a59aa4a 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts +++ b/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts @@ -1,6 +1,6 @@ import { Dayjs } from "@triliumnext/commons"; -interface DateRangeInfo { +export interface DateRangeInfo { weekNumbers: number[]; dates: Dayjs[]; } @@ -8,7 +8,6 @@ interface DateRangeInfo { export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) { return { prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO), - currentMonth: getCurMonthDays(date, firstDayOfWeekISO), nextMonth: getNextMonthDays(date, firstDayOfWeekISO) } } @@ -29,24 +28,6 @@ function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: n return { weekNumbers: [ weekNumber ], dates }; } -function getCurMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo { - let dateCursor = date; - const currentMonth = date.month(); - const dates: Dayjs[] = []; - const weekNumbers: number[] = []; - while (dateCursor.month() === currentMonth) { - const weekNumber = getWeekNumber(date, firstDayOfWeekISO); - if (date.isoWeekday() === firstDayOfWeekISO) { - weekNumbers.push(weekNumber); - } - - dates.push(dateCursor); - dateCursor = dateCursor.add(1, "day"); - - } - return { weekNumbers, dates }; -} - function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo { const lastDayOfMonth = date.endOf('month'); const lastDayISO = lastDayOfMonth.isoWeekday(); @@ -64,7 +45,7 @@ function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo return { weekNumbers: [], dates }; } -function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number { +export function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number { const weekStart = getWeekStartDate(date, firstDayOfWeekISO); return weekStart.isoWeek(); } From 62fd07258ec5f5f9916eac5ca7aed71a2ce22dac Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 4 Dec 2025 19:43:47 +0200 Subject: [PATCH 24/83] chore(react/launch_bar): get days of the week to render --- apps/client/src/widgets/buttons/calendar.ts | 21 ------------ .../src/widgets/launch_bar/CalendarWidget.tsx | 34 ++++++++++++++----- .../widgets/launch_bar/CalendarWidgetUtils.ts | 11 ++++++ 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/apps/client/src/widgets/buttons/calendar.ts b/apps/client/src/widgets/buttons/calendar.ts index 286b94a14..0d993540e 100644 --- a/apps/client/src/widgets/buttons/calendar.ts +++ b/apps/client/src/widgets/buttons/calendar.ts @@ -56,18 +56,6 @@ const DROPDOWN_TPL = ` `; -const DAYS_OF_WEEK = [ - t("calendar.sun"), - t("calendar.mon"), - t("calendar.tue"), - t("calendar.wed"), - t("calendar.thu"), - t("calendar.fri"), - t("calendar.sat") -]; - - - interface WeekCalculationOptions { firstWeekType: number; minDaysInFirstWeek: number; @@ -100,7 +88,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget { super.doRender(); this.$month = this.$dropdownContent.find('[data-calendar-area="month"]'); - this.$weekHeader = this.$dropdownContent.find(".calendar-week"); this.manageFirstDayOfWeek(); this.initWeekCalculation(); @@ -216,14 +203,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.weekNoteEnable = noteAttributes.some(a => a.name === 'enableWeekNote'); } - // Store firstDayOfWeek as ISO (1–7) - manageFirstDayOfWeek() { - let localeDaysOfWeek = [...DAYS_OF_WEEK]; - const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek); - localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted]; - this.$weekHeader.html(localeDaysOfWeek.map((el) => `${el}`).join('')); - } - initWeekCalculation() { this.weekCalculationOptions = { firstWeekType: options.getInt("firstWeekOfYear") || 0, diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index b8b94ba73..f8b20c2f0 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -7,7 +7,7 @@ import { useTriliumOptionInt } from "../react/hooks"; import clsx from "clsx"; import "./CalendarWidget.css"; import server from "../../services/server"; -import { DateRangeInfo, getMonthInformation, getWeekNumber } from "./CalendarWidgetUtils"; +import { DateRangeInfo, DAYS_OF_WEEK, getMonthInformation, getWeekNumber } from "./CalendarWidgetUtils"; import { VNode } from "preact"; interface DateNotesForMonth { @@ -17,8 +17,6 @@ interface DateNotesForMonth { export default function CalendarWidget({ launcherNote }: { launcherNote: FNote }) { const { title, icon } = useLauncherIconAndTitle(launcherNote); const [ date, setDate ] = useState(); - const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek") ?? 0; - const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek); useEffect(() => { @@ -36,23 +34,41 @@ export default function CalendarWidget({ launcherNote }: { launcherNote: FNote } }} > {date &&
- +
} ) } -function Calendar({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) { +function Calendar({ date }: { date: Dayjs }) { + const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek") ?? 0; + const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek); + const month = date.format('YYYY-MM'); const firstDay = date.startOf('month'); const firstDayISO = firstDay.isoWeekday(); const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO); return ( -
- {firstDayISO !== firstDayOfWeekISO && } - - + <> + +
+ {firstDayISO !== firstDayOfWeekISO && } + + +
+ + ) +} + +function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number }) { + let localeDaysOfWeek = [...DAYS_OF_WEEK]; + const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek); + localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted]; + + return ( +
+ {localeDaysOfWeek.map(dayOfWeek => {dayOfWeek})}
) } diff --git a/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts b/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts index e7a59aa4a..3b598bbe9 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts +++ b/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts @@ -1,4 +1,15 @@ import { Dayjs } from "@triliumnext/commons"; +import { t } from "../../services/i18n"; + +export const DAYS_OF_WEEK = [ + t("calendar.sun"), + t("calendar.mon"), + t("calendar.tue"), + t("calendar.wed"), + t("calendar.thu"), + t("calendar.fri"), + t("calendar.sat") +]; export interface DateRangeInfo { weekNumbers: number[]; From e0aed26f637b4dafc8f3c52ab77b8fd23db2b566 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 4 Dec 2025 19:46:25 +0200 Subject: [PATCH 25/83] refactor(react/launch_bar): extract calendar impl into single file --- .../src/widgets/launch_bar/Calendar.tsx | 175 ++++++++++++++++++ .../src/widgets/launch_bar/CalendarWidget.tsx | 110 +---------- .../widgets/launch_bar/CalendarWidgetUtils.ts | 68 ------- 3 files changed, 176 insertions(+), 177 deletions(-) create mode 100644 apps/client/src/widgets/launch_bar/Calendar.tsx delete mode 100644 apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts diff --git a/apps/client/src/widgets/launch_bar/Calendar.tsx b/apps/client/src/widgets/launch_bar/Calendar.tsx new file mode 100644 index 000000000..b1753a038 --- /dev/null +++ b/apps/client/src/widgets/launch_bar/Calendar.tsx @@ -0,0 +1,175 @@ +import { useTriliumOptionInt } from "../react/hooks"; +import clsx from "clsx"; +import server from "../../services/server"; +import { VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { Dayjs } from "@triliumnext/commons"; +import { t } from "../../services/i18n"; + +interface DateNotesForMonth { + [date: string]: string; +} + +const DAYS_OF_WEEK = [ + t("calendar.sun"), + t("calendar.mon"), + t("calendar.tue"), + t("calendar.wed"), + t("calendar.thu"), + t("calendar.fri"), + t("calendar.sat") +]; + +interface DateRangeInfo { + weekNumbers: number[]; + dates: Dayjs[]; +} + +export default function Calendar({ date }: { date: Dayjs }) { + const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek") ?? 0; + const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek); + + const month = date.format('YYYY-MM'); + const firstDay = date.startOf('month'); + const firstDayISO = firstDay.isoWeekday(); + const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO); + + return ( + <> + +
+ {firstDayISO !== firstDayOfWeekISO && } + + +
+ + ) +} + +function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number }) { + let localeDaysOfWeek = [...DAYS_OF_WEEK]; + const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek); + localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted]; + + return ( +
+ {localeDaysOfWeek.map(dayOfWeek => {dayOfWeek})} +
+ ) +} + +function PreviousMonthDays({ date, info: { dates, weekNumbers } }: { date: Dayjs, info: DateRangeInfo }) { + const prevMonth = date.subtract(1, 'month').format('YYYY-MM'); + const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState(); + + useEffect(() => { + server.get(`special-notes/notes-for-month/${prevMonth}`).then(setDateNotesForPrevMonth); + }, [ date ]); + + return ( + <> + + {dates.map(date => )} + + ) +} + +function CurrentMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) { + let dateCursor = date; + const currentMonth = date.month(); + const items: VNode[] = []; + while (dateCursor.month() === currentMonth) { + const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO); + if (dateCursor.isoWeekday() === firstDayOfWeekISO) { + items.push() + } + + items.push() + dateCursor = dateCursor.add(1, "day"); + } + + return items; +} + +function NextMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { + const nextMonth = date.add(1, 'month').format('YYYY-MM'); + const [ dateNotesForNextMonth, setDateNotesForNextMonth ] = useState(); + + useEffect(() => { + server.get(`special-notes/notes-for-month/${nextMonth}`).then(setDateNotesForNextMonth); + }, [ date ]); + + return dates.map(date => ( + + )); +} + +function CalendarDay({ date, dateNotesForMonth, className }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string }) { + return ( + + + {date.date()} + + + ); +} + +function CalendarWeek({ weekNumber }: { weekNumber: number }) { + return ( + {weekNumber} + ) +} + +export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) { + return { + prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO), + nextMonth: getNextMonthDays(date, firstDayOfWeekISO) + } +} + +function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): DateRangeInfo { + const prevMonthLastDay = date.subtract(1, 'month').endOf('month'); + const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7; + const dates: Dayjs[] = []; + + const firstDay = date.startOf('month'); + const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO); + + // Get dates from previous month + for (let i = daysToAdd - 1; i >= 0; i--) { + dates.push(prevMonthLastDay.subtract(i, 'day')); + } + + return { weekNumbers: [ weekNumber ], dates }; +} + +function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo { + const lastDayOfMonth = date.endOf('month'); + const lastDayISO = lastDayOfMonth.isoWeekday(); + const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1; + const nextMonthFirstDay = date.add(1, 'month').startOf('month'); + const dates: Dayjs[] = []; + + if (lastDayISO !== lastDayOfUserWeek) { + const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7; + + for (let i = 0; i < daysToAdd; i++) { + dates.push(nextMonthFirstDay.add(i, 'day')); + } + } + return { weekNumbers: [], dates }; +} + +export function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number { + const weekStart = getWeekStartDate(date, firstDayOfWeekISO); + return weekStart.isoWeek(); +} + +function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs { + const currentISO = date.isoWeekday(); + const diff = (currentISO - firstDayOfWeekISO + 7) % 7; + return date.clone().subtract(diff, "day").startOf("day"); +} diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index f8b20c2f0..873567b29 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -3,25 +3,13 @@ import FNote from "../../entities/fnote"; import { LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; import { Dayjs, dayjs } from "@triliumnext/commons"; import appContext from "../../components/app_context"; -import { useTriliumOptionInt } from "../react/hooks"; -import clsx from "clsx"; import "./CalendarWidget.css"; -import server from "../../services/server"; -import { DateRangeInfo, DAYS_OF_WEEK, getMonthInformation, getWeekNumber } from "./CalendarWidgetUtils"; -import { VNode } from "preact"; - -interface DateNotesForMonth { - [date: string]: string; -} +import Calendar from "./Calendar"; export default function CalendarWidget({ launcherNote }: { launcherNote: FNote }) { const { title, icon } = useLauncherIconAndTitle(launcherNote); const [ date, setDate ] = useState(); - useEffect(() => { - - }) - return ( - -
- {firstDayISO !== firstDayOfWeekISO && } - - -
- - ) -} - -function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number }) { - let localeDaysOfWeek = [...DAYS_OF_WEEK]; - const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek); - localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted]; - - return ( -
- {localeDaysOfWeek.map(dayOfWeek => {dayOfWeek})} -
- ) -} - -function PreviousMonthDays({ date, info: { dates, weekNumbers } }: { date: Dayjs, info: DateRangeInfo }) { - const prevMonth = date.subtract(1, 'month').format('YYYY-MM'); - const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState(); - - useEffect(() => { - server.get(`special-notes/notes-for-month/${prevMonth}`).then(setDateNotesForPrevMonth); - }, [ date ]); - - return ( - <> - - {dates.map(date => )} - - ) -} - -function CurrentMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) { - let dateCursor = date; - const currentMonth = date.month(); - const items: VNode[] = []; - while (dateCursor.month() === currentMonth) { - const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO); - if (dateCursor.isoWeekday() === firstDayOfWeekISO) { - items.push() - } - - items.push() - dateCursor = dateCursor.add(1, "day"); - } - - return items; -} - -function NextMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { - const nextMonth = date.add(1, 'month').format('YYYY-MM'); - const [ dateNotesForNextMonth, setDateNotesForNextMonth ] = useState(); - - useEffect(() => { - server.get(`special-notes/notes-for-month/${nextMonth}`).then(setDateNotesForNextMonth); - }, [ date ]); - - return dates.map(date => ( - - )); -} - -function CalendarDay({ date, dateNotesForMonth, className }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string }) { - return ( - - - {date.date()} - - - ); -} - -function CalendarWeek({ weekNumber }: { weekNumber: number }) { - return ( - {weekNumber} - ) -} diff --git a/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts b/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts deleted file mode 100644 index 3b598bbe9..000000000 --- a/apps/client/src/widgets/launch_bar/CalendarWidgetUtils.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Dayjs } from "@triliumnext/commons"; -import { t } from "../../services/i18n"; - -export const DAYS_OF_WEEK = [ - t("calendar.sun"), - t("calendar.mon"), - t("calendar.tue"), - t("calendar.wed"), - t("calendar.thu"), - t("calendar.fri"), - t("calendar.sat") -]; - -export interface DateRangeInfo { - weekNumbers: number[]; - dates: Dayjs[]; -} - -export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) { - return { - prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO), - nextMonth: getNextMonthDays(date, firstDayOfWeekISO) - } -} - -function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): DateRangeInfo { - const prevMonthLastDay = date.subtract(1, 'month').endOf('month'); - const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7; - const dates: Dayjs[] = []; - - const firstDay = date.startOf('month'); - const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO); - - // Get dates from previous month - for (let i = daysToAdd - 1; i >= 0; i--) { - dates.push(prevMonthLastDay.subtract(i, 'day')); - } - - return { weekNumbers: [ weekNumber ], dates }; -} - -function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo { - const lastDayOfMonth = date.endOf('month'); - const lastDayISO = lastDayOfMonth.isoWeekday(); - const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1; - const nextMonthFirstDay = date.add(1, 'month').startOf('month'); - const dates: Dayjs[] = []; - - if (lastDayISO !== lastDayOfUserWeek) { - const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7; - - for (let i = 0; i < daysToAdd; i++) { - dates.push(nextMonthFirstDay.add(i, 'day')); - } - } - return { weekNumbers: [], dates }; -} - -export function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number { - const weekStart = getWeekStartDate(date, firstDayOfWeekISO); - return weekStart.isoWeek(); -} - -function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs { - const currentISO = date.isoWeekday(); - const diff = (currentISO - firstDayOfWeekISO + 7) % 7; - return date.clone().subtract(diff, "day").startOf("day"); -} From e1cce220b30abdaba1777fe7b0e67049f6f51e08 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 4 Dec 2025 19:56:34 +0200 Subject: [PATCH 26/83] chore(react/launch_bar): add back icons for previous/next month --- apps/client/src/widgets/buttons/calendar.ts | 13 ------ .../src/widgets/launch_bar/CalendarWidget.tsx | 43 ++++++++++++++++++- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/apps/client/src/widgets/buttons/calendar.ts b/apps/client/src/widgets/buttons/calendar.ts index 0d993540e..a8f507fd1 100644 --- a/apps/client/src/widgets/buttons/calendar.ts +++ b/apps/client/src/widgets/buttons/calendar.ts @@ -29,8 +29,6 @@ const DROPDOWN_TPL = `
- - -
diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index 557d05f5e..cda7b8444 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -1,4 +1,4 @@ -import { Dispatch, StateUpdater, useEffect, useState } from "preact/hooks"; +import { Dispatch, StateUpdater, useEffect, useMemo, useState } from "preact/hooks"; import FNote from "../../entities/fnote"; import { LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; import { Dayjs, dayjs } from "@triliumnext/commons"; @@ -6,6 +6,24 @@ import appContext from "../../components/app_context"; import "./CalendarWidget.css"; import Calendar from "./Calendar"; import ActionButton from "../react/ActionButton"; +import Dropdown from "../react/Dropdown"; +import { t } from "../../services/i18n"; +import FormDropdownList from "../react/FormDropdownList"; + +const MONTHS = [ + t("calendar.january"), + t("calendar.february"), + t("calendar.march"), + t("calendar.april"), + t("calendar.may"), + t("calendar.june"), + t("calendar.july"), + t("calendar.august"), + t("calendar.september"), + t("calendar.october"), + t("calendar.november"), + t("calendar.december") +]; export default function CalendarWidget({ launcherNote }: { launcherNote: FNote }) { const { title, icon } = useLauncherIconAndTitle(launcherNote); @@ -21,6 +39,9 @@ export default function CalendarWidget({ launcherNote }: { launcherNote: FNote } const date = dayjs(activeDate || todaysDate).startOf('month'); setDate(date); }} + dropdownOptions={{ + autoClose: "outside" + }} > {date &&
@@ -44,9 +65,23 @@ function CalendarHeader(props: CalendarHeaderProps) { } function CalendarMonthSelector({ date, setDate }: CalendarHeaderProps) { + const months = useMemo(() => ( + Array.from(MONTHS.entries().map(([ index, text ]) => ({ + index: index.toString(), text + }))) + ), []); + console.log("Got months ", months); + return (
+ { + + }} + />
); diff --git a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx index 715b5dbe5..d5302adc0 100644 --- a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx +++ b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx @@ -20,7 +20,7 @@ export function LaunchBarActionButton(props: Omit & { icon: string }) { +export function LaunchBarDropdownButton({ children, icon, ...props }: Pick & { icon: string }) { return ( , "id" | "c forceShown?: boolean; onShown?: () => void; onHidden?: () => void; + dropdownOptions?: Partial; } -export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden }: DropdownProps) { +export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions }: DropdownProps) { const dropdownRef = useRef(null); const triggerRef = useRef(null); @@ -32,7 +33,7 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi useEffect(() => { if (!triggerRef.current) return; - const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current); + const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current, dropdownOptions); if (forceShown) { dropdown.show(); setShown(true); From a65d2a1bbab6fb15f1147623f157df768bdcddca Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 4 Dec 2025 21:24:25 +0200 Subject: [PATCH 28/83] chore(react/launch_bar): reintroduce year selector --- apps/client/src/widgets/buttons/calendar.ts | 26 ------------------- .../src/widgets/launch_bar/CalendarWidget.css | 4 +++ .../src/widgets/launch_bar/CalendarWidget.tsx | 26 +++++++++++++++++-- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/apps/client/src/widgets/buttons/calendar.ts b/apps/client/src/widgets/buttons/calendar.ts index 22a62e193..731678cd1 100644 --- a/apps/client/src/widgets/buttons/calendar.ts +++ b/apps/client/src/widgets/buttons/calendar.ts @@ -10,32 +10,6 @@ import type { EventData } from "../../components/app_context.js"; import { dayjs, type Dayjs } from "@triliumnext/commons"; import type { AttributeRow, OptionDefinitions } from "@triliumnext/commons"; - - -const DROPDOWN_TPL = ` -
-
-
- - - -
- -
- - - - - -
-
- - -
`; - interface WeekCalculationOptions { firstWeekType: number; minDaysInFirstWeek: number; diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.css b/apps/client/src/widgets/launch_bar/CalendarWidget.css index 314439846..72249b997 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.css +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.css @@ -174,4 +174,8 @@ background-color: var(--hover-item-background-color); color: var(--hover-item-text-color); text-decoration: underline; +} + +.calendar-dropdown-widget .form-control { + padding: 0; } \ No newline at end of file diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index cda7b8444..f1cdfe20b 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -9,6 +9,7 @@ import ActionButton from "../react/ActionButton"; import Dropdown from "../react/Dropdown"; import { t } from "../../services/i18n"; import FormDropdownList from "../react/FormDropdownList"; +import FormTextBox from "../react/FormTextBox"; const MONTHS = [ t("calendar.january"), @@ -60,6 +61,7 @@ function CalendarHeader(props: CalendarHeaderProps) { return (
+
) } @@ -70,7 +72,6 @@ function CalendarMonthSelector({ date, setDate }: CalendarHeaderProps) { index: index.toString(), text }))) ), []); - console.log("Got months ", months); return (
@@ -87,9 +88,30 @@ function CalendarMonthSelector({ date, setDate }: CalendarHeaderProps) { ); } +function CalendarYearSelector({ date, setDate }: CalendarHeaderProps) { + return ( +
+ + { + const year = parseInt(newValue, 10); + if (!Number.isNaN(year)) { + setDate(date.set("year", year)); + } + }} + data-calendar-input="year" + /> + +
+ ) +} + function AdjustDateButton({ date, setDate, unit, direction }: CalendarHeaderProps & { direction: "prev" | "next", - unit: "month" + unit: "month" | "year" }) { return ( Date: Thu, 4 Dec 2025 21:36:04 +0200 Subject: [PATCH 29/83] chore(react/launch_bar): fix style for month selector --- apps/client/src/widgets/buttons/calendar.ts | 20 ------------------- .../src/widgets/launch_bar/CalendarWidget.tsx | 2 +- apps/client/src/widgets/react/Dropdown.tsx | 10 ++++++++-- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/apps/client/src/widgets/buttons/calendar.ts b/apps/client/src/widgets/buttons/calendar.ts index 731678cd1..60b372fee 100644 --- a/apps/client/src/widgets/buttons/calendar.ts +++ b/apps/client/src/widgets/buttons/calendar.ts @@ -62,26 +62,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget { } }); - // Year navigation - this.$yearSelect = this.$dropdownContent.find('[data-calendar-input="year"]'); - this.$yearSelect.on("input", (e) => { - const target = e.target as HTMLInputElement; - this.date = this.date.year(parseInt(target.value)); - this.createMonth(); - }); - - this.$nextYear = this.$dropdownContent.find('[data-calendar-toggle="nextYear"]'); - this.$nextYear.on("click", () => { - this.date = this.date.add(1, 'year'); - this.createMonth(); - }); - - this.$previousYear = this.$dropdownContent.find('[data-calendar-toggle="previousYear"]'); - this.$previousYear.on("click", () => { - this.date = this.date.subtract(1, 'year'); - this.createMonth(); - }); - // Date click this.$dropdownContent.on("click", ".calendar-date", async (ev) => { const date = $(ev.target).closest(".calendar-date").attr("data-calendar-date"); diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index f1cdfe20b..2fcceb571 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -6,7 +6,6 @@ import appContext from "../../components/app_context"; import "./CalendarWidget.css"; import Calendar from "./Calendar"; import ActionButton from "../react/ActionButton"; -import Dropdown from "../react/Dropdown"; import { t } from "../../services/i18n"; import FormDropdownList from "../react/FormDropdownList"; import FormTextBox from "../react/FormTextBox"; @@ -82,6 +81,7 @@ function CalendarMonthSelector({ date, setDate }: CalendarHeaderProps) { onChange={value => { }} + buttonProps={{ "data-calendar-input": "month" }} />
diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx index c0f5f5585..fbafda25e 100644 --- a/apps/client/src/widgets/react/Dropdown.tsx +++ b/apps/client/src/widgets/react/Dropdown.tsx @@ -1,11 +1,16 @@ import { Dropdown as BootstrapDropdown } from "bootstrap"; -import { ComponentChildren } from "preact"; +import { ComponentChildren, HTMLAttributes } from "preact"; import { CSSProperties, HTMLProps } from "preact/compat"; import { useCallback, useEffect, useRef, useState } from "preact/hooks"; import { useUniqueName } from "./hooks"; +type DataAttributes = { + [key: `data-${string}`]: string | number | boolean | undefined; +}; + export interface DropdownProps extends Pick, "id" | "className"> { buttonClassName?: string; + buttonProps?: Partial & DataAttributes>; isStatic?: boolean; children: ComponentChildren; title?: string; @@ -24,7 +29,7 @@ export interface DropdownProps extends Pick, "id" | "c dropdownOptions?: Partial; } -export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions }: DropdownProps) { +export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps }: DropdownProps) { const dropdownRef = useRef(null); const triggerRef = useRef(null); @@ -80,6 +85,7 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi title={title} id={id ?? ariaId} disabled={disabled} + {...buttonProps} > {text} From 18f9ebbc4f74a08312e118f09d1bafe5936286c0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 5 Dec 2025 08:56:35 +0200 Subject: [PATCH 30/83] chore(react/launch_bar): reintroduce day highlighting --- apps/client/src/widgets/buttons/calendar.ts | 3 -- .../src/widgets/launch_bar/Calendar.tsx | 34 ++++++++++++------- .../src/widgets/launch_bar/CalendarWidget.tsx | 16 +++++---- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/apps/client/src/widgets/buttons/calendar.ts b/apps/client/src/widgets/buttons/calendar.ts index 60b372fee..df71819c1 100644 --- a/apps/client/src/widgets/buttons/calendar.ts +++ b/apps/client/src/widgets/buttons/calendar.ts @@ -147,9 +147,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget { $newDay.addClass("calendar-date-exists").attr("data-href", `#root/${dateNoteId}`); } - if (this.date.isSame(this.activeDate, 'day')) $newDay.addClass("calendar-date-active"); - if (this.date.isSame(this.todaysDate, 'day')) $newDay.addClass("calendar-date-today"); - $newDay.append($date); return $newDay; } diff --git a/apps/client/src/widgets/launch_bar/Calendar.tsx b/apps/client/src/widgets/launch_bar/Calendar.tsx index b1753a038..14d5fe516 100644 --- a/apps/client/src/widgets/launch_bar/Calendar.tsx +++ b/apps/client/src/widgets/launch_bar/Calendar.tsx @@ -25,10 +25,17 @@ interface DateRangeInfo { dates: Dayjs[]; } -export default function Calendar({ date }: { date: Dayjs }) { +export interface CalendarArgs { + date: Dayjs; + todaysDate: Dayjs; + activeDate: Dayjs | null; +} + +export default function Calendar(args: CalendarArgs) { const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek") ?? 0; const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek); + const date = args.date; const month = date.format('YYYY-MM'); const firstDay = date.startOf('month'); const firstDayISO = firstDay.isoWeekday(); @@ -38,9 +45,9 @@ export default function Calendar({ date }: { date: Dayjs }) { <>
- {firstDayISO !== firstDayOfWeekISO && } - - + {firstDayISO !== firstDayOfWeekISO && } + +
) @@ -58,7 +65,7 @@ function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number } ) } -function PreviousMonthDays({ date, info: { dates, weekNumbers } }: { date: Dayjs, info: DateRangeInfo }) { +function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { date: Dayjs, info: DateRangeInfo } & CalendarArgs) { const prevMonth = date.subtract(1, 'month').format('YYYY-MM'); const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState(); @@ -69,12 +76,12 @@ function PreviousMonthDays({ date, info: { dates, weekNumbers } }: { date: Dayjs return ( <> - {dates.map(date => )} + {dates.map(date => )} ) } -function CurrentMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) { +function CurrentMonthDays({ date, firstDayOfWeekISO, ...args }: { date: Dayjs, firstDayOfWeekISO: number } & CalendarArgs) { let dateCursor = date; const currentMonth = date.month(); const items: VNode[] = []; @@ -84,14 +91,14 @@ function CurrentMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOf items.push() } - items.push() + items.push() dateCursor = dateCursor.add(1, "day"); } return items; } -function NextMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { +function NextMonthDays({ date, dates, ...args }: { date: Dayjs, dates: Dayjs[] } & CalendarArgs) { const nextMonth = date.add(1, 'month').format('YYYY-MM'); const [ dateNotesForNextMonth, setDateNotesForNextMonth ] = useState(); @@ -100,14 +107,17 @@ function NextMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) { }, [ date ]); return dates.map(date => ( - + )); } -function CalendarDay({ date, dateNotesForMonth, className }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string }) { +function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDate }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string } & CalendarArgs) { return ( diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index 2fcceb571..03c2d57f0 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -4,7 +4,7 @@ import { LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_w import { Dayjs, dayjs } from "@triliumnext/commons"; import appContext from "../../components/app_context"; import "./CalendarWidget.css"; -import Calendar from "./Calendar"; +import Calendar, { CalendarArgs } from "./Calendar"; import ActionButton from "../react/ActionButton"; import { t } from "../../services/i18n"; import FormDropdownList from "../react/FormDropdownList"; @@ -27,6 +27,7 @@ const MONTHS = [ export default function CalendarWidget({ launcherNote }: { launcherNote: FNote }) { const { title, icon } = useLauncherIconAndTitle(launcherNote); + const [ calendarArgs, setCalendarArgs ] = useState>(); const [ date, setDate ] = useState(); return ( @@ -34,18 +35,21 @@ export default function CalendarWidget({ launcherNote }: { launcherNote: FNote } icon={icon} title={title} onShown={() => { const dateNote = appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote"); - const activeDate = dateNote ? dayjs(`${dateNote}T12:00:00`) : null; + const activeDate = dateNote ? dayjs(`${dateNote}T12:00:00`) : null const todaysDate = dayjs(); - const date = dayjs(activeDate || todaysDate).startOf('month'); - setDate(date); + setCalendarArgs({ + activeDate, + todaysDate, + }); + setDate(dayjs(activeDate || todaysDate).startOf('month')); }} dropdownOptions={{ autoClose: "outside" }} > - {date &&
+ {calendarArgs && date &&
- +
} ) From 07498c6bef37550530c3031078587bfc23a5589e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 5 Dec 2025 09:02:51 +0200 Subject: [PATCH 31/83] chore(react/launch_bar): add link to existing days --- apps/client/src/widgets/buttons/calendar.ts | 28 ------------------- .../src/widgets/launch_bar/Calendar.tsx | 12 +++++++- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/apps/client/src/widgets/buttons/calendar.ts b/apps/client/src/widgets/buttons/calendar.ts index df71819c1..636ee50e4 100644 --- a/apps/client/src/widgets/buttons/calendar.ts +++ b/apps/client/src/widgets/buttons/calendar.ts @@ -46,22 +46,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.manageFirstDayOfWeek(); this.initWeekCalculation(); - // Month navigation - this.$monthSelect = this.$dropdownContent.find('[data-calendar-input="month"]'); - this.$monthSelect.on("show.bs.dropdown", (e) => { - // Don't trigger dropdownShown() at widget level when the month selection dropdown is shown, since it would cause a redundant refresh. - e.stopPropagation(); - }); - this.monthDropdown = Dropdown.getOrCreateInstance(this.$monthSelect[0]); - this.$dropdownContent.find('[data-calendar-input="month-list"] button').on("click", (e) => { - const target = e.target as HTMLElement; - const value = target.dataset.value; - if (value) { - this.date = this.date.month(parseInt(value)); - this.createMonth(); - } - }); - // Date click this.$dropdownContent.on("click", ".calendar-date", async (ev) => { const date = $(ev.target).closest(".calendar-date").attr("data-calendar-date"); @@ -139,18 +123,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.init( ?? null); } - createDay() { - const $date = $("").html(String(num)); - const dateNoteId = dateNotesForMonth[this.date.local().format('YYYY-MM-DD')]; - - if (dateNoteId) { - $newDay.addClass("calendar-date-exists").attr("data-href", `#root/${dateNoteId}`); - } - - $newDay.append($date); - return $newDay; - } - createWeekNumber(weekNumber: number) { const weekNoteId = this.date.local().format('YYYY-') + 'W' + String(weekNumber).padStart(2, '0'); let $newWeekNumber; diff --git a/apps/client/src/widgets/launch_bar/Calendar.tsx b/apps/client/src/widgets/launch_bar/Calendar.tsx index 14d5fe516..a4b4886d3 100644 --- a/apps/client/src/widgets/launch_bar/Calendar.tsx +++ b/apps/client/src/widgets/launch_bar/Calendar.tsx @@ -85,13 +85,20 @@ function CurrentMonthDays({ date, firstDayOfWeekISO, ...args }: { date: Dayjs, f let dateCursor = date; const currentMonth = date.month(); const items: VNode[] = []; + const curMonthString = date.format('YYYY-MM'); + const [ dateNotesForCurMonth, setDateNotesForCurMonth ] = useState(); + + useEffect(() => { + server.get(`special-notes/notes-for-month/${curMonthString}`).then(setDateNotesForCurMonth); + }, [ date ]); + while (dateCursor.month() === currentMonth) { const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO); if (dateCursor.isoWeekday() === firstDayOfWeekISO) { items.push() } - items.push() + items.push() dateCursor = dateCursor.add(1, "day"); } @@ -112,13 +119,16 @@ function NextMonthDays({ date, dates, ...args }: { date: Dayjs, dates: Dayjs[] } } function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDate }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string } & CalendarArgs) { + const dateNoteId = dateNotesForMonth?.[date.local().format('YYYY-MM-DD')]; return (
{date.date()} From 1af76c4d0631042b342026d83ac583a32c1e428f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 5 Dec 2025 09:11:50 +0200 Subject: [PATCH 32/83] chore(react/launch_bar): clicking on calendar days --- apps/client/src/widgets/buttons/calendar.ts | 15 --------------- .../src/widgets/launch_bar/Calendar.tsx | 9 ++++++--- .../src/widgets/launch_bar/CalendarWidget.tsx | 19 +++++++++++++++++-- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/apps/client/src/widgets/buttons/calendar.ts b/apps/client/src/widgets/buttons/calendar.ts index 636ee50e4..2e8728dc2 100644 --- a/apps/client/src/widgets/buttons/calendar.ts +++ b/apps/client/src/widgets/buttons/calendar.ts @@ -46,21 +46,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.manageFirstDayOfWeek(); this.initWeekCalculation(); - // Date click - this.$dropdownContent.on("click", ".calendar-date", async (ev) => { - const date = $(ev.target).closest(".calendar-date").attr("data-calendar-date"); - if (date) { - const note = await dateNoteService.getDayNote(date); - if (note) { - appContext.tabManager.getActiveContext()?.setNote(note.noteId); - this.dropdown?.hide(); - } else { - toastService.showError(t("calendar.cannot_find_day_note")); - } - } - ev.stopPropagation(); - }); - // Week click this.$dropdownContent.on("click", ".calendar-week-number", async (ev) => { if (!this.weekNoteEnable) { diff --git a/apps/client/src/widgets/launch_bar/Calendar.tsx b/apps/client/src/widgets/launch_bar/Calendar.tsx index a4b4886d3..5ddf56f9c 100644 --- a/apps/client/src/widgets/launch_bar/Calendar.tsx +++ b/apps/client/src/widgets/launch_bar/Calendar.tsx @@ -1,7 +1,7 @@ import { useTriliumOptionInt } from "../react/hooks"; import clsx from "clsx"; import server from "../../services/server"; -import { VNode } from "preact"; +import { TargetedMouseEvent, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { Dayjs } from "@triliumnext/commons"; import { t } from "../../services/i18n"; @@ -29,6 +29,7 @@ export interface CalendarArgs { date: Dayjs; todaysDate: Dayjs; activeDate: Dayjs | null; + onDateClicked(date: string, e: TargetedMouseEvent): void; } export default function Calendar(args: CalendarArgs) { @@ -118,8 +119,9 @@ function NextMonthDays({ date, dates, ...args }: { date: Dayjs, dates: Dayjs[] } )); } -function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDate }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string } & CalendarArgs) { - const dateNoteId = dateNotesForMonth?.[date.local().format('YYYY-MM-DD')]; +function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDate, onDateClicked }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string } & CalendarArgs) { + const dateString = date.local().format('YYYY-MM-DD'); + const dateNoteId = dateNotesForMonth?.[dateString]; return ( onDateClicked(dateString, e)} > {date.date()} diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index 03c2d57f0..1bdeba620 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -9,6 +9,8 @@ import ActionButton from "../react/ActionButton"; import { t } from "../../services/i18n"; import FormDropdownList from "../react/FormDropdownList"; import FormTextBox from "../react/FormTextBox"; +import toast from "../../services/toast"; +import date_notes from "../../services/date_notes"; const MONTHS = [ t("calendar.january"), @@ -27,7 +29,7 @@ const MONTHS = [ export default function CalendarWidget({ launcherNote }: { launcherNote: FNote }) { const { title, icon } = useLauncherIconAndTitle(launcherNote); - const [ calendarArgs, setCalendarArgs ] = useState>(); + const [ calendarArgs, setCalendarArgs ] = useState>(); const [ date, setDate ] = useState(); return ( @@ -49,7 +51,20 @@ export default function CalendarWidget({ launcherNote }: { launcherNote: FNote } > {calendarArgs && date &&
- + { + const note = await date_notes.getDayNote(date); + if (note) { + appContext.tabManager.getActiveContext()?.setNote(note.noteId); + // this.dropdown?.hide(); + } else { + toast.showError(t("calendar.cannot_find_day_note")); + } + e.stopPropagation(); + }} + {...calendarArgs} + />
} ) From d283f5dbb42599902bb5c731a434b79e819765cb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 5 Dec 2025 09:15:01 +0200 Subject: [PATCH 33/83] chore(react/launch_bar): hide dropdown when selecting date --- .../src/widgets/launch_bar/CalendarWidget.tsx | 7 +++++-- .../widgets/launch_bar/launch_bar_widgets.tsx | 2 +- apps/client/src/widgets/react/Dropdown.tsx | 16 ++++++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index 1bdeba620..9a89b2507 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -1,4 +1,4 @@ -import { Dispatch, StateUpdater, useEffect, useMemo, useState } from "preact/hooks"; +import { Dispatch, StateUpdater, useEffect, useMemo, useRef, useState } from "preact/hooks"; import FNote from "../../entities/fnote"; import { LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; import { Dayjs, dayjs } from "@triliumnext/commons"; @@ -11,6 +11,7 @@ import FormDropdownList from "../react/FormDropdownList"; import FormTextBox from "../react/FormTextBox"; import toast from "../../services/toast"; import date_notes from "../../services/date_notes"; +import { Dropdown } from "bootstrap"; const MONTHS = [ t("calendar.january"), @@ -31,6 +32,7 @@ export default function CalendarWidget({ launcherNote }: { launcherNote: FNote } const { title, icon } = useLauncherIconAndTitle(launcherNote); const [ calendarArgs, setCalendarArgs ] = useState>(); const [ date, setDate ] = useState(); + const dropdownRef = useRef(null); return ( & { icon: string }) { +export function LaunchBarDropdownButton({ children, icon, ...props }: Pick & { icon: string }) { return ( , "id" | "c onShown?: () => void; onHidden?: () => void; dropdownOptions?: Partial; + dropdownRef?: MutableRef; } -export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps }: DropdownProps) { - const dropdownRef = useRef(null); +export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef }: DropdownProps) { + const containerRef = useRef(null); const triggerRef = useRef(null); const [ shown, setShown ] = useState(false); @@ -39,6 +40,9 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi if (!triggerRef.current) return; const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current, dropdownOptions); + if (dropdownRef) { + dropdownRef.current = dropdown; + } if (forceShown) { dropdown.show(); setShown(true); @@ -57,9 +61,9 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi }, []); useEffect(() => { - if (!dropdownRef.current) return; + if (!containerRef.current) return; - const $dropdown = $(dropdownRef.current); + const $dropdown = $(containerRef.current); $dropdown.on("show.bs.dropdown", onShown); $dropdown.on("hide.bs.dropdown", onHidden); @@ -73,7 +77,7 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi const ariaId = useUniqueName("button"); return ( -
+
} From 185e5691a4882d8bad3c46b02da696e5baaea527 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 5 Dec 2025 09:47:13 +0200 Subject: [PATCH 35/83] chore(react/launch_bar): bring back week highlighting --- apps/client/src/widgets/buttons/calendar.ts | 109 ------------------ .../widgets/buttons/right_dropdown_button.ts | 86 -------------- .../src/widgets/launch_bar/Calendar.tsx | 22 ++-- .../src/widgets/launch_bar/CalendarWidget.tsx | 11 +- 4 files changed, 25 insertions(+), 203 deletions(-) delete mode 100644 apps/client/src/widgets/buttons/calendar.ts delete mode 100644 apps/client/src/widgets/buttons/right_dropdown_button.ts diff --git a/apps/client/src/widgets/buttons/calendar.ts b/apps/client/src/widgets/buttons/calendar.ts deleted file mode 100644 index 4406bd2b7..000000000 --- a/apps/client/src/widgets/buttons/calendar.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { t } from "../../services/i18n.js"; -import dateNoteService from "../../services/date_notes.js"; -import server from "../../services/server.js"; -import appContext from "../../components/app_context.js"; -import RightDropdownButtonWidget from "./right_dropdown_button.js"; -import toastService from "../../services/toast.js"; -import options from "../../services/options.js"; -import { Dropdown } from "bootstrap"; -import type { EventData } from "../../components/app_context.js"; -import { dayjs, type Dayjs } from "@triliumnext/commons"; -import type { AttributeRow, OptionDefinitions } from "@triliumnext/commons"; - -interface WeekCalculationOptions { - firstWeekType: number; - minDaysInFirstWeek: number; -} - -export default class CalendarWidget extends RightDropdownButtonWidget { - private $month!: JQuery; - private $weekHeader!: JQuery; - private $monthSelect!: JQuery; - private $yearSelect!: JQuery; - private $next!: JQuery; - private $previous!: JQuery; - private $nextYear!: JQuery; - private $previousYear!: JQuery; - private monthDropdown!: Dropdown; - // stored in ISO 1–7 - private firstDayOfWeekISO!: number; - private weekCalculationOptions!: WeekCalculationOptions; - private activeDate: Dayjs | null = null; - private todaysDate!: Dayjs; - private date!: Dayjs; - private weekNoteEnable: boolean = false; - private weekNotes: string[] = []; - - constructor(title: string = "", icon: string = "") { - super(title, icon, DROPDOWN_TPL, "calendar-dropdown-menu"); - } - - doRender() { - super.doRender(); - - this.$month = this.$dropdownContent.find('[data-calendar-area="month"]'); - - this.manageFirstDayOfWeek(); - this.initWeekCalculation(); - - // Handle click events for the entire calendar widget - this.$dropdownContent.on("click", (e) => { - const $target = $(e.target); - - // Keep dropdown open when clicking on month select button or year selector area - if ($target.closest('.btn.dropdown-toggle.select-button').length) { - e.stopPropagation(); - return; - } - - // Hide dropdown for all other cases - this.monthDropdown.hide(); - // Prevent dismissing the calendar popup by clicking on an empty space inside it. - e.stopPropagation(); - }); - } - - initWeekCalculation() { - this.weekCalculationOptions = { - firstWeekType: options.getInt("firstWeekOfYear") || 0, - minDaysInFirstWeek: options.getInt("minDaysInFirstWeek") || 4 - }; - } - - async dropdownShown() { - await this.getWeekNoteEnable(); - this.weekNotes = await server.get(`attribute-values/weekNote`); - this.init( ?? null); - } - - createWeekNumber(weekNumber: number) { - - let $newWeekNumber; - - if (this.weekNoteEnable) { - if (this.weekNotes.includes(weekNoteId)) { - $newWeekNumber.addClass("calendar-date-exists").attr("data-href", `#root/${weekNoteId}`); - } - } else { - - } - - $newWeekNumber.addClass("calendar-week-number").attr("data-calendar-week-number", weekNoteId); - return $newWeekNumber; - } - - async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - const WEEK_OPTIONS: (keyof OptionDefinitions)[] = [ - "firstDayOfWeek", - "firstWeekOfYear", - "minDaysInFirstWeek", - ]; - if (!WEEK_OPTIONS.some(opt => loadResults.getOptionNames().includes(opt))) { - return; - } - - this.manageFirstDayOfWeek(); - this.initWeekCalculation(); - this.createMonth(); - } -} diff --git a/apps/client/src/widgets/buttons/right_dropdown_button.ts b/apps/client/src/widgets/buttons/right_dropdown_button.ts deleted file mode 100644 index 45915c254..000000000 --- a/apps/client/src/widgets/buttons/right_dropdown_button.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { handleRightToLeftPlacement } from "../../services/utils.js"; -import BasicWidget from "../basic_widget.js"; -import { Tooltip, Dropdown } from "bootstrap"; -type PopoverPlacement = Tooltip.PopoverPlacement; - -const TPL = /*html*/` - -`; - -export default class RightDropdownButtonWidget extends BasicWidget { - protected iconClass: string; - protected title: string; - protected dropdownTpl: string; - protected settings: { titlePlacement: PopoverPlacement }; - protected $dropdownMenu!: JQuery; - protected dropdown!: Dropdown; - protected $tooltip!: JQuery; - protected tooltip!: Tooltip; - private dropdownClass?: string; - public $dropdownContent!: JQuery; - - constructor(title: string, iconClass: string, dropdownTpl: string, dropdownClass?: string) { - super(); - - this.iconClass = iconClass; - this.title = title; - this.dropdownTpl = dropdownTpl; - this.dropdownClass = dropdownClass; - - this.settings = { - titlePlacement: "right" - }; - } - - doRender() { - this.$widget = $(TPL); - this.$dropdownMenu = this.$widget.find(".dropdown-menu"); - if (this.dropdownClass) { - this.$dropdownMenu.addClass(this.dropdownClass); - } - this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], { - popperConfig: { - placement: this.settings.titlePlacement, - } - }); - - this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title); - this.tooltip = new Tooltip(this.$tooltip[0], { - placement: handleRightToLeftPlacement(this.settings.titlePlacement), - fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ] - }); - - this.$widget - .find(".right-dropdown-button") - .addClass(this.iconClass) - .on("click", () => this.tooltip.hide()) - .on("mouseenter", () => this.tooltip.show()) - .on("mouseleave", () => this.tooltip.hide()); - - this.$widget.on("show.bs.dropdown", async () => { - await this.dropdownShown(); - - const rect = this.$dropdownMenu[0].getBoundingClientRect(); - const windowHeight = $(window).height() || 0; - const pixelsToBottom = windowHeight - rect.bottom; - - if (pixelsToBottom < 0) { - this.$dropdownMenu.css("top", pixelsToBottom); - } - }); - - this.$dropdownContent = $(this.dropdownTpl); - this.$widget.find(".dropdown-menu").append(this.$dropdownContent); - } - - // to be overridden - async dropdownShown(): Promise {} -} diff --git a/apps/client/src/widgets/launch_bar/Calendar.tsx b/apps/client/src/widgets/launch_bar/Calendar.tsx index b058dd8c5..2b7225db5 100644 --- a/apps/client/src/widgets/launch_bar/Calendar.tsx +++ b/apps/client/src/widgets/launch_bar/Calendar.tsx @@ -31,6 +31,7 @@ export interface CalendarArgs { activeDate: Dayjs | null; onDateClicked(date: string, e: TargetedMouseEvent): void; onWeekClicked?: (week: string, e: TargetedMouseEvent) => void; + weekNotes: string[]; } export default function Calendar(args: CalendarArgs) { @@ -77,7 +78,7 @@ function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { da return ( <> - + {dates.map(date => )} ) @@ -97,7 +98,7 @@ function CurrentMonthDays({ date, firstDayOfWeekISO, ...args }: { date: Dayjs, f while (dateCursor.month() === currentMonth) { const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO); if (dateCursor.isoWeekday() === firstDayOfWeekISO) { - items.push() + items.push() } items.push() @@ -141,18 +142,25 @@ function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDat ); } -function CalendarWeek({ date, weekNumber, onWeekClicked }: { weekNumber: number } & Pick) { +function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumber: number, weekNotes: string[] } & Pick) { + const weekString = date.local().format('YYYY-') + 'W' + String(weekNumber).padStart(2, '0'); + if (onWeekClicked) { - const weekNoteId = date.local().format('YYYY-') + 'W' + String(weekNumber).padStart(2, '0'); return ( onWeekClicked(weekNoteId, e)} + className={clsx("calendar-week-number", "calendar-date", + weekNotes.includes(weekString) && "calendar-date-exists")} + data-calendar-week-number={weekNumber} + onClick={(e) => onWeekClicked(weekString, e)} >{weekNumber} ) } - return ({weekNumber}); + return ( + {weekNumber}); } export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) { diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx index 617dd8c80..70c4dc76a 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.tsx +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.tsx @@ -13,6 +13,7 @@ import toast from "../../services/toast"; import date_notes from "../../services/date_notes"; import { Dropdown } from "bootstrap"; import search from "../../services/search"; +import server from "../../services/server"; const MONTHS = [ t("calendar.january"), @@ -35,6 +36,7 @@ export default function CalendarWidget({ launcherNote }: { launcherNote: FNote } const [ date, setDate ] = useState(); const dropdownRef = useRef(null); const [ enableWeekNotes, setEnableWeekNotes ] = useState(false); + const [ weekNotes, setWeekNotes ] = useState([]); const calendarRootRef = useRef(); async function checkEnableWeekNotes() { @@ -45,7 +47,13 @@ export default function CalendarWidget({ launcherNote }: { launcherNote: FNote } } if (!calendarRootRef.current) return; - setEnableWeekNotes(calendarRootRef.current.hasLabel("enableWeekNote")); + + const enableWeekNotes = calendarRootRef.current.hasLabel("enableWeekNote"); + setEnableWeekNotes(enableWeekNotes); + + if (enableWeekNotes) { + server.get(`attribute-values/weekNote`).then(setWeekNotes); + } } return ( @@ -91,6 +99,7 @@ export default function CalendarWidget({ launcherNote }: { launcherNote: FNote } } e.stopPropagation(); } : undefined} + weekNotes={weekNotes} {...calendarArgs} />
} From caaa3583a7b0f167f70c44f80ecbf864bf689d8b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 5 Dec 2025 10:22:34 +0200 Subject: [PATCH 36/83] chore(react/launch_bar): port sync status --- .../src/widgets/containers/launcher.tsx | 8 +- .../src/widgets/launch_bar/SyncStatus.css | 26 ++++ .../src/widgets/launch_bar/SyncStatus.tsx | 118 ++++++++++++++ apps/client/src/widgets/sync_status.ts | 146 ------------------ 4 files changed, 148 insertions(+), 150 deletions(-) create mode 100644 apps/client/src/widgets/launch_bar/SyncStatus.css create mode 100644 apps/client/src/widgets/launch_bar/SyncStatus.tsx delete mode 100644 apps/client/src/widgets/sync_status.ts diff --git a/apps/client/src/widgets/containers/launcher.tsx b/apps/client/src/widgets/containers/launcher.tsx index 2e9b6693f..93820296d 100644 --- a/apps/client/src/widgets/containers/launcher.tsx +++ b/apps/client/src/widgets/containers/launcher.tsx @@ -1,4 +1,3 @@ -import SyncStatusWidget from "../sync_status.js"; import BasicWidget, { wrapReactWidgets } from "../basic_widget.js"; import utils, { isMobile } from "../../services/utils.js"; import type FNote from "../../entities/fnote.js"; @@ -16,6 +15,7 @@ import { ParentComponent } from "../react/react_utils.jsx"; import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { LaunchBarActionButton, useLauncherIconAndTitle } from "../launch_bar/launch_bar_widgets.jsx"; import CalendarWidget from "../launch_bar/CalendarWidget.jsx"; +import SyncStatus from "../launch_bar/SyncStatus.jsx"; interface InnerWidget extends BasicWidget { settings?: { @@ -106,11 +106,11 @@ export default class LauncherWidget extends BasicWidget { return ; case "bookmarks": - return + return ; case "protectedSession": - return + return ; case "syncStatus": - return new SyncStatusWidget(); + return ; case "backInHistoryButton": return case "forwardInHistoryButton": diff --git a/apps/client/src/widgets/launch_bar/SyncStatus.css b/apps/client/src/widgets/launch_bar/SyncStatus.css new file mode 100644 index 000000000..dc9795e6a --- /dev/null +++ b/apps/client/src/widgets/launch_bar/SyncStatus.css @@ -0,0 +1,26 @@ +.sync-status { + box-sizing: border-box; +} + +.sync-status .sync-status-icon { + display: inline-block; + position: relative; + top: -5px; + font-size: 110%; +} + +.sync-status .sync-status-sub-icon { + font-size: 40%; + position: absolute; + inset-inline-start: 0; + top: 16px; +} + +.sync-status .sync-status-icon span { + border: none !important; +} + +.sync-status-icon:not(.sync-status-in-progress):hover { + background-color: var(--hover-item-background-color); + cursor: pointer; +} \ No newline at end of file diff --git a/apps/client/src/widgets/launch_bar/SyncStatus.tsx b/apps/client/src/widgets/launch_bar/SyncStatus.tsx new file mode 100644 index 000000000..b6f122d44 --- /dev/null +++ b/apps/client/src/widgets/launch_bar/SyncStatus.tsx @@ -0,0 +1,118 @@ +import { useEffect, useRef, useState } from "preact/hooks"; +import "./SyncStatus.css"; +import { t } from "../../services/i18n"; +import clsx from "clsx"; +import { escapeQuotes } from "../../services/utils"; +import { useStaticTooltip, useTriliumOption } from "../react/hooks"; +import sync from "../../services/sync"; +import ws, { subscribeToMessages, unsubscribeToMessage } from "../../services/ws"; +import { WebSocketMessage } from "@triliumnext/commons"; + +type SyncState = "unknown" | "in-progress" + | "connected-with-changes" | "connected-no-changes" + | "disconnected-with-changes" | "disconnected-no-changes"; + +interface StateMapping { + title: string; + icon: string; + hasChanges?: boolean; +} + +const STATE_MAPPINGS: Record = { + unknown: { + title: t("sync_status.unknown"), + icon: "bx bx-time" + }, + "connected-with-changes": { + title: t("sync_status.connected_with_changes"), + icon: "bx bx-wifi", + hasChanges: true + }, + "connected-no-changes": { + title: t("sync_status.connected_no_changes"), + icon: "bx bx-wifi" + }, + "disconnected-with-changes": { + title: t("sync_status.disconnected_with_changes"), + icon: "bx bx-wifi-off", + hasChanges: true + }, + "disconnected-no-changes": { + title: t("sync_status.disconnected_no_changes"), + icon: "bx bx-wifi-off" + }, + "in-progress": { + title: t("sync_status.in_progress"), + icon: "bx bx-analyse bx-spin" + } +}; + +export default function SyncStatus() { + const syncState = useSyncStatus(); + const { title, icon, hasChanges } = STATE_MAPPINGS[syncState]; + const spanRef = useRef(null); + const [ syncServerHost ] = useTriliumOption("syncServerHost"); + useStaticTooltip(spanRef, { + html: true + // TODO: Placement + }); + + return (syncServerHost && +
+
+ { + if (syncState === "in-progress") return; + sync.syncNow(); + }} + > + {hasChanges && ( + + )} + +
+
+ ) +} + +function useSyncStatus() { + const [ syncState, setSyncState ] = useState("unknown"); + + useEffect(() => { + let lastSyncedPush: number; + + function onMessage(message: WebSocketMessage) { + // First, read last synced push. + if ("lastSyncedPush" in message) { + lastSyncedPush = message.lastSyncedPush; + } else if ("data" in message && message.data && "lastSyncedPush" in message.data && lastSyncedPush) { + lastSyncedPush = message.data.lastSyncedPush; + } + + // Determine if all changes were pushed. + const allChangesPushed = lastSyncedPush === ws.getMaxKnownEntityChangeSyncId(); + + let syncState: SyncState = "unknown"; + if (message.type === "sync-pull-in-progress") { + syncState = "in-progress"; + } else if (message.type === "sync-push-in-progress") { + syncState = "in-progress"; + } else if (message.type === "sync-finished") { + syncState = allChangesPushed ? "connected-no-changes" : "connected-with-changes"; + } else if (message.type === "sync-failed") { + syncState = allChangesPushed ? "disconnected-no-changes" : "disconnected-with-changes"; + } else if (message.type === "frontend-update") { + lastSyncedPush = message.data.lastSyncedPush; + } + setSyncState(syncState); + } + + subscribeToMessages(onMessage); + return () => unsubscribeToMessage(onMessage); + }, []); + + return syncState; +} diff --git a/apps/client/src/widgets/sync_status.ts b/apps/client/src/widgets/sync_status.ts deleted file mode 100644 index 3ce671034..000000000 --- a/apps/client/src/widgets/sync_status.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { t } from "../services/i18n.js"; -import BasicWidget from "./basic_widget.js"; -import ws from "../services/ws.js"; -import options from "../services/options.js"; -import syncService from "../services/sync.js"; -import { escapeQuotes, handleRightToLeftPlacement } from "../services/utils.js"; -import { Tooltip } from "bootstrap"; -import { WebSocketMessage } from "@triliumnext/commons"; - -const TPL = /*html*/` -
- - -
- - - - - - - - - - - - - - -
-
-`; - -export default class SyncStatusWidget extends BasicWidget { - - syncState: "unknown" | "in-progress" | "connected" | "disconnected"; - allChangesPushed: boolean; - lastSyncedPush!: number; - settings: { - // TriliumNextTODO: narrow types and use TitlePlacement Type - titlePlacement: string; - }; - - constructor() { - super(); - - this.syncState = "unknown"; - this.allChangesPushed = false; - this.settings = { - titlePlacement: "right" - }; - } - - doRender() { - this.$widget = $(TPL); - this.$widget.hide(); - - this.$widget.find(".sync-status-icon:not(.sync-status-in-progress)").on("click", () => syncService.syncNow()); - - ws.subscribeToMessages((message) => this.processMessage(message)); - } - - showIcon(className: string) { - if (!options.get("syncServerHost")) { - this.toggleInt(false); - return; - } - - Tooltip.getOrCreateInstance(this.$widget.find(`.sync-status-${className}`)[0], { - html: true, - placement: handleRightToLeftPlacement(this.settings.titlePlacement), - fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ] - }); - - this.$widget.show(); - this.$widget.find(".sync-status-icon").hide(); - this.$widget.find(`.sync-status-${className}`).show(); - } - - processMessage(message: WebSocketMessage) { - if (message.type === "sync-pull-in-progress") { - this.syncState = "in-progress"; - this.lastSyncedPush = message.lastSyncedPush; - } else if (message.type === "sync-push-in-progress") { - this.syncState = "in-progress"; - this.lastSyncedPush = message.lastSyncedPush; - } else if (message.type === "sync-finished") { - this.syncState = "connected"; - this.lastSyncedPush = message.lastSyncedPush; - } else if (message.type === "sync-failed") { - this.syncState = "disconnected"; - this.lastSyncedPush = message.lastSyncedPush; - } else if (message.type === "frontend-update") { - this.lastSyncedPush = message.data.lastSyncedPush; - } - - this.allChangesPushed = this.lastSyncedPush === ws.getMaxKnownEntityChangeSyncId(); - - if (["unknown", "in-progress"].includes(this.syncState)) { - this.showIcon(this.syncState); - } else { - this.showIcon(`${this.syncState}-${this.allChangesPushed ? "no-changes" : "with-changes"}`); - } - } -} From d511085db3af24fdbdec53015681007b4fa8ad09 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 5 Dec 2025 11:31:10 +0200 Subject: [PATCH 37/83] chore(react/launch_bar): port launcher container & launcher --- apps/client/src/layouts/desktop_layout.tsx | 6 +- apps/client/src/layouts/mobile_layout.tsx | 4 +- apps/client/src/services/utils.ts | 2 +- .../src/widgets/containers/launcher.tsx | 198 ------------------ .../widgets/containers/launcher_container.ts | 78 ------- .../widgets/launch_bar/LauncherContainer.tsx | 120 +++++++++++ .../launch_bar/LauncherDefinitions.tsx | 80 +++++++ 7 files changed, 206 insertions(+), 282 deletions(-) delete mode 100644 apps/client/src/widgets/containers/launcher.tsx delete mode 100644 apps/client/src/widgets/containers/launcher_container.ts create mode 100644 apps/client/src/widgets/launch_bar/LauncherContainer.tsx create mode 100644 apps/client/src/widgets/launch_bar/LauncherDefinitions.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 479a163f2..50dc05d99 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -10,7 +10,6 @@ import FlexContainer from "../widgets/containers/flex_container.js"; import FloatingButtons from "../widgets/FloatingButtons.jsx"; import GlobalMenu from "../widgets/buttons/global_menu.jsx"; import HighlightsListWidget from "../widgets/highlights_list.js"; -import LauncherContainer from "../widgets/containers/launcher_container.js"; import LeftPaneContainer from "../widgets/containers/left_pane_container.js"; import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js"; import MovePaneButton from "../widgets/buttons/move_pane_button.js"; @@ -44,6 +43,7 @@ import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status import NoteDetail from "../widgets/NoteDetail.jsx"; import PromotedAttributes from "../widgets/PromotedAttributes.jsx"; import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx"; +import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx"; export default class DesktopLayout { @@ -184,14 +184,14 @@ export default class DesktopLayout { launcherPane = new FlexContainer("row") .css("height", "53px") .class("horizontal") - .child(new LauncherContainer(true)) + .child() .child(); } else { launcherPane = new FlexContainer("column") .css("width", "53px") .class("vertical") .child() - .child(new LauncherContainer(false)) + .child() .child(); } diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index 0c2b3e7ea..99c460024 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -6,7 +6,6 @@ import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx"; import FlexContainer from "../widgets/containers/flex_container.js"; import FloatingButtons from "../widgets/FloatingButtons.jsx"; import GlobalMenuWidget from "../widgets/buttons/global_menu.js"; -import LauncherContainer from "../widgets/containers/launcher_container.js"; import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js"; import NoteList from "../widgets/collections/NoteList.jsx"; import NoteTitleWidget from "../widgets/note_title.js"; @@ -30,6 +29,7 @@ import NoteDetail from "../widgets/NoteDetail.jsx"; import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx"; import PromotedAttributes from "../widgets/PromotedAttributes.jsx"; import SplitNoteContainer from "../widgets/containers/split_note_container.js"; +import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx"; const MOBILE_CSS = `