Codex commited on
Commit
4d1cba1
·
1 Parent(s): b132f4e

Fix planner resize anchoring and alignment

Browse files
Files changed (2) hide show
  1. static/planner.js +151 -37
  2. static/v020.css +85 -6
static/planner.js CHANGED
@@ -7,6 +7,7 @@
7
  activePage: 0,
8
  dragTaskId: null,
9
  interaction: null,
 
10
  };
11
 
12
  const PIXELS_PER_MINUTE = 1.16;
@@ -15,9 +16,12 @@
15
  const CANVAS_GAP = 18;
16
  const SNAP_MINUTES = 5;
17
  const MIN_DURATION = 15;
 
18
  const AXIS_LABEL_MIN_GAP = 28;
 
19
 
20
  const pageTrack = document.getElementById("pageTrack");
 
21
  const plannerDateInput = document.getElementById("plannerDateInput");
22
  const plannerPrevDay = document.getElementById("plannerPrevDay");
23
  const plannerNextDay = document.getElementById("plannerNextDay");
@@ -185,12 +189,55 @@
185
  if (task && task.schedule) {
186
  return Math.max(MIN_DURATION, toMinutes(task.schedule.end_time) - toMinutes(task.schedule.start_time));
187
  }
188
- return Number((state.planner.settings && state.planner.settings.default_task_duration_minutes) || 45);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  }
190
 
191
  function setActivePage(index) {
192
  state.activePage = clamp(index, 0, 1);
193
  pageTrack.style.transform = `translateX(-${state.activePage * 100}%)`;
 
 
 
194
  document.querySelectorAll(".story-tab").forEach((tab) => {
195
  tab.classList.toggle("is-active", Number(tab.dataset.goPage) === state.activePage);
196
  });
@@ -361,8 +408,8 @@
361
 
362
  const band = document.createElement("div");
363
  band.className = "timeline-slot-band";
364
- band.style.top = `${(slotStart - dayStart) * PIXELS_PER_MINUTE}px`;
365
- band.style.height = `${Math.max((slotEnd - slotStart) * PIXELS_PER_MINUTE, 52)}px`;
366
  band.innerHTML = `
367
  <strong>${escapeHtml(slot.label)}</strong>
368
  <span>${escapeHtml(slot.start)} - ${escapeHtml(slot.end)}</span>
@@ -378,16 +425,16 @@
378
  Array.from(lineMarkers).sort((left, right) => left - right).forEach((minute) => {
379
  const line = document.createElement("div");
380
  line.className = `timeline-line ${minute % 60 === 0 ? "is-hour" : "is-slot"}`;
381
- line.style.top = `${(minute - dayStart) * PIXELS_PER_MINUTE}px`;
382
  canvasLayer.appendChild(line);
383
  });
384
 
385
  const axisLabelMinutes = [];
386
  const appendAxisLabel = (minute) => {
387
- const top = (minute - dayStart) * PIXELS_PER_MINUTE;
388
  const previous = axisLabelMinutes[axisLabelMinutes.length - 1];
389
  if (previous !== undefined) {
390
- const previousTop = (previous - dayStart) * PIXELS_PER_MINUTE;
391
  if (top - previousTop < AXIS_LABEL_MIN_GAP) {
392
  return;
393
  }
@@ -406,7 +453,7 @@
406
  axisLabelMinutes.forEach((minute) => {
407
  const tick = document.createElement("div");
408
  tick.className = "timeline-axis-tick";
409
- tick.style.top = `${(minute - dayStart) * PIXELS_PER_MINUTE}px`;
410
  tick.textContent = minutesToTime(minute);
411
  axisLayer.appendChild(tick);
412
  });
@@ -423,8 +470,8 @@
423
  const endMinutes = toMinutes(block.end);
424
  const overlay = document.createElement("div");
425
  overlay.className = "timeline-major-block";
426
- overlay.style.top = `${(startMinutes - dayStart) * PIXELS_PER_MINUTE}px`;
427
- overlay.style.height = `${Math.max((endMinutes - startMinutes) * PIXELS_PER_MINUTE, 88)}px`;
428
  overlay.innerHTML = `<span>${escapeHtml(block.label)}</span>`;
429
  canvasLayer.appendChild(overlay);
430
  });
@@ -434,10 +481,9 @@
434
  const leftPercent = widthPercent * (item.column || 0);
435
  const block = document.createElement("article");
436
  block.className = `planner-event ${item.kind === "course" ? "course-event" : "task-event"} ${item.completed ? "is-complete" : ""}`;
437
- block.style.top = `${(item.startMinutes - dayStart) * PIXELS_PER_MINUTE}px`;
438
- block.style.height = `${Math.max((item.endMinutes - item.startMinutes) * PIXELS_PER_MINUTE, 54)}px`;
439
  block.style.left = `${leftPercent}%`;
440
  block.style.width = `calc(${widthPercent}% - 8px)`;
 
441
 
442
  if (item.kind === "course") {
443
  if (item.color) {
@@ -476,27 +522,37 @@
476
  return;
477
  }
478
 
 
 
 
479
  const mode = event.target.closest("[data-resize-task-start]")
480
  ? "resize-start"
481
  : event.target.closest("[data-resize-task-end]")
482
  ? "resize-end"
483
  : "move";
484
 
485
- const rect = getCanvasRect();
486
- if (!rect) {
487
- return;
488
- }
489
-
490
  state.interaction = {
491
  mode,
492
  block,
493
  taskId: item.task_id,
 
 
 
 
494
  startMinutes: item.startMinutes,
495
  endMinutes: item.endMinutes,
496
  duration: item.endMinutes - item.startMinutes,
497
- offsetY: event.clientY - (rect.top + ((item.startMinutes - dayStart) * PIXELS_PER_MINUTE)),
498
  };
499
 
 
 
 
 
 
 
 
 
500
  block.classList.add("is-dragging");
501
  });
502
  }
@@ -517,8 +573,7 @@
517
  const duration = getTaskDuration(task);
518
  const startMinutes = clamp(clientYToMinutes(event.clientY), dayStart, dayEnd - duration);
519
  preview.style.display = "block";
520
- preview.style.top = `${(startMinutes - dayStart) * PIXELS_PER_MINUTE}px`;
521
- preview.style.height = `${Math.max(duration * PIXELS_PER_MINUTE, 54)}px`;
522
  preview.textContent = `${minutesToTime(startMinutes)} - ${minutesToTime(startMinutes + duration)}`;
523
  });
524
 
@@ -601,6 +656,13 @@
601
  }
602
  }
603
 
 
 
 
 
 
 
 
604
  document.addEventListener("click", (event) => {
605
  const pageButton = event.target.closest("[data-go-page]");
606
  if (pageButton) {
@@ -634,6 +696,7 @@
634
  state.dragTaskId = card.dataset.plannerTaskId;
635
  event.dataTransfer.setData("text/plain", state.dragTaskId);
636
  event.dataTransfer.effectAllowed = "move";
 
637
  card.classList.add("is-dragging");
638
  });
639
 
@@ -643,6 +706,7 @@
643
  card.classList.remove("is-dragging");
644
  }
645
  state.dragTaskId = null;
 
646
  const preview = document.getElementById("timelineDropPreview");
647
  if (preview) {
648
  preview.style.display = "none";
@@ -650,51 +714,71 @@
650
  });
651
 
652
  document.addEventListener("pointermove", (event) => {
653
- if (!state.interaction) {
654
  return;
655
  }
656
 
 
 
657
  const { dayStart, dayEnd } = getPlannerConfig();
 
 
658
  if (state.interaction.mode === "move") {
 
659
  const startMinutes = clamp(
660
- clientYToMinutes(event.clientY - state.interaction.offsetY + 8),
661
  dayStart,
662
- dayEnd - state.interaction.duration
663
  );
664
  state.interaction.startMinutes = startMinutes;
665
- state.interaction.endMinutes = startMinutes + state.interaction.duration;
 
666
  } else if (state.interaction.mode === "resize-start") {
667
  const startMinutes = clamp(
668
- clientYToMinutes(event.clientY),
669
  dayStart,
670
- state.interaction.endMinutes - MIN_DURATION
671
  );
672
  state.interaction.startMinutes = startMinutes;
 
673
  state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
674
  } else {
675
  const endMinutes = clamp(
676
- clientYToMinutes(event.clientY),
677
- state.interaction.startMinutes + MIN_DURATION,
678
  dayEnd
679
  );
 
680
  state.interaction.endMinutes = endMinutes;
681
  state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
682
  }
683
 
684
- state.interaction.block.style.top = `${(state.interaction.startMinutes - dayStart) * PIXELS_PER_MINUTE}px`;
685
- state.interaction.block.style.height = `${Math.max((state.interaction.endMinutes - state.interaction.startMinutes) * PIXELS_PER_MINUTE, 54)}px`;
 
 
 
 
686
  updateEventTimeLabel(state.interaction.block, state.interaction.startMinutes, state.interaction.endMinutes);
687
  });
688
 
689
- document.addEventListener("pointerup", () => {
690
- if (!state.interaction) {
691
- return;
 
 
 
 
 
 
 
 
 
 
692
  }
 
693
 
694
- const current = state.interaction;
695
- current.block.classList.remove("is-dragging");
696
- state.interaction = null;
697
-
698
  requestJSON(`/api/tasks/${current.taskId}/schedule`, {
699
  method: "PATCH",
700
  body: JSON.stringify({
@@ -706,7 +790,36 @@
706
  .then(() => loadPlanner(state.selectedDate, true))
707
  .then(() => showToast("规划时间已更新"))
708
  .catch((error) => showToast(error.message, "error"));
709
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
710
 
711
  plannerDateInput.addEventListener("change", () => {
712
  if (plannerDateInput.value) {
@@ -738,4 +851,5 @@
738
  });
739
 
740
  renderPlanner();
 
741
  })();
 
7
  activePage: 0,
8
  dragTaskId: null,
9
  interaction: null,
10
+ suppressClickUntil: 0,
11
  };
12
 
13
  const PIXELS_PER_MINUTE = 1.16;
 
16
  const CANVAS_GAP = 18;
17
  const SNAP_MINUTES = 5;
18
  const MIN_DURATION = 15;
19
+ const MIN_BLOCK_HEIGHT = MIN_DURATION * PIXELS_PER_MINUTE;
20
  const AXIS_LABEL_MIN_GAP = 28;
21
+ const CLICK_SUPPRESS_MS = 260;
22
 
23
  const pageTrack = document.getElementById("pageTrack");
24
+ const pageSlides = Array.from(document.querySelectorAll(".page-slide"));
25
  const plannerDateInput = document.getElementById("plannerDateInput");
26
  const plannerPrevDay = document.getElementById("plannerPrevDay");
27
  const plannerNextDay = document.getElementById("plannerNextDay");
 
189
  if (task && task.schedule) {
190
  return Math.max(MIN_DURATION, toMinutes(task.schedule.end_time) - toMinutes(task.schedule.start_time));
191
  }
192
+ return Math.max(MIN_DURATION, Number((state.planner.settings && state.planner.settings.default_task_duration_minutes) || 45));
193
+ }
194
+
195
+ function minutesToPixels(minutes) {
196
+ return minutes * PIXELS_PER_MINUTE;
197
+ }
198
+
199
+ function getBlockHeight(startMinutes, endMinutes) {
200
+ return Math.max(minutesToPixels(endMinutes - startMinutes), MIN_BLOCK_HEIGHT);
201
+ }
202
+
203
+ function setBlockBounds(block, startMinutes, endMinutes, dayStart) {
204
+ const height = getBlockHeight(startMinutes, endMinutes);
205
+ block.style.top = `${minutesToPixels(startMinutes - dayStart)}px`;
206
+ block.style.height = `${height}px`;
207
+ return height;
208
+ }
209
+
210
+ function updateEventLayout(block, startMinutes, endMinutes, dayStart) {
211
+ const height = setBlockBounds(block, startMinutes, endMinutes, dayStart);
212
+ block.classList.toggle("is-compact", height < 64);
213
+ block.classList.toggle("is-tight", height < 32);
214
+ return height;
215
+ }
216
+
217
+ function getPointerDeltaMinutes(pointerStartY, clientY) {
218
+ return snapMinutes((clientY - pointerStartY) / PIXELS_PER_MINUTE);
219
+ }
220
+
221
+ function suppressRecentClicks(duration = CLICK_SUPPRESS_MS) {
222
+ state.suppressClickUntil = Date.now() + duration;
223
+ }
224
+
225
+ function beginPlannerInteraction() {
226
+ document.body.classList.add("planner-interacting");
227
+ suppressRecentClicks();
228
+ }
229
+
230
+ function finishPlannerInteraction() {
231
+ document.body.classList.remove("planner-interacting");
232
+ suppressRecentClicks();
233
  }
234
 
235
  function setActivePage(index) {
236
  state.activePage = clamp(index, 0, 1);
237
  pageTrack.style.transform = `translateX(-${state.activePage * 100}%)`;
238
+ pageSlides.forEach((slide, slideIndex) => {
239
+ slide.classList.toggle("is-active", slideIndex === state.activePage);
240
+ });
241
  document.querySelectorAll(".story-tab").forEach((tab) => {
242
  tab.classList.toggle("is-active", Number(tab.dataset.goPage) === state.activePage);
243
  });
 
408
 
409
  const band = document.createElement("div");
410
  band.className = "timeline-slot-band";
411
+ band.style.top = `${minutesToPixels(slotStart - dayStart)}px`;
412
+ band.style.height = `${minutesToPixels(slotEnd - slotStart)}px`;
413
  band.innerHTML = `
414
  <strong>${escapeHtml(slot.label)}</strong>
415
  <span>${escapeHtml(slot.start)} - ${escapeHtml(slot.end)}</span>
 
425
  Array.from(lineMarkers).sort((left, right) => left - right).forEach((minute) => {
426
  const line = document.createElement("div");
427
  line.className = `timeline-line ${minute % 60 === 0 ? "is-hour" : "is-slot"}`;
428
+ line.style.top = `${minutesToPixels(minute - dayStart)}px`;
429
  canvasLayer.appendChild(line);
430
  });
431
 
432
  const axisLabelMinutes = [];
433
  const appendAxisLabel = (minute) => {
434
+ const top = minutesToPixels(minute - dayStart);
435
  const previous = axisLabelMinutes[axisLabelMinutes.length - 1];
436
  if (previous !== undefined) {
437
+ const previousTop = minutesToPixels(previous - dayStart);
438
  if (top - previousTop < AXIS_LABEL_MIN_GAP) {
439
  return;
440
  }
 
453
  axisLabelMinutes.forEach((minute) => {
454
  const tick = document.createElement("div");
455
  tick.className = "timeline-axis-tick";
456
+ tick.style.top = `${minutesToPixels(minute - dayStart)}px`;
457
  tick.textContent = minutesToTime(minute);
458
  axisLayer.appendChild(tick);
459
  });
 
470
  const endMinutes = toMinutes(block.end);
471
  const overlay = document.createElement("div");
472
  overlay.className = "timeline-major-block";
473
+ overlay.style.top = `${minutesToPixels(startMinutes - dayStart)}px`;
474
+ overlay.style.height = `${minutesToPixels(endMinutes - startMinutes)}px`;
475
  overlay.innerHTML = `<span>${escapeHtml(block.label)}</span>`;
476
  canvasLayer.appendChild(overlay);
477
  });
 
481
  const leftPercent = widthPercent * (item.column || 0);
482
  const block = document.createElement("article");
483
  block.className = `planner-event ${item.kind === "course" ? "course-event" : "task-event"} ${item.completed ? "is-complete" : ""}`;
 
 
484
  block.style.left = `${leftPercent}%`;
485
  block.style.width = `calc(${widthPercent}% - 8px)`;
486
+ updateEventLayout(block, item.startMinutes, item.endMinutes, dayStart);
487
 
488
  if (item.kind === "course") {
489
  if (item.color) {
 
522
  return;
523
  }
524
 
525
+ event.preventDefault();
526
+ event.stopPropagation();
527
+
528
  const mode = event.target.closest("[data-resize-task-start]")
529
  ? "resize-start"
530
  : event.target.closest("[data-resize-task-end]")
531
  ? "resize-end"
532
  : "move";
533
 
534
+ beginPlannerInteraction();
 
 
 
 
535
  state.interaction = {
536
  mode,
537
  block,
538
  taskId: item.task_id,
539
+ pointerId: event.pointerId,
540
+ pointerStartY: event.clientY,
541
+ initialStartMinutes: item.startMinutes,
542
+ initialEndMinutes: item.endMinutes,
543
  startMinutes: item.startMinutes,
544
  endMinutes: item.endMinutes,
545
  duration: item.endMinutes - item.startMinutes,
 
546
  };
547
 
548
+ if (typeof block.setPointerCapture === "function") {
549
+ try {
550
+ block.setPointerCapture(event.pointerId);
551
+ } catch (error) {
552
+ // Ignore browsers that reject capture for synthetic pointer sequences.
553
+ }
554
+ }
555
+
556
  block.classList.add("is-dragging");
557
  });
558
  }
 
573
  const duration = getTaskDuration(task);
574
  const startMinutes = clamp(clientYToMinutes(event.clientY), dayStart, dayEnd - duration);
575
  preview.style.display = "block";
576
+ setBlockBounds(preview, startMinutes, startMinutes + duration, dayStart);
 
577
  preview.textContent = `${minutesToTime(startMinutes)} - ${minutesToTime(startMinutes + duration)}`;
578
  });
579
 
 
656
  }
657
  }
658
 
659
+ document.addEventListener("click", (event) => {
660
+ if (Date.now() < state.suppressClickUntil) {
661
+ event.preventDefault();
662
+ event.stopPropagation();
663
+ }
664
+ }, true);
665
+
666
  document.addEventListener("click", (event) => {
667
  const pageButton = event.target.closest("[data-go-page]");
668
  if (pageButton) {
 
696
  state.dragTaskId = card.dataset.plannerTaskId;
697
  event.dataTransfer.setData("text/plain", state.dragTaskId);
698
  event.dataTransfer.effectAllowed = "move";
699
+ beginPlannerInteraction();
700
  card.classList.add("is-dragging");
701
  });
702
 
 
706
  card.classList.remove("is-dragging");
707
  }
708
  state.dragTaskId = null;
709
+ finishPlannerInteraction();
710
  const preview = document.getElementById("timelineDropPreview");
711
  if (preview) {
712
  preview.style.display = "none";
 
714
  });
715
 
716
  document.addEventListener("pointermove", (event) => {
717
+ if (!state.interaction || event.pointerId !== state.interaction.pointerId) {
718
  return;
719
  }
720
 
721
+ event.preventDefault();
722
+
723
  const { dayStart, dayEnd } = getPlannerConfig();
724
+ const deltaMinutes = getPointerDeltaMinutes(state.interaction.pointerStartY, event.clientY);
725
+
726
  if (state.interaction.mode === "move") {
727
+ const duration = state.interaction.initialEndMinutes - state.interaction.initialStartMinutes;
728
  const startMinutes = clamp(
729
+ state.interaction.initialStartMinutes + deltaMinutes,
730
  dayStart,
731
+ dayEnd - duration
732
  );
733
  state.interaction.startMinutes = startMinutes;
734
+ state.interaction.endMinutes = startMinutes + duration;
735
+ state.interaction.duration = duration;
736
  } else if (state.interaction.mode === "resize-start") {
737
  const startMinutes = clamp(
738
+ state.interaction.initialStartMinutes + deltaMinutes,
739
  dayStart,
740
+ state.interaction.initialEndMinutes - MIN_DURATION
741
  );
742
  state.interaction.startMinutes = startMinutes;
743
+ state.interaction.endMinutes = state.interaction.initialEndMinutes;
744
  state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
745
  } else {
746
  const endMinutes = clamp(
747
+ state.interaction.initialEndMinutes + deltaMinutes,
748
+ state.interaction.initialStartMinutes + MIN_DURATION,
749
  dayEnd
750
  );
751
+ state.interaction.startMinutes = state.interaction.initialStartMinutes;
752
  state.interaction.endMinutes = endMinutes;
753
  state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
754
  }
755
 
756
+ updateEventLayout(
757
+ state.interaction.block,
758
+ state.interaction.startMinutes,
759
+ state.interaction.endMinutes,
760
+ dayStart
761
+ );
762
  updateEventTimeLabel(state.interaction.block, state.interaction.startMinutes, state.interaction.endMinutes);
763
  });
764
 
765
+ function releaseInteractionPointer(current) {
766
+ if (
767
+ current
768
+ && typeof current.block.releasePointerCapture === "function"
769
+ && typeof current.block.hasPointerCapture === "function"
770
+ && current.pointerId !== undefined
771
+ && current.block.hasPointerCapture(current.pointerId)
772
+ ) {
773
+ try {
774
+ current.block.releasePointerCapture(current.pointerId);
775
+ } catch (error) {
776
+ // Ignore browsers that already released pointer capture.
777
+ }
778
  }
779
+ }
780
 
781
+ function persistInteraction(current) {
 
 
 
782
  requestJSON(`/api/tasks/${current.taskId}/schedule`, {
783
  method: "PATCH",
784
  body: JSON.stringify({
 
790
  .then(() => loadPlanner(state.selectedDate, true))
791
  .then(() => showToast("规划时间已更新"))
792
  .catch((error) => showToast(error.message, "error"));
793
+ }
794
+
795
+ function finishInteraction(event) {
796
+ if (!state.interaction) {
797
+ return;
798
+ }
799
+
800
+ if (event && event.pointerId !== undefined && event.pointerId !== state.interaction.pointerId) {
801
+ return;
802
+ }
803
+
804
+ const current = state.interaction;
805
+ current.block.classList.remove("is-dragging");
806
+ state.interaction = null;
807
+ releaseInteractionPointer(current);
808
+ finishPlannerInteraction();
809
+
810
+ if (
811
+ current.startMinutes === current.initialStartMinutes
812
+ && current.endMinutes === current.initialEndMinutes
813
+ ) {
814
+ return;
815
+ }
816
+
817
+ persistInteraction(current);
818
+ }
819
+
820
+ document.addEventListener("pointerup", finishInteraction);
821
+ document.addEventListener("pointercancel", finishInteraction);
822
+ window.addEventListener("blur", finishInteraction);
823
 
824
  plannerDateInput.addEventListener("change", () => {
825
  if (plannerDateInput.value) {
 
851
  });
852
 
853
  renderPlanner();
854
+ setActivePage(0);
855
  })();
static/v020.css CHANGED
@@ -60,6 +60,17 @@
60
  max-width: 100%;
61
  min-width: 100%;
62
  overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
63
  }
64
 
65
  .page-home {
@@ -225,6 +236,7 @@
225
  position: absolute;
226
  left: 10px;
227
  right: 10px;
 
228
  padding: 12px;
229
  border-radius: 18px;
230
  background: rgba(255, 255, 255, 0.04);
@@ -261,6 +273,7 @@
261
  position: absolute;
262
  left: 0;
263
  right: 0;
 
264
  border-radius: 22px;
265
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.015) 100%);
266
  pointer-events: none;
@@ -278,6 +291,8 @@
278
  .planner-event,
279
  .timeline-drop-preview {
280
  position: absolute;
 
 
281
  border-radius: 20px;
282
  padding: 14px;
283
  border: 1px solid rgba(255, 255, 255, 0.08);
@@ -285,6 +300,10 @@
285
  box-shadow: 0 16px 30px rgba(0, 0, 0, 0.2);
286
  }
287
 
 
 
 
 
288
  .planner-event::before,
289
  .timeline-drop-preview::before {
290
  content: "";
@@ -304,6 +323,7 @@
304
 
305
  .planner-event-top strong {
306
  max-width: 72%;
 
307
  }
308
 
309
  .planner-event-meta {
@@ -338,20 +358,21 @@
338
 
339
  .planner-event-resize {
340
  position: absolute;
341
- left: 18px;
342
- right: 18px;
343
- height: 8px;
344
  border-radius: 999px;
345
- background: rgba(255, 255, 255, 0.18);
346
  cursor: ns-resize;
 
347
  }
348
 
349
  .planner-event-resize-top {
350
- top: 8px;
351
  }
352
 
353
  .planner-event-resize-bottom {
354
- bottom: 8px;
355
  }
356
 
357
  .planner-event.is-dragging,
@@ -359,6 +380,64 @@
359
  opacity: 0.8;
360
  }
361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  .planner-sidebar {
363
  padding: 18px;
364
  display: grid;
 
60
  max-width: 100%;
61
  min-width: 100%;
62
  overflow: hidden;
63
+ pointer-events: none;
64
+ }
65
+
66
+ .page-slide.is-active {
67
+ pointer-events: auto;
68
+ }
69
+
70
+ body.planner-interacting,
71
+ body.planner-interacting * {
72
+ user-select: none !important;
73
+ -webkit-user-select: none !important;
74
  }
75
 
76
  .page-home {
 
236
  position: absolute;
237
  left: 10px;
238
  right: 10px;
239
+ box-sizing: border-box;
240
  padding: 12px;
241
  border-radius: 18px;
242
  background: rgba(255, 255, 255, 0.04);
 
273
  position: absolute;
274
  left: 0;
275
  right: 0;
276
+ box-sizing: border-box;
277
  border-radius: 22px;
278
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.015) 100%);
279
  pointer-events: none;
 
291
  .planner-event,
292
  .timeline-drop-preview {
293
  position: absolute;
294
+ box-sizing: border-box;
295
+ overflow: hidden;
296
  border-radius: 20px;
297
  padding: 14px;
298
  border: 1px solid rgba(255, 255, 255, 0.08);
 
300
  box-shadow: 0 16px 30px rgba(0, 0, 0, 0.2);
301
  }
302
 
303
+ .planner-event {
304
+ touch-action: none;
305
+ }
306
+
307
  .planner-event::before,
308
  .timeline-drop-preview::before {
309
  content: "";
 
323
 
324
  .planner-event-top strong {
325
  max-width: 72%;
326
+ line-height: 1.2;
327
  }
328
 
329
  .planner-event-meta {
 
358
 
359
  .planner-event-resize {
360
  position: absolute;
361
+ left: 10px;
362
+ right: 10px;
363
+ height: 6px;
364
  border-radius: 999px;
365
+ background: linear-gradient(90deg, rgba(123, 231, 234, 0.9), rgba(97, 210, 159, 0.72));
366
  cursor: ns-resize;
367
+ z-index: 2;
368
  }
369
 
370
  .planner-event-resize-top {
371
+ top: 0;
372
  }
373
 
374
  .planner-event-resize-bottom {
375
+ bottom: 0;
376
  }
377
 
378
  .planner-event.is-dragging,
 
380
  opacity: 0.8;
381
  }
382
 
383
+ .planner-event.is-compact {
384
+ border-radius: 16px;
385
+ padding: 8px 12px 10px;
386
+ }
387
+
388
+ .planner-event.is-compact::before {
389
+ border-radius: 16px 0 0 16px;
390
+ }
391
+
392
+ .task-event.is-compact .planner-event-top {
393
+ display: grid;
394
+ gap: 4px;
395
+ }
396
+
397
+ .task-event.is-compact .planner-event-meta,
398
+ .task-event.is-compact .planner-event-clear {
399
+ display: none;
400
+ }
401
+
402
+ .course-event.is-compact .planner-lock-badge {
403
+ display: none;
404
+ }
405
+
406
+ .course-event.is-compact .planner-event-meta {
407
+ margin-top: 4px;
408
+ font-size: 0.76rem;
409
+ }
410
+
411
+ .course-event.is-compact .planner-event-meta span:last-child {
412
+ display: none;
413
+ }
414
+
415
+ .planner-event.is-tight {
416
+ padding: 6px 10px;
417
+ }
418
+
419
+ .planner-event.is-tight .planner-event-meta,
420
+ .planner-event.is-tight .planner-event-clear,
421
+ .planner-event.is-tight .planner-lock-badge,
422
+ .planner-event.is-tight .planner-event-time {
423
+ display: none;
424
+ }
425
+
426
+ .planner-event.is-tight .planner-event-top {
427
+ display: block;
428
+ }
429
+
430
+ .planner-event.is-tight .planner-event-top strong {
431
+ max-width: 100%;
432
+ font-size: 0.74rem;
433
+ }
434
+
435
+ .planner-event.is-tight .planner-event-resize {
436
+ left: 8px;
437
+ right: 8px;
438
+ height: 5px;
439
+ }
440
+
441
  .planner-sidebar {
442
  padding: 18px;
443
  display: grid;