diff --git a/backlog/tasks/task-122 - Canvas-element-reminder-scheduling-UX-enhancements.md b/backlog/tasks/task-122 - Canvas-element-reminder-scheduling-UX-enhancements.md
new file mode 100644
index 0000000..4679af9
--- /dev/null
+++ b/backlog/tasks/task-122 - Canvas-element-reminder-scheduling-UX-enhancements.md
@@ -0,0 +1,41 @@
+---
+id: TASK-122
+title: Canvas element reminder scheduling UX enhancements
+status: Done
+assignee: []
+created_date: '2026-03-17 01:01'
+labels:
+ - canvas
+ - rschedule
+ - UX
+dependencies: []
+references:
+ - website/canvas.html
+priority: medium
+---
+
+## Description
+
+
+Add multiple UX affordances for scheduling reminders on canvas shapes: floating calendar icon on selected shapes, right-click context menu option, drag-to-calendar compact mode, and email notifications on reminder creation.
+
+
+## Acceptance Criteria
+
+- [ ] #1 Floating 📅 icon appears near top-right of selected shape
+- [ ] #2 Clicking calendar icon toggles the reminder widget
+- [ ] #3 Right-click context menu shows 'Schedule a reminder' option
+- [ ] #4 Context menu option opens reminder widget for the target shape
+- [ ] #5 Dragging a shape for 200ms+ shows compact calendar in bottom-right
+- [ ] #6 Hovering over calendar days during drag highlights them
+- [ ] #7 Releasing shape over a highlighted day creates the reminder
+- [ ] #8 Reminder API call includes notifyEmail when user email is available
+- [ ] #9 Email is fetched from EncryptID and cached for session
+- [ ] #10 Feedback message indicates email notification when applicable
+
+
+## Final Summary
+
+
+Implemented 4 reminder scheduling UX enhancements in `website/canvas.html` (156 insertions):\n\n1. **Right-click context menu** — \"📅 Schedule a reminder\" option in shape context menu opens reminder widget\n2. **Email notification** — Fetches user email from EncryptID `/auth/api/account/security`, caches it, passes `notifyEmail` to rSchedule API, shows confirmation in feedback\n3. **Floating calendar icon** — 28px circular 📅 button positioned at selected shape's top-right corner, repositions on scroll/zoom, toggles widget on click\n4. **Drag-to-calendar** — Compact calendar appears after 200ms of shape drag, day cells highlight on hover, releasing over a day creates the reminder
+
diff --git a/website/canvas.html b/website/canvas.html
index c971039..ccf5967 100644
--- a/website/canvas.html
+++ b/website/canvas.html
@@ -2556,6 +2556,31 @@
text-align: center; font-size: 11px; color: #22c55e; font-weight: 600;
margin-top: 6px; min-height: 16px;
}
+ .shape-schedule-icon {
+ position: fixed; z-index: 9999; width: 28px; height: 28px;
+ border-radius: 50%; border: 1px solid #444;
+ background: var(--rs-bg-surface, #1e1e2e); color: #e0e0e0;
+ font-size: 14px; cursor: pointer; display: flex;
+ align-items: center; justify-content: center;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
+ opacity: 0; transition: opacity 0.15s; pointer-events: none;
+ padding: 0; line-height: 1;
+ }
+ .shape-schedule-icon.visible {
+ opacity: 1; pointer-events: auto;
+ }
+ .shape-schedule-icon:hover {
+ background: #2a2a3e; border-color: #818cf8;
+ }
+ #reminder-widget.rw-compact {
+ width: 200px; padding: 10px; top: auto; bottom: 16px; right: 16px;
+ border-color: #f59e0b;
+ }
+ #reminder-widget.rw-compact .rw-header,
+ #reminder-widget.rw-compact .rw-shape-label { font-size: 10px; margin-bottom: 4px; }
+ #reminder-widget.rw-compact .rw-day { font-size: 9px; }
+ #reminder-widget.rw-compact .rw-nav-btn { font-size: 11px; }
+ #reminder-widget.rw-compact .rw-month { font-size: 10px; }
@@ -3262,6 +3287,39 @@
}
__miCanvasBridge.setSelection([...selectedShapeIds]);
updateReminderWidget();
+ updateScheduleIcon();
+ }
+
+ // ── Floating schedule icon on selected shape ──
+ let scheduleIconEl = null;
+ function updateScheduleIcon() {
+ if (selectedShapeIds.size === 1) {
+ const id = [...selectedShapeIds][0];
+ const el = document.getElementById(id);
+ if (el) {
+ if (!scheduleIconEl) {
+ scheduleIconEl = document.createElement("button");
+ scheduleIconEl.className = "shape-schedule-icon";
+ scheduleIconEl.textContent = "📅";
+ scheduleIconEl.title = "Schedule a reminder";
+ scheduleIconEl.addEventListener("click", (ev) => {
+ ev.stopPropagation();
+ if (rwWidget.classList.contains("visible")) {
+ rwWidget.classList.remove("visible");
+ } else {
+ updateReminderWidget();
+ }
+ });
+ document.body.appendChild(scheduleIconEl);
+ }
+ const rect = el.getBoundingClientRect();
+ scheduleIconEl.style.left = (rect.right + 4) + "px";
+ scheduleIconEl.style.top = (rect.top - 4) + "px";
+ scheduleIconEl.classList.add("visible");
+ return;
+ }
+ }
+ if (scheduleIconEl) scheduleIconEl.classList.remove("visible");
}
function rectsOverlapScreen(sel, r) {
@@ -3746,6 +3804,10 @@
// Track position for group dragging
shape.addEventListener("pointerdown", () => {
shapeLastPos.set(shape.id, { x: shape.x, y: shape.y });
+ onShapeMoveStart(shape);
+ }, { capture: true });
+ shape.addEventListener("pointerup", () => {
+ onShapeMoveEnd();
}, { capture: true });
// Transform events (move, resize, rotate)
@@ -5446,6 +5508,7 @@
html += ``;
html += `
`;
}
+ html += ``;
} else if (state === 'forgotten') {
html += ``;
if (!alreadyForgotten) {
@@ -5497,7 +5560,24 @@
return;
}
- // Group actions
+
+ if (action === 'schedule-reminder') {
+ const targetId = contextTargetIds[0];
+ const el = document.getElementById(targetId);
+ if (el) {
+ rwSelectedShape = el;
+ const info = getShapeInfo(el);
+ rwLabel.innerHTML = `${info.typeName} ${info.label}`;
+ rwWidget.classList.add('visible');
+ rwFeedback.textContent = '';
+ renderRwCalendar();
+ }
+ shapeContextMenu.classList.remove('open');
+ contextTargetIds = [];
+ return;
+ }
+
+// Group actions
if (action === 'group-create') {
const name = prompt("Group name:", "New Group") || "New Group";
groupManager.createGroup(name, contextTargetIds);
@@ -5957,6 +6037,7 @@
}
// Update remote cursors to match new camera position
presence.setCamera(panX, panY, scale);
+ updateScheduleIcon();
}
// Re-render canvas background when user changes preference
@@ -6859,6 +6940,7 @@
let rwDate = new Date();
let rwSelectedShape = null;
let rwDragShape = null;
+ let cachedUserEmail = null;
const TYPE_COLORS = {
"folk-markdown": "#6366f1", "folk-rapp": "#818cf8", "folk-embed": "#06b6d4",
@@ -6994,25 +7076,39 @@
const remindAt = new Date(dateStr + "T09:00:00").getTime();
const schedBase = `/${communitySlug}/rschedule`;
+ // Fetch user email for notification (cached after first call)
+ if (cachedUserEmail === null) {
+ try {
+ const sess = JSON.parse(localStorage.getItem('encryptid_session') || '{}');
+ if (sess?.accessToken) {
+ const r = await fetch('/auth/api/account/security', { headers: { Authorization: 'Bearer ' + sess.accessToken } });
+ if (r.ok) { const d = await r.json(); cachedUserEmail = d.emailAddress || false; }
+ else cachedUserEmail = false;
+ } else cachedUserEmail = false;
+ } catch { cachedUserEmail = false; }
+ }
+
try {
+ const body = {
+ title: info.label,
+ remindAt,
+ allDay: true,
+ syncToCalendar: true,
+ sourceModule: info.moduleId || info.tag,
+ sourceEntityId: info.id,
+ sourceLabel: info.typeName,
+ sourceColor: info.color,
+ };
+ if (cachedUserEmail) body.notifyEmail = cachedUserEmail;
const res = await fetch(`${schedBase}/api/reminders`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- title: info.label,
- remindAt,
- allDay: true,
- syncToCalendar: true,
- sourceModule: info.moduleId || info.tag,
- sourceEntityId: info.id,
- sourceLabel: info.typeName,
- sourceColor: info.color,
- }),
+ body: JSON.stringify(body),
});
if (res.ok) {
const friendlyDate = new Date(dateStr + "T12:00:00").toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
- rwFeedback.textContent = `✓ Reminder set for ${friendlyDate}`;
+ rwFeedback.textContent = cachedUserEmail ? `✓ Reminder set for ${friendlyDate} — email reminder will be sent` : `✓ Reminder set for ${friendlyDate}`;
// Flash the day cell
const dayEl = rwGrid.querySelector(`[data-rw-date="${dateStr}"]`);
if (dayEl) {
@@ -7033,6 +7129,54 @@
}
}
+ // ── Drag-to-calendar: show compact calendar when shape is being moved ──
+ let dragCalendarActive = false;
+ let dragCalendarShape = null;
+ let dragMoveTimer = null;
+ let lastPointerPos = { x: 0, y: 0 };
+
+ // Track pointer globally for drop detection
+ document.addEventListener("pointermove", (e) => { lastPointerPos.x = e.clientX; lastPointerPos.y = e.clientY; }, { passive: true });
+
+ function onShapeMoveStart(shape) {
+ if (dragMoveTimer) clearTimeout(dragMoveTimer);
+ dragCalendarShape = shape;
+ dragMoveTimer = setTimeout(() => {
+ if (!dragCalendarShape) return;
+ dragCalendarActive = true;
+ rwSelectedShape = dragCalendarShape;
+ const info = getShapeInfo(dragCalendarShape);
+ rwLabel.innerHTML = `${info.typeName} ${info.label}`;
+ rwWidget.classList.add("visible", "rw-compact");
+ rwFeedback.textContent = "Drop on a day to set reminder";
+ renderRwCalendar();
+ }, 200);
+ }
+
+ function onShapeMoveEnd() {
+ if (dragMoveTimer) { clearTimeout(dragMoveTimer); dragMoveTimer = null; }
+ if (!dragCalendarActive) { dragCalendarShape = null; return; }
+ // Check if pointer is over a calendar day
+ const hitEl = document.elementFromPoint(lastPointerPos.x, lastPointerPos.y);
+ const dayEl = hitEl?.closest?.(".rw-day[data-rw-date]");
+ if (dayEl && rwSelectedShape) {
+ createReminderForShape(dayEl.dataset.rwDate);
+ }
+ rwWidget.classList.remove("rw-compact");
+ if (!dayEl) rwWidget.classList.remove("visible");
+ dragCalendarActive = false;
+ dragCalendarShape = null;
+ }
+
+ // Highlight calendar days on hover during drag
+ setInterval(() => {
+ if (!dragCalendarActive) return;
+ const hitEl = document.elementFromPoint(lastPointerPos.x, lastPointerPos.y);
+ rwGrid.querySelectorAll(".rw-day").forEach(d => d.classList.remove("drop-highlight"));
+ const dayEl = hitEl?.closest?.(".rw-day[data-rw-date]");
+ if (dayEl) dayEl.classList.add("drop-highlight");
+ }, 60);
+
rwPrev.addEventListener("click", () => {
rwDate = new Date(rwDate.getFullYear(), rwDate.getMonth() - 1, 1);
renderRwCalendar();