fix(calendar): integrate @taskgenius/calendar and fix date handling

- Refactor calendar component to use @taskgenius/calendar for month/week/day views
- Add working hours configuration (showWorkingHoursOnly, workingHoursStart/End)
- Fix drag-and-drop date handling for all-day and timed events
- Add handleTGEventResize for event resizing support
- Improve event callback signatures to match library API
- Add NormalizedDateFnsAdapter for date-fns v4 compatibility
This commit is contained in:
Quorafind 2025-11-25 10:42:26 +08:00
parent cee8349ba9
commit 8ffd5cee0e
7 changed files with 2792 additions and 786 deletions

View file

@ -118,6 +118,11 @@ export interface CalendarSpecificConfig {
viewType: "calendar"; // Discriminator
firstDayOfWeek?: number; // 0=Sun, 1=Mon, ..., 6=Sat; undefined=locale default
hideWeekends?: boolean; // Whether to hide weekend columns/cells in calendar views
// Working hours configuration (v0.6.0+)
showWorkingHoursOnly?: boolean; // Only show working hours in week/day views
workingHoursStart?: number; // Start hour (0-23), default: 9
workingHoursEnd?: number; // End hour (0-23), default: 18
}
export interface GanttSpecificConfig {
@ -1347,6 +1352,9 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
viewType: "calendar",
firstDayOfWeek: undefined, // Use locale default initially
hideWeekends: false, // Show weekends by default
showWorkingHoursOnly: false, // Show all hours by default
workingHoursStart: 9, // Default: 9 AM
workingHoursEnd: 18, // Default: 6 PM
} as CalendarSpecificConfig,
},
{

File diff suppressed because it is too large Load diff

View file

@ -75,16 +75,14 @@ export abstract class BaseQuickCaptureModal extends Modal {
app: App,
plugin: TaskProgressBarPlugin,
initialMode: QuickCaptureMode = "checkbox",
metadata?: TaskMetadata
metadata?: TaskMetadata,
) {
super(app);
this.plugin = plugin;
this.currentMode = initialMode;
const scopeControls =
this.plugin.settings.fileFilter?.scopeControls;
this.inlineModeAvailable =
scopeControls?.inlineTasksEnabled !== false;
const scopeControls = this.plugin.settings.fileFilter?.scopeControls;
this.inlineModeAvailable = scopeControls?.inlineTasksEnabled !== false;
this.fileModeAvailable =
(this.plugin.settings.fileSource?.enabled ?? false) &&
scopeControls?.fileTasksEnabled !== false;
@ -102,7 +100,7 @@ export abstract class BaseQuickCaptureModal extends Modal {
// Initialize metadata
if (metadata) {
this.taskMetadata = metadata;
this.taskMetadata = this.normalizeMetadataDates(metadata);
// Auto-switch to file mode if location is file
if (metadata.location === "file") {
this.currentMode = "file";
@ -129,6 +127,31 @@ export abstract class BaseQuickCaptureModal extends Modal {
this.initializeTargetFile();
}
private normalizeMetadataDates(metadata: TaskMetadata): TaskMetadata {
const normalize = (value?: Date | string | number | null) => {
if (value === null || value === undefined) return undefined;
if (value instanceof Date) return value;
if (typeof value === "number") {
const fromNumber = new Date(value);
return isNaN(fromNumber.getTime()) ? undefined : fromNumber;
}
if (typeof value === "string") {
const isoParsed = moment(value, moment.ISO_8601, true);
if (isoParsed.isValid()) return isoParsed.toDate();
const strictDate = moment(value, "YYYY-MM-DD", true);
if (strictDate.isValid()) return strictDate.toDate();
}
return undefined;
};
return {
...metadata,
startDate: normalize(metadata.startDate),
dueDate: normalize(metadata.dueDate),
scheduledDate: normalize(metadata.scheduledDate),
};
}
/**
* Initialize target file based on settings
*/
@ -155,7 +178,7 @@ export abstract class BaseQuickCaptureModal extends Modal {
* Called when the modal is opened
*/
onOpen() {
const {contentEl} = this;
const { contentEl } = this;
this.modalEl.toggleClass("quick-capture-modal", true);
this.modalEl.toggleClass(`quick-capture-${this.currentMode}`, true);
@ -220,11 +243,11 @@ export abstract class BaseQuickCaptureModal extends Modal {
checkboxButtonEl.setAttribute("role", "tab");
checkboxButtonEl.setAttribute(
"aria-selected",
String(this.currentMode === "checkbox")
String(this.currentMode === "checkbox"),
);
checkboxButtonEl.setAttribute(
"aria-controls",
"quick-capture-content"
"quick-capture-content",
);
checkboxButtonEl.setAttribute("data-mode", "checkbox");
@ -256,7 +279,7 @@ export abstract class BaseQuickCaptureModal extends Modal {
fileButtonEl.setAttribute("role", "tab");
fileButtonEl.setAttribute(
"aria-selected",
String(this.currentMode === "file")
String(this.currentMode === "file"),
);
fileButtonEl.setAttribute("aria-controls", "quick-capture-content");
fileButtonEl.setAttribute("data-mode", "file");
@ -311,7 +334,7 @@ export abstract class BaseQuickCaptureModal extends Modal {
cls: "quick-capture-continue",
});
continueButton.addEventListener("click", () =>
this.handleContinueCreate()
this.handleContinueCreate(),
);
// Right side: Main action buttons
@ -343,8 +366,8 @@ export abstract class BaseQuickCaptureModal extends Modal {
if (mode === "file" && !this.fileModeAvailable) {
new Notice(
t(
"File Task is disabled. Enable FileSource in Settings to use File mode."
)
"File Task is disabled. Enable FileSource in Settings to use File mode.",
),
);
return;
}
@ -354,20 +377,19 @@ export abstract class BaseQuickCaptureModal extends Modal {
// Save current state
const savedContent = this.capturedContent;
const savedMetadata = {...this.taskMetadata};
const savedMetadata = { ...this.taskMetadata };
// Update mode
this.currentMode = mode;
// Persist last used mode to local storage
try {
this.app.saveLocalStorage(LAST_USED_MODE_KEY, mode);
} catch {
}
} catch {}
// Update modal classes
this.modalEl.removeClass(
"quick-capture-checkbox",
"quick-capture-file"
"quick-capture-file",
);
this.modalEl.addClass(`quick-capture-${mode}`);
@ -391,11 +413,11 @@ export abstract class BaseQuickCaptureModal extends Modal {
// Update button text
const submitButton = this.footerContainer?.querySelector(
".mod-cta"
".mod-cta",
) as HTMLButtonElement;
if (submitButton) {
submitButton.setText(
mode === "file" ? t("Save as File") : t("Add Task")
mode === "file" ? t("Save as File") : t("Add Task"),
);
}
}
@ -470,7 +492,7 @@ export abstract class BaseQuickCaptureModal extends Modal {
new Notice(
this.currentMode === "file"
? t("File saved successfully")
: t("Task added successfully")
: t("Task added successfully"),
);
if (!this.keepOpenAfterCapture) {
@ -506,12 +528,12 @@ export abstract class BaseQuickCaptureModal extends Modal {
!targetFile.includes("/")
) {
targetFile = this.sanitizeFilePath(
`${defaultFolder}/${targetFile}`
`${defaultFolder}/${targetFile}`,
);
}
processedContent = await this.buildFileModeContent(
content,
processedContent
processedContent,
);
}
@ -618,10 +640,9 @@ export abstract class BaseQuickCaptureModal extends Modal {
protected async buildFileModeContent(
rawContent: string,
processedContent: string,
options: { preview?: boolean } = {}
options: { preview?: boolean } = {},
): Promise<string> {
const createFileMode =
this.plugin.settings.quickCapture.createFileMode;
const createFileMode = this.plugin.settings.quickCapture.createFileMode;
const useTemplate = !!createFileMode?.useTemplate;
if (useTemplate) {
@ -635,37 +656,35 @@ export abstract class BaseQuickCaptureModal extends Modal {
await this.app.vault.read(templateFile);
const merged = this.mergeContentIntoTemplate(
templateContent,
processedContent
processedContent,
);
return this.ensureMinimalFrontmatter(merged);
} catch (error) {
console.error(
"Failed to read quick capture template:",
error
error,
);
if (!options.preview) {
new Notice(
`${t("Failed to read template file:")} ${templatePath}`
`${t("Failed to read template file:")} ${templatePath}`,
);
}
}
} else if (!options.preview) {
new Notice(
`${t("Template file not found:")} ${templatePath}`
`${t("Template file not found:")} ${templatePath}`,
);
}
} else if (!options.preview) {
new Notice(
t(
"Template file is not configured for Quick Capture file mode."
)
"Template file is not configured for Quick Capture file mode.",
),
);
}
}
const hasFrontmatter = processedContent
.trimStart()
.startsWith("---");
const hasFrontmatter = processedContent.trimStart().startsWith("---");
if (useTemplate && hasFrontmatter) {
return processedContent;
}
@ -679,7 +698,7 @@ export abstract class BaseQuickCaptureModal extends Modal {
private mergeContentIntoTemplate(
templateContent: string,
captureContent: string
captureContent: string,
): string {
if (!templateContent) {
return captureContent;
@ -701,14 +720,12 @@ export abstract class BaseQuickCaptureModal extends Modal {
return content;
}
const statusText = this.mapStatusToText(this.taskMetadata.status);
return `---\nstatus: ${JSON.stringify(
statusText
)}\n---\n\n${content}`;
return `---\nstatus: ${JSON.stringify(statusText)}\n---\n\n${content}`;
}
private buildFullFrontmatter(
processedContent: string,
rawContent: string
rawContent: string,
): string {
const trimmed = processedContent.trimStart();
if (trimmed.startsWith("---")) {
@ -743,19 +760,22 @@ export abstract class BaseQuickCaptureModal extends Modal {
const yamlLines: string[] = [];
yamlLines.push(`status: ${JSON.stringify(statusText)}`);
if (dueDate) yamlLines.push(`dueDate: ${JSON.stringify(dueDate)}`);
if (startDate) yamlLines.push(`startDate: ${JSON.stringify(startDate)}`);
if (startDate)
yamlLines.push(`startDate: ${JSON.stringify(startDate)}`);
if (scheduledDate)
yamlLines.push(`scheduledDate: ${JSON.stringify(scheduledDate)}`);
if (priorityVal)
yamlLines.push(`priority: ${JSON.stringify(priorityVal)}`);
if (projectVal) yamlLines.push(`project: ${JSON.stringify(projectVal)}`);
if (contextVal) yamlLines.push(`context: ${JSON.stringify(contextVal)}`);
if (projectVal)
yamlLines.push(`project: ${JSON.stringify(projectVal)}`);
if (contextVal)
yamlLines.push(`context: ${JSON.stringify(contextVal)}`);
if (repeatVal) yamlLines.push(`repeat: ${JSON.stringify(repeatVal)}`);
if (mergedTags.length > 0) {
yamlLines.push(
`tags: [${mergedTags
.map((t) => JSON.stringify(t))
.join(", ")}]`
.join(", ")}]`,
);
}
@ -791,7 +811,7 @@ export abstract class BaseQuickCaptureModal extends Modal {
protected formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
2,
"0"
"0",
)}-${String(date.getDate()).padStart(2, "0")}`;
}

View file

@ -124,7 +124,7 @@ export class QuickCaptureModal extends Modal {
app: App,
plugin: TaskProgressBarPlugin,
metadata?: TaskMetadata,
useFullFeaturedMode: boolean = false
useFullFeaturedMode: boolean = false,
) {
super(app);
this.plugin = plugin;
@ -135,7 +135,7 @@ export class QuickCaptureModal extends Modal {
// Initialize target file path based on target type
if (this.plugin.settings.quickCapture.targetType === "daily-note") {
const dateStr = moment().format(
this.plugin.settings.quickCapture.dailyNoteSettings.format
this.plugin.settings.quickCapture.dailyNoteSettings.format,
);
// For daily notes, the format might include path separators (e.g., YYYY-MM/YYYY-MM-DD)
// We need to preserve the path structure and only sanitize the final filename
@ -153,11 +153,18 @@ export class QuickCaptureModal extends Modal {
// Initialize time parsing service
this.timeParsingService = new TimeParsingService(
this.plugin.settings.timeParsing || DEFAULT_TIME_PARSING_CONFIG
this.plugin.settings.timeParsing || DEFAULT_TIME_PARSING_CONFIG,
);
if (metadata) {
this.taskMetadata = metadata;
this.taskMetadata = {
...metadata,
startDate: this.normalizeIncomingDate(metadata.startDate),
dueDate: this.normalizeIncomingDate(metadata.dueDate),
scheduledDate: this.normalizeIncomingDate(
metadata.scheduledDate,
),
};
}
this.useFullFeaturedMode = useFullFeaturedMode && !Platform.isPhone;
@ -182,7 +189,7 @@ export class QuickCaptureModal extends Modal {
if (this.markdownEditor?.editor?.editor) {
this.universalSuggest =
this.suggestManager.enableForQuickCaptureModal(
this.markdownEditor.editor.editor
this.markdownEditor.editor.editor,
);
this.universalSuggest.enable();
}
@ -241,7 +248,7 @@ export class QuickCaptureModal extends Modal {
this.tempTargetFilePath = file.path;
// Focus current editor
this.markdownEditor?.editor?.focus();
}
},
);
}
}
@ -294,7 +301,7 @@ export class QuickCaptureModal extends Modal {
targetFileEl.textContent = file.path;
this.tempTargetFilePath = file.path;
this.markdownEditor?.editor?.focus();
}
},
);
}
@ -330,7 +337,7 @@ export class QuickCaptureModal extends Modal {
this.taskMetadata.status = status;
this.updatePreview();
},
}
},
);
statusComponent.load();
@ -340,7 +347,7 @@ export class QuickCaptureModal extends Modal {
.setValue(
this.taskMetadata.startDate
? this.formatDate(this.taskMetadata.startDate)
: ""
: "",
)
.onChange((value) => {
if (value) {
@ -366,7 +373,7 @@ export class QuickCaptureModal extends Modal {
.setValue(
this.taskMetadata.dueDate
? this.formatDate(this.taskMetadata.dueDate)
: ""
: "",
)
.onChange((value) => {
if (value) {
@ -394,7 +401,7 @@ export class QuickCaptureModal extends Modal {
.setValue(
this.taskMetadata.scheduledDate
? this.formatDate(this.taskMetadata.scheduledDate)
: ""
: "",
)
.onChange((value) => {
if (value) {
@ -405,8 +412,7 @@ export class QuickCaptureModal extends Modal {
this.taskMetadata.scheduledDate = undefined;
// Reset manual flag when cleared
if (this.taskMetadata.manuallySet) {
this.taskMetadata.manuallySet.scheduledDate =
false;
this.taskMetadata.manuallySet.scheduledDate = false;
}
}
this.updatePreview();
@ -486,7 +492,7 @@ export class QuickCaptureModal extends Modal {
this.app,
this.previewContainerEl,
"",
false
false,
);
this.setupMarkdownEditor(editorContainer);
@ -512,7 +518,7 @@ export class QuickCaptureModal extends Modal {
updatePreview() {
if (this.previewContainerEl) {
this.markdownRenderer?.render(
this.processContentWithMetadata(this.capturedContent)
this.processContentWithMetadata(this.capturedContent),
);
}
}
@ -564,7 +570,7 @@ export class QuickCaptureModal extends Modal {
this.updatePreview();
}
},
}
},
);
this.markdownEditor?.scope.register(
@ -580,7 +586,7 @@ export class QuickCaptureModal extends Modal {
this.handleSubmit();
}
return true;
}
},
);
if (targetFileEl) {
@ -597,7 +603,7 @@ export class QuickCaptureModal extends Modal {
targetFileEl.focus();
}
return true;
}
},
);
}
@ -668,14 +674,17 @@ export class QuickCaptureModal extends Modal {
// Preserve the original indentation from the original line
const originalIndent = indentMatch[1];
const cleanedContent = this.cleanTemporaryMarks(
cleanedLine.trim()
cleanedLine.trim(),
);
processedLines.push(originalIndent + cleanedContent);
} else if (isTaskOrList) {
// If it's a task, add line-specific metadata
if (cleanedLine.trim().match(/^(-|\d+\.|\*|\+)\s+\[[^\]]+\]/)) {
processedLines.push(
this.addLineMetadataToTask(cleanedLine, lineParseResult)
this.addLineMetadataToTask(
cleanedLine,
lineParseResult,
),
);
} else {
// If it's a list item but not a task, convert to task and add line-specific metadata
@ -686,14 +695,14 @@ export class QuickCaptureModal extends Modal {
cleanedLine
.trim()
.substring(listPrefix?.length || 0)
.trim()
.trim(),
);
// Use the specified status or default to empty checkbox
const statusMark = this.taskMetadata.status || " ";
const taskLine = `${listPrefix} [${statusMark}] ${restOfLine}`;
processedLines.push(
this.addLineMetadataToTask(taskLine, lineParseResult)
this.addLineMetadataToTask(taskLine, lineParseResult),
);
}
} else {
@ -703,7 +712,7 @@ export class QuickCaptureModal extends Modal {
const cleanedContent = this.cleanTemporaryMarks(cleanedLine);
const taskLine = `- [${statusMark}] ${cleanedContent}`;
processedLines.push(
this.addLineMetadataToTask(taskLine, lineParseResult)
this.addLineMetadataToTask(taskLine, lineParseResult),
);
}
}
@ -726,7 +735,7 @@ export class QuickCaptureModal extends Modal {
*/
addLineMetadataToTask(
taskLine: string,
lineParseResult: LineParseResult
lineParseResult: LineParseResult,
): string {
const metadata = this.generateLineMetadata(lineParseResult);
if (!metadata) return taskLine;
@ -756,7 +765,7 @@ export class QuickCaptureModal extends Modal {
metadata.push(
useDataviewFormat
? `[start:: ${formattedStartDate}]`
: `🛫 ${formattedStartDate}`
: `🛫 ${formattedStartDate}`,
);
}
@ -765,7 +774,7 @@ export class QuickCaptureModal extends Modal {
metadata.push(
useDataviewFormat
? `[due:: ${formattedDueDate}]`
: `📅 ${formattedDueDate}`
: `📅 ${formattedDueDate}`,
);
}
@ -774,7 +783,7 @@ export class QuickCaptureModal extends Modal {
metadata.push(
useDataviewFormat
? `[scheduled:: ${formattedScheduledDate}]`
: `${formattedScheduledDate}`
: `${formattedScheduledDate}`,
);
}
@ -837,7 +846,7 @@ export class QuickCaptureModal extends Modal {
this.plugin.settings.preferMetadataFormat
] || "project";
metadata.push(
`[${projectPrefix}:: ${this.taskMetadata.project}]`
`[${projectPrefix}:: ${this.taskMetadata.project}]`,
);
} else {
const projectPrefix =
@ -856,7 +865,7 @@ export class QuickCaptureModal extends Modal {
this.plugin.settings.preferMetadataFormat
] || "context";
metadata.push(
`[${contextPrefix}:: ${this.taskMetadata.context}]`
`[${contextPrefix}:: ${this.taskMetadata.context}]`,
);
} else {
const contextPrefix =
@ -872,7 +881,7 @@ export class QuickCaptureModal extends Modal {
metadata.push(
useDataviewFormat
? `[repeat:: ${this.taskMetadata.recurrence}]`
: `🔁 ${this.taskMetadata.recurrence}`
: `🔁 ${this.taskMetadata.recurrence}`,
);
}
@ -886,12 +895,12 @@ export class QuickCaptureModal extends Modal {
// Format dates to strings in YYYY-MM-DD format
if (this.taskMetadata.startDate) {
const formattedStartDate = this.formatDate(
this.taskMetadata.startDate
this.taskMetadata.startDate,
);
metadata.push(
useDataviewFormat
? `[start:: ${formattedStartDate}]`
: `🛫 ${formattedStartDate}`
: `🛫 ${formattedStartDate}`,
);
}
@ -900,18 +909,18 @@ export class QuickCaptureModal extends Modal {
metadata.push(
useDataviewFormat
? `[due:: ${formattedDueDate}]`
: `📅 ${formattedDueDate}`
: `📅 ${formattedDueDate}`,
);
}
if (this.taskMetadata.scheduledDate) {
const formattedScheduledDate = this.formatDate(
this.taskMetadata.scheduledDate
this.taskMetadata.scheduledDate,
);
metadata.push(
useDataviewFormat
? `[scheduled:: ${formattedScheduledDate}]`
: `${formattedScheduledDate}`
: `${formattedScheduledDate}`,
);
}
@ -974,7 +983,7 @@ export class QuickCaptureModal extends Modal {
this.plugin.settings.preferMetadataFormat
] || "project";
metadata.push(
`[${projectPrefix}:: ${this.taskMetadata.project}]`
`[${projectPrefix}:: ${this.taskMetadata.project}]`,
);
} else {
const projectPrefix =
@ -993,7 +1002,7 @@ export class QuickCaptureModal extends Modal {
this.plugin.settings.preferMetadataFormat
] || "context";
metadata.push(
`[${contextPrefix}:: ${this.taskMetadata.context}]`
`[${contextPrefix}:: ${this.taskMetadata.context}]`,
);
} else {
const contextPrefix =
@ -1009,7 +1018,7 @@ export class QuickCaptureModal extends Modal {
metadata.push(
useDataviewFormat
? `[repeat:: ${this.taskMetadata.recurrence}]`
: `🔁 ${this.taskMetadata.recurrence}`
: `🔁 ${this.taskMetadata.recurrence}`,
);
}
@ -1019,7 +1028,7 @@ export class QuickCaptureModal extends Modal {
formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
2,
"0"
"0",
)}-${String(date.getDate()).padStart(2, "0")}`;
}
@ -1028,6 +1037,25 @@ export class QuickCaptureModal extends Modal {
return new Date(year, month - 1, day); // month is 0-indexed in JavaScript Date
}
private normalizeIncomingDate(
value?: Date | string | number | null,
): Date | undefined {
if (value === null || value === undefined) return undefined;
if (value instanceof Date) return value;
if (typeof value === "number") {
const fromNumber = new Date(value);
return isNaN(fromNumber.getTime()) ? undefined : fromNumber;
}
if (typeof value === "string") {
// Try strict ISO/known formats first, then fall back to YYYY-MM-DD parser
const parsed = moment(value, moment.ISO_8601, true);
if (parsed.isValid()) return parsed.toDate();
const fallback = this.parseDate(value);
return isNaN(fallback.getTime()) ? undefined : fallback;
}
return undefined;
}
/**
* Check if a metadata field was manually set by the user
* @param field - The field name to check
@ -1132,7 +1160,7 @@ export class QuickCaptureModal extends Modal {
// Update UI input field
if (this.scheduledDateInput) {
this.scheduledDateInput.value = this.formatDate(
aggregatedScheduledDate
aggregatedScheduledDate,
);
}
}

View file

@ -0,0 +1,594 @@
/**
* TaskGenius Calendar - Obsidian Theme Adaptation
*
* This stylesheet overrides the default styles from @taskgenius/calendar
* to seamlessly integrate with Obsidian's theme system (light and dark modes).
*/
/* ============================================
Base Calendar Styles - Obsidian Integration
============================================ */
.tg-calendar {
/* Use Obsidian's theme colors */
--tg-primary-color: var(--interactive-accent);
--tg-primary-rgb: var(--interactive-accent-rgb, 59, 130, 246);
--tg-cell-height: 60px;
--tg-font-header: var(--font-ui-small);
--tg-font-event: var(--font-ui-smaller);
/* Typography */
font-family: var(--font-interface);
-webkit-font-smoothing: antialiased;
user-select: none;
/* Layout */
overflow-x: hidden;
width: 100%;
border-radius: 0;
box-shadow: none;
}
/* ============================================
View Container
============================================ */
.tg-view-container {
min-height: 600px;
background: var(--background-primary);
color: var(--text-normal);
}
/* ============================================
Header Styles
============================================ */
/* Hide the library's built-in header since we use our own custom calendar-header */
.tg-header {
display: none !important;
}
.tg-title {
font-size: 1.25rem;
font-weight: bold;
color: var(--text-normal);
}
/* Navigation buttons */
.tg-nav {
display: flex;
gap: 8px;
}
.tg-nav-btn {
padding: 4px 12px;
font-size: 14px;
border-radius: 4px;
border: 1px solid var(--background-modifier-border);
background: var(--background-secondary);
color: var(--text-normal);
cursor: pointer;
transition: all 0.2s;
}
.tg-nav-btn:hover {
background: var(--background-modifier-hover);
border-color: var(--background-modifier-border-hover);
}
.tg-nav-btn.tg-today {
background: var(--interactive-accent);
color: var(--text-on-accent);
border-color: var(--interactive-accent);
}
.tg-nav-btn.tg-today:hover {
opacity: 0.9;
}
/* View switcher */
.tg-view-switch {
display: flex;
background: var(--background-secondary);
padding: 4px;
border-radius: 8px;
gap: 4px;
}
.tg-view-btn {
padding: 4px 12px;
font-size: 14px;
border-radius: 6px;
border: none;
background: transparent;
cursor: pointer;
transition: all 0.2s;
color: var(--text-muted);
}
.tg-view-btn:hover {
color: var(--text-normal);
background: var(--background-modifier-hover);
}
.tg-view-btn.tg-active {
background: var(--background-primary);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
color: var(--interactive-accent);
font-weight: 500;
}
/* ============================================
Month View Styles
============================================ */
.tg-month-row {
position: relative;
height: 120px;
border-bottom: 1px solid var(--background-modifier-border);
}
.tg-month-cell {
height: 100%;
border-right: 1px solid var(--background-modifier-border);
position: relative;
z-index: 1;
background: var(--background-primary);
}
.tg-month-cell:hover {
background: var(--background-primary-alt);
}
.tg-month-cell:last-child {
border-right: none;
}
/* Month view header */
.tg-month-header {
border-bottom: 1px solid var(--background-modifier-border);
background: var(--background-secondary);
}
.tg-month-header-cell {
padding: 8px;
text-align: center;
font-size: 12px;
font-weight: bold;
color: var(--text-muted);
}
/* Date number in month cell */
.tg-date-number {
text-align: right;
font-size: 12px;
padding: 4px;
color: var(--text-normal);
}
.tg-date-number.tg-current-month {
color: var(--text-normal);
}
.tg-date-number.tg-other-month {
color: var(--text-faint);
}
.tg-date-number.tg-today {
color: var(--interactive-accent);
font-weight: bold;
background: var(--background-modifier-hover);
border-radius: 50%;
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.tg-date-number.past-due {
color: var(--text-error);
}
/* ============================================
Time View Styles (Week/Day)
============================================ */
.tg-time-grid-container {
display: flex;
flex-direction: column;
height: 600px;
overflow-y: auto;
position: relative;
scroll-behavior: auto;
}
.tg-time-header {
display: flex;
border-bottom: 1px solid var(--background-modifier-border);
position: sticky;
top: 0;
background: var(--background-primary);
z-index: 30;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
padding-right: var(--size-4-3);
}
.tg-time-header-cell {
flex: 1;
text-align: center;
padding: 8px;
border-right: 1px solid var(--background-modifier-border);
font-size: var(--tg-font-header);
color: var(--text-normal);
}
.tg-time-body {
display: flex;
position: relative;
min-height: calc(var(--tg-cell-height) * 24);
}
/* Time axis */
.tg-time-axis {
width: 60px;
flex-shrink: 0;
background: var(--background-secondary);
border-right: 1px solid var(--background-modifier-border);
position: sticky;
left: 0;
z-index: 20;
}
.tg-time-axis-label {
height: var(--tg-cell-height);
position: absolute;
width: 100%;
text-align: right;
padding-right: 8px;
color: var(--text-muted);
font-size: 11px;
top: -6px;
pointer-events: none;
}
/* Day columns */
.tg-day-column {
flex: 1;
position: relative;
border-right: 1px solid var(--background-modifier-border);
background: repeating-linear-gradient(
to bottom,
transparent 0,
transparent calc(var(--tg-cell-height) - 1px),
var(--background-modifier-border) var(--tg-cell-height)
);
}
/* ============================================
Event Styles
============================================ */
.tg-event-base {
position: absolute;
border-radius: 4px;
padding: 2px 6px;
font-size: var(--tg-font-event);
color: white;
cursor: grab;
z-index: 10;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
transition:
opacity 0.2s,
transform 0.1s;
}
.tg-event-base:active {
cursor: grabbing;
}
.tg-event-base:hover {
opacity: 0.9;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.tg-event-base.tg-is-dragging-source {
opacity: 0.3;
filter: grayscale(0.3);
}
/* Month view event bar */
.tg-event-bar {
height: 26px;
line-height: 26px;
}
/* Time view event block */
.tg-event-block {
display: flex;
flex-direction: column;
justify-content: flex-start;
line-height: 1.3;
padding-top: 4px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.tg-event-block .tg-time-text {
font-size: 10px;
opacity: 0.9;
margin-bottom: 0;
}
.tg-event-title {
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
}
/* ============================================
Task Count Badge
============================================ */
.tg-event-count-badge {
display: none;
}
.tg-event-count-badge.has-priority {
background: var(--color-red);
}
/* ============================================
Drag & Drop Styles
============================================ */
/* Resize handles */
.tg-resize-handle {
position: absolute;
z-index: 20;
opacity: 0;
transition: opacity 0.2s;
}
.tg-event-base:hover .tg-resize-handle {
opacity: 1;
}
.tg-resize-handle:hover {
background-color: rgba(255, 255, 255, 0.4);
}
/* Horizontal resize handles (month view) */
.tg-resize-h {
top: 0;
bottom: 0;
width: 12px;
cursor: col-resize;
}
.tg-resize-h.tg-left {
left: 0;
}
.tg-resize-h.tg-right {
right: 0;
}
/* Vertical resize handle (time view) */
.tg-resize-v {
bottom: 0;
left: 0;
right: 0;
height: 8px;
cursor: row-resize;
}
/* Ghost element for drag preview */
.tg-ghost-event {
position: absolute;
background-color: rgba(var(--tg-primary-rgb), 0.15);
border: 2px dashed rgba(var(--tg-primary-rgb), 0.8);
border-radius: 4px;
z-index: 5;
pointer-events: none;
box-sizing: border-box;
}
/* Drag proxy */
#tg-drag-proxy {
position: fixed;
pointer-events: none;
z-index: 9999;
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
border-radius: 4px;
visibility: hidden;
opacity: 0.9;
transform-origin: top left;
}
/* ============================================
Grid Layout Helpers
============================================ */
.tg-grid-7 {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
}
/* ============================================
Dark Theme Specific Adjustments
============================================ */
.theme-dark .tg-view-container {
background: var(--background-primary);
}
.theme-dark .tg-month-cell {
background: var(--background-primary);
}
.theme-dark .tg-month-cell:hover {
background: var(--background-primary-alt);
}
.theme-dark .tg-time-header {
background: var(--background-primary);
}
.theme-dark .tg-event-base {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.theme-dark .tg-ghost-event {
background-color: rgba(var(--tg-primary-rgb), 0.2);
border-color: rgba(var(--tg-primary-rgb), 0.6);
}
/* ============================================
Light Theme Specific Adjustments
============================================ */
.theme-light .tg-view-container {
background: var(--background-primary);
}
.theme-light .tg-month-cell {
background: var(--background-primary);
}
.theme-light .tg-event-base {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* ============================================
Scrollbar Styling (Obsidian-like)
============================================ */
.tg-time-grid-container::-webkit-scrollbar {
width: 8px;
}
.tg-time-grid-container::-webkit-scrollbar-track {
background: var(--background-secondary);
}
.tg-time-grid-container::-webkit-scrollbar-thumb {
background: var(--background-modifier-border);
border-radius: 4px;
}
.tg-time-grid-container::-webkit-scrollbar-thumb:hover {
background: var(--background-modifier-border-hover);
}
/* ============================================
Responsive Adjustments
============================================ */
@media (max-width: 768px) {
.tg-calendar {
--tg-cell-height: 50px;
--tg-font-header: 12px;
--tg-font-event: 11px;
}
.tg-time-axis {
width: 50px;
}
.tg-view-switch {
flex-wrap: wrap;
}
}
/* ============================================
Accessibility
============================================ */
.tg-event-base:focus {
outline: 2px solid var(--interactive-accent);
outline-offset: 2px;
}
.tg-nav-btn:focus,
.tg-view-btn:focus {
outline: 2px solid var(--interactive-accent);
outline-offset: 1px;
}
/* ============================================
Animation & Transitions
============================================ */
.tg-month-cell,
.tg-event-base,
.tg-nav-btn,
.tg-view-btn {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Smooth entry animation for events */
@keyframes tg-event-enter {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
/*.tg-event-base {
animation: tg-event-enter 0.2s ease-out;
}*/
/* ============================================
Task Checkbox Overlay
============================================ */
/* Container for checkbox in TGCalendar events */
.tg-event .task-checkbox-overlay {
position: absolute;
left: 2px;
top: 50%;
transform: translateY(-50%);
z-index: 15;
background: rgba(var(--background-primary-rgb, 255, 255, 255), 0.9);
border-radius: 3px;
padding: 0 2px;
display: flex;
align-items: center;
opacity: 0;
transition: opacity 0.2s;
}
/* Show checkbox on hover */
.tg-event:hover .task-checkbox-overlay {
opacity: 1;
}
/* Checkbox styling */
.task-checkbox-overlay .task-list-item-checkbox {
width: 16px;
height: 16px;
margin: 0;
cursor: pointer;
z-index: 20;
}
/* Ensure event title doesn't overlap with checkbox */
.tg-event:has(.task-checkbox-overlay) .tg-event-title {
padding-left: 22px;
}
/* Month view - always show checkbox for better UX */
.tg-month-view .tg-event .task-checkbox-overlay {
opacity: 1;
}

View file

@ -0,0 +1,309 @@
/**
* DateFnsAdapter - Adapter for @taskgenius/calendar using date-fns
*
* This adapter bridges date-fns with @taskgenius/calendar's DateAdapter interface,
* allowing the calendar to use date-fns for all date operations.
*/
import {
parse,
format,
startOfDay,
startOfWeek,
startOfMonth,
startOfYear,
endOfDay,
endOfWeek,
endOfMonth,
endOfYear,
addDays,
addWeeks,
addMonths,
addYears,
subDays,
subWeeks,
subMonths,
subYears,
differenceInDays,
differenceInWeeks,
differenceInMonths,
differenceInYears,
isSameDay,
isSameWeek,
isSameMonth,
isSameYear,
isBefore,
isAfter,
getDay,
getDaysInMonth,
isValid,
parseISO,
} from "date-fns";
/**
* Time unit types supported by the adapter
*/
type TimeUnit = "day" | "week" | "month" | "year" | "hour" | "minute";
/**
* DateAdapter interface that must be implemented
* This is the contract expected by @taskgenius/calendar
*/
interface DateAdapter<T> {
create(date?: string | Date | T): T;
parse(dateStr: string, format?: string): T;
format(date: T, format: string): string;
startOf(date: T, unit: TimeUnit): T;
endOf(date: T, unit: TimeUnit): T;
add(date: T, amount: number, unit: TimeUnit): T;
subtract(date: T, amount: number, unit: TimeUnit): T;
diff(date1: T, date2: T, unit: TimeUnit): number;
isSame(date1: T, date2: T, unit?: TimeUnit): boolean;
isBefore(date: T, compare: T): boolean;
isAfter(date: T, compare: T): boolean;
getDay(date: T): number;
getDaysInMonth(date: T): number;
}
/**
* DateFnsAdapter implementation using date-fns library
*/
export class DateFnsAdapter implements DateAdapter<Date> {
/**
* Create a Date object from various input types
*/
create(date?: string | Date): Date {
if (!date) {
return new Date();
}
if (date instanceof Date) {
return new Date(date);
}
if (typeof date === "string") {
// Try to parse ISO format first
const parsed = parseISO(date);
if (isValid(parsed)) {
return parsed;
}
// Fallback to native Date constructor
return new Date(date);
}
return new Date();
}
/**
* Parse a date string with optional format
*/
parse(dateStr: string, formatStr?: string): Date {
if (!formatStr) {
// Default to ISO format
const parsed = parseISO(dateStr);
if (isValid(parsed)) {
return parsed;
}
return new Date(dateStr);
}
// Use date-fns parse with format
const parsed = parse(dateStr, formatStr, new Date());
if (!isValid(parsed)) {
throw new Error(`Invalid date string: ${dateStr}`);
}
return parsed;
}
/**
* Format a date to string
*/
format(date: Date, formatStr: string): string {
return format(date, formatStr);
}
/**
* Get the start of a time unit
*/
startOf(date: Date, unit: TimeUnit): Date {
switch (unit) {
case "day":
return startOfDay(date);
case "week":
return startOfWeek(date);
case "month":
return startOfMonth(date);
case "year":
return startOfYear(date);
case "hour":
// date-fns doesn't have startOfHour, so we set minutes and seconds to 0
const hourStart = new Date(date);
hourStart.setMinutes(0, 0, 0);
return hourStart;
case "minute":
// Set seconds and milliseconds to 0
const minuteStart = new Date(date);
minuteStart.setSeconds(0, 0);
return minuteStart;
default:
return startOfDay(date);
}
}
/**
* Get the end of a time unit
*/
endOf(date: Date, unit: TimeUnit): Date {
switch (unit) {
case "day":
return endOfDay(date);
case "week":
return endOfWeek(date);
case "month":
return endOfMonth(date);
case "year":
return endOfYear(date);
case "hour":
// Set to last millisecond of the hour
const hourEnd = new Date(date);
hourEnd.setMinutes(59, 59, 999);
return hourEnd;
case "minute":
// Set to last millisecond of the minute
const minuteEnd = new Date(date);
minuteEnd.setSeconds(59, 999);
return minuteEnd;
default:
return endOfDay(date);
}
}
/**
* Add time to a date
*/
add(date: Date, amount: number, unit: TimeUnit): Date {
switch (unit) {
case "day":
return addDays(date, amount);
case "week":
return addWeeks(date, amount);
case "month":
return addMonths(date, amount);
case "year":
return addYears(date, amount);
case "hour":
return new Date(date.getTime() + amount * 60 * 60 * 1000);
case "minute":
return new Date(date.getTime() + amount * 60 * 1000);
default:
return addDays(date, amount);
}
}
/**
* Subtract time from a date
*/
subtract(date: Date, amount: number, unit: TimeUnit): Date {
switch (unit) {
case "day":
return subDays(date, amount);
case "week":
return subWeeks(date, amount);
case "month":
return subMonths(date, amount);
case "year":
return subYears(date, amount);
case "hour":
return new Date(date.getTime() - amount * 60 * 60 * 1000);
case "minute":
return new Date(date.getTime() - amount * 60 * 1000);
default:
return subDays(date, amount);
}
}
/**
* Calculate difference between two dates
*/
diff(date1: Date, date2: Date, unit: TimeUnit): number {
switch (unit) {
case "day":
return differenceInDays(date1, date2);
case "week":
return differenceInWeeks(date1, date2);
case "month":
return differenceInMonths(date1, date2);
case "year":
return differenceInYears(date1, date2);
case "hour":
return Math.floor((date1.getTime() - date2.getTime()) / (60 * 60 * 1000));
case "minute":
return Math.floor((date1.getTime() - date2.getTime()) / (60 * 1000));
default:
return differenceInDays(date1, date2);
}
}
/**
* Check if two dates are the same
*/
isSame(date1: Date, date2: Date, unit?: TimeUnit): boolean {
if (!unit) {
return date1.getTime() === date2.getTime();
}
switch (unit) {
case "day":
return isSameDay(date1, date2);
case "week":
return isSameWeek(date1, date2);
case "month":
return isSameMonth(date1, date2);
case "year":
return isSameYear(date1, date2);
case "hour":
return (
isSameDay(date1, date2) &&
date1.getHours() === date2.getHours()
);
case "minute":
return (
isSameDay(date1, date2) &&
date1.getHours() === date2.getHours() &&
date1.getMinutes() === date2.getMinutes()
);
default:
return isSameDay(date1, date2);
}
}
/**
* Check if date is before compare date
*/
isBefore(date: Date, compare: Date): boolean {
return isBefore(date, compare);
}
/**
* Check if date is after compare date
*/
isAfter(date: Date, compare: Date): boolean {
return isAfter(date, compare);
}
/**
* Get day of week (0 = Sunday, 6 = Saturday)
*/
getDay(date: Date): number {
return getDay(date);
}
/**
* Get number of days in the month
*/
getDaysInMonth(date: Date): number {
return getDaysInMonth(date);
}
}
/**
* Export a singleton instance for convenience
*/
export const dateFnsAdapter = new DateFnsAdapter();

File diff suppressed because one or more lines are too long