mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
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:
parent
cee8349ba9
commit
8ffd5cee0e
7 changed files with 2792 additions and 786 deletions
|
|
@ -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
|
|
@ -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")}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
594
src/styles/taskgenius-calendar.css
Normal file
594
src/styles/taskgenius-calendar.css
Normal 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;
|
||||
}
|
||||
309
src/utils/adapters/DateFnsAdapter.ts
Normal file
309
src/utils/adapters/DateFnsAdapter.ts
Normal 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();
|
||||
790
styles.css
790
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue