@nomideusz/svelte-calendar 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -470,6 +470,8 @@ Component fallbacks use system fonts and a neutral blue accent, so the package s
470
470
  | `--dt-text-3` | Tertiary text |
471
471
  | `--dt-accent` | Accent color |
472
472
  | `--dt-accent-dim` | Accent at ~12% opacity (selection, highlights) |
473
+ | `--dt-ev-fill` | Event wash, as a percentage of the event's colour (default `0%`; the `eventFill` prop sets it) |
474
+ | `--dt-ev-bar` | Width of an event's colour bar (default `3px`; the `eventBar` prop sets it) |
473
475
  | `--dt-event` | Bar color of an event without its own `color` (the presets set it to the text color, so such events draw in ink) |
474
476
  | `--dt-accent-fg` | Text on the planner's today-number badge (default `#fff`; not set by the presets) |
475
477
  | `--dt-btn-text` | Text on accent-filled buttons and badges |
@@ -535,6 +537,14 @@ const palette = generatePalette('#e11d48'); // (accent?, count = 15)
535
537
  const adapter = createMemoryAdapter(events, { palette });
536
538
  ```
537
539
 
540
+ Events are transparent by default: a bar in the event's colour, ink text on the paper. To give them a background, or change the bar:
541
+
542
+ ```svelte
543
+ <Calendar {adapter} eventFill={0.15} /> <!-- a light wash in each event's colour -->
544
+ <Calendar {adapter} eventFill={0.15} eventBar={0} /> <!-- wash only, no bar -->
545
+ <Calendar {adapter} eventBar={5} /> <!-- a heavier bar -->
546
+ ```
547
+
538
548
  Related exports: `VIVID_PALETTE` (the vivid palette, the default before 1.4) and `extractAccent(themeString)` (the accent color of a `--dt-*` theme string).
539
549
 
540
550
  ### Multi-day & All-day
@@ -1106,6 +1116,8 @@ Every `<Calendar>` prop (`CalendarProps`), grouped by what it controls.
1106
1116
  | `theme` | `string` | `auto` | CSS theme string of `--dt-*` properties. `auto` probes the host page. |
1107
1117
  | `autoTheme` | `AutoThemeOptions \| false` | `undefined` (probe) | Fine-tune auto-detection: `{ mode, accent, font }`. `false` disables probing. |
1108
1118
  | `borderRadius` | `number` | `0` | Corner radius in px (square by default) |
1119
+ | `eventFill` | `number` | `0` | Wash each event in its own colour, 0–1 (`0.15` = a light tint). Sets `--dt-ev-fill` |
1120
+ | `eventBar` | `number` | `3` | Width in px of the colour bar on an event's leading edge; `0` removes it. Sets `--dt-ev-bar` |
1109
1121
 
1110
1122
  ### Locale and timezone
1111
1123
 
@@ -95,7 +95,7 @@ import { wrapAdapterWithTimezone, toZonedTime, fromZonedTime } from "../core/tim
95
95
  import { isReadOnlyError, isNotFoundError } from "../adapters/errors.js";
96
96
  /** Breakpoint (px) at which auto-mobile activates */
97
97
  const MOBILE_BREAKPOINT = 768;
98
- let { adapter, views = defaultViews, view: activeViewId, theme = auto, autoTheme, mondayStart = true, height: heightProp = 600, borderRadius = 0, dir, locale, labels: labelsProp, readOnly = false, visibleHours, initialDate, snapInterval = 15, minColumnWidth = 110, showModePills = true, showNavigation = true, equalDays = false, showDates = true, hideDays, currentDate, blockedSlots, days, minDuration, maxDuration, disabledDates, compact = false, columns = false, mobile: mobileProp = "auto", event: eventSnippet, empty: emptySnippet, dayHeader: dayHeaderSnippet, header: headerSnippet, navigation: navigationSnippet, oneventclick, oneventcreate, onexternaldrop, oneventmove, onviewchange, ondatechange, oneventhover, ondayclick, onerror, timezone } = $props();
98
+ let { adapter, views = defaultViews, view: activeViewId, theme = auto, autoTheme, mondayStart = true, height: heightProp = 600, borderRadius = 0, eventFill, eventBar, dir, locale, labels: labelsProp, readOnly = false, visibleHours, initialDate, snapInterval = 15, minColumnWidth = 110, showModePills = true, showNavigation = true, equalDays = false, showDates = true, hideDays, currentDate, blockedSlots, days, minDuration, maxDuration, disabledDates, compact = false, columns = false, mobile: mobileProp = "auto", event: eventSnippet, empty: emptySnippet, dayHeader: dayHeaderSnippet, header: headerSnippet, navigation: navigationSnippet, oneventclick, oneventcreate, onexternaldrop, oneventmove, onviewchange, ondatechange, oneventhover, ondayclick, onerror, timezone } = $props();
99
99
  // In readOnly mode, suppress mutation callbacks. With a timezone, the
100
100
  // drag plane is zoned wall-clock — hosts always receive real instants.
101
101
  const unzone = (d) => timezone ? fromZonedTime(d, timezone) : d;
@@ -672,7 +672,7 @@ const navCtx = $derived({
672
672
  <div
673
673
  class="cal"
674
674
  bind:this={calEl}
675
- style="{effectiveTheme}; {heightProp === 'auto' ? '' : `--cal-h: ${heightProp}px;`} --cal-r: {borderRadius}px"
675
+ style="{effectiveTheme}; {heightProp === 'auto' ? '' : `--cal-h: ${heightProp}px;`} --cal-r: {borderRadius}px;{eventFill == null ? '' : ` --dt-ev-fill: ${eventFill * 100}%;`}{eventBar == null ? '' : ` --dt-ev-bar: ${eventBar}px;`}"
676
676
  class:cal--auto={heightProp === 'auto'}
677
677
  role="region"
678
678
  aria-label={L.calendar}
@@ -759,7 +759,8 @@ const navCtx = $derived({
759
759
  <span class="cal-sr" role="status" aria-live="polite" aria-atomic="true">{noteText}</span>
760
760
  {/if}
761
761
 
762
- <div class="cal-body">
762
+ <!-- mobile='auto' doesn't know phone vs desktop until measured: hidden till then (SSR), shown anyway after 1.5s -->
763
+ <div class="cal-body" class:cal-body--unmeasured={mobileProp === 'auto' && containerWidth === 0}>
763
764
  {#if activeView}
764
765
  {@const Comp = activeView.component}
765
766
  <Comp
@@ -1002,6 +1003,11 @@ const navCtx = $derived({
1002
1003
  .cal--auto .cal-body {
1003
1004
  overflow: visible;
1004
1005
  }
1006
+ .cal-body--unmeasured {
1007
+ visibility: hidden;
1008
+ animation: cal-reveal 0s 1.5s forwards;
1009
+ }
1010
+ @keyframes cal-reveal { to { visibility: visible; } }
1005
1011
 
1006
1012
  .cal-empty {
1007
1013
  display: flex;
@@ -71,6 +71,13 @@ export interface CalendarProps {
71
71
  height?: number | 'auto';
72
72
  /** Border radius in px (default: 0, square-cut). */
73
73
  borderRadius?: number;
74
+ /**
75
+ * Wash each event in its own colour, 0–1 (default 0: transparent). Text
76
+ * stays the theme's ink, so a light tint (0.1–0.2) reads best.
77
+ */
78
+ eventFill?: number;
79
+ /** Width in px of the colour bar on an event's leading edge (default 3). 0 removes it. */
80
+ eventBar?: number;
74
81
  /** Text direction: 'ltr' (default), 'rtl', or 'auto' */
75
82
  dir?: 'ltr' | 'rtl' | 'auto';
76
83
  /** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
@@ -526,7 +526,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
526
526
  }
527
527
  .ag-body::-webkit-scrollbar-thumb {
528
528
  background: var(--dt-border);
529
- border-radius: 2px;
529
+ border-radius: 0;
530
530
  }
531
531
 
532
532
  /* ═══ All-day strip ═══ */
@@ -600,6 +600,9 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
600
600
  align-items: stretch;
601
601
  border-radius: 0;
602
602
  background: transparent;
603
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
604
+ --_fill: color-mix(in srgb, var(--ev-color) var(--dt-ev-fill, 0%), transparent);
605
+ background-image: linear-gradient(var(--_fill), var(--_fill));
603
606
  border: none;
604
607
  border-bottom: 1px solid var(--dt-border, rgba(15, 15, 15, 0.3));
605
608
  overflow: hidden;
@@ -612,12 +615,12 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
612
615
  left: 0;
613
616
  top: 0;
614
617
  bottom: 0;
615
- width: 3px;
618
+ width: var(--dt-ev-bar, 3px);
616
619
  background: var(--ev-color);
617
620
  }
618
621
  .ag-card:hover,
619
622
  .ag-card:active {
620
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
623
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
621
624
  }
622
625
  .ag-card:focus-visible {
623
626
  outline: none;
@@ -859,6 +862,9 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
859
862
  margin-bottom: 8px;
860
863
  border-radius: 0;
861
864
  background: var(--dt-accent-dim, color-mix(in srgb, var(--dt-accent, #1d4fd7) 12%, transparent));
865
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
866
+ --_fill: color-mix(in srgb, var(--ev-color) var(--dt-ev-fill, 0%), transparent);
867
+ background-image: linear-gradient(var(--_fill), var(--_fill));
862
868
  border: none;
863
869
  border-bottom: 1px solid var(--dt-border, rgba(15, 15, 15, 0.3));
864
870
  cursor: pointer;
@@ -870,12 +876,13 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
870
876
  left: 0;
871
877
  top: 0;
872
878
  bottom: 0;
873
- width: 3px;
879
+ width: var(--dt-ev-bar, 3px);
874
880
  background: var(--ev-color, var(--dt-accent));
875
881
  }
876
882
  .ag-q-now:hover,
877
883
  .ag-q-now:active {
878
884
  background:
885
+ linear-gradient(var(--_fill), var(--_fill)),
879
886
  linear-gradient(var(--dt-hover, rgba(15, 15, 15, 0.04)), var(--dt-hover, rgba(15, 15, 15, 0.04))),
880
887
  var(--dt-accent-dim, color-mix(in srgb, var(--dt-accent, #1d4fd7) 12%, transparent));
881
888
  }
@@ -892,16 +899,8 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
892
899
  border-radius: 50%;
893
900
  background: var(--ev-color, var(--dt-accent, #1d4fd7));
894
901
  margin-bottom: 8px;
895
- animation: ag-pulse 2.5s ease-in-out infinite;
896
- }
897
- @keyframes ag-pulse {
898
- 0%, 100% { opacity: 1; }
899
- 50% { opacity: 0.4; }
900
902
  }
901
903
  @media (prefers-reduced-motion: reduce) {
902
- .ag-q-now-dot {
903
- animation: none;
904
- }
905
904
  .ag-q-now-fill {
906
905
  transition: none;
907
906
  }
@@ -1006,6 +1005,9 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1006
1005
  display: none;
1007
1006
  }
1008
1007
  .ag-log-row {
1008
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
1009
+ --_fill: color-mix(in srgb, var(--ev-color) var(--dt-ev-fill, 0%), transparent);
1010
+ background-image: linear-gradient(var(--_fill), var(--_fill));
1009
1011
  display: flex;
1010
1012
  align-items: baseline;
1011
1013
  gap: 10px;
@@ -1024,7 +1026,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1024
1026
  }
1025
1027
  .ag-log-row:hover,
1026
1028
  .ag-log-row:active {
1027
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
1029
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
1028
1030
  }
1029
1031
  .ag-log-row:focus-visible {
1030
1032
  outline: none;
@@ -1050,7 +1052,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1050
1052
  /* The colour dot of a past row becomes its 3px start bar (dimmed: it is over). */
1051
1053
  .ag-log-dot {
1052
1054
  align-self: stretch;
1053
- width: 3px;
1055
+ width: var(--dt-ev-bar, 3px);
1054
1056
  border-radius: 0;
1055
1057
  flex-shrink: 0;
1056
1058
  opacity: 0.45;
@@ -1088,6 +1090,9 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1088
1090
  .ag-compact-list::-webkit-scrollbar { display: none; }
1089
1091
  /* A compact row: 3px start bar, hairline beneath, ink text. */
1090
1092
  .ag-compact-row {
1093
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
1094
+ --_fill: color-mix(in srgb, var(--ev-color) var(--dt-ev-fill, 0%), transparent);
1095
+ background-image: linear-gradient(var(--_fill), var(--_fill));
1091
1096
  position: relative;
1092
1097
  display: flex;
1093
1098
  align-items: baseline;
@@ -1104,7 +1109,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1104
1109
  left: 0;
1105
1110
  top: 0;
1106
1111
  bottom: 0;
1107
- width: 3px;
1112
+ width: var(--dt-ev-bar, 3px);
1108
1113
  background: var(--ev-color, var(--dt-accent));
1109
1114
  }
1110
1115
  .ag-compact-row--selected {
@@ -1114,7 +1119,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1114
1119
  .ag-compact-row:active .ag-compact-row-title { color: var(--dt-text); }
1115
1120
  .ag-compact-row:hover,
1116
1121
  .ag-compact-row:active {
1117
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
1122
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
1118
1123
  }
1119
1124
  .ag-compact-row:focus-visible {
1120
1125
  outline: none;
@@ -579,7 +579,7 @@ const weekDays = $derived.by(() => {
579
579
  }
580
580
  .ag-body::-webkit-scrollbar-thumb {
581
581
  background: var(--dt-border);
582
- border-radius: 2px;
582
+ border-radius: 0;
583
583
  }
584
584
 
585
585
  /* ═══ All-day chips ═══ */
@@ -639,6 +639,9 @@ const weekDays = $derived.by(() => {
639
639
  align-items: stretch;
640
640
  border-radius: 0;
641
641
  background: transparent;
642
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
643
+ --_fill: color-mix(in srgb, var(--ev-color) var(--dt-ev-fill, 0%), transparent);
644
+ background-image: linear-gradient(var(--_fill), var(--_fill));
642
645
  border: none;
643
646
  border-bottom: 1px solid var(--dt-border, rgba(15, 15, 15, 0.3));
644
647
  overflow: hidden;
@@ -651,12 +654,12 @@ const weekDays = $derived.by(() => {
651
654
  left: 0;
652
655
  top: 0;
653
656
  bottom: 0;
654
- width: 3px;
657
+ width: var(--dt-ev-bar, 3px);
655
658
  background: var(--ev-color);
656
659
  }
657
660
  .ag-card:hover,
658
661
  .ag-card:active {
659
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
662
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
660
663
  }
661
664
  .ag-card:focus-visible {
662
665
  outline: none;
@@ -939,6 +942,9 @@ const weekDays = $derived.by(() => {
939
942
  }
940
943
  /* A compact row: 3px start bar, hairline beneath, ink text. */
941
944
  .ag-compact {
945
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
946
+ --_fill: color-mix(in srgb, var(--ev-color) var(--dt-ev-fill, 0%), transparent);
947
+ background-image: linear-gradient(var(--_fill), var(--_fill));
942
948
  position: relative;
943
949
  display: flex;
944
950
  align-items: baseline;
@@ -955,7 +961,7 @@ const weekDays = $derived.by(() => {
955
961
  left: 0;
956
962
  top: 0;
957
963
  bottom: 0;
958
- width: 3px;
964
+ width: var(--dt-ev-bar, 3px);
959
965
  background: var(--ev-color, var(--dt-accent));
960
966
  }
961
967
  .ag-compact--selected {
@@ -967,7 +973,7 @@ const weekDays = $derived.by(() => {
967
973
  }
968
974
  .ag-compact:hover,
969
975
  .ag-compact:active {
970
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
976
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
971
977
  }
972
978
  .ag-compact:focus-visible {
973
979
  outline: none;
@@ -13,6 +13,7 @@ import { HOUR_MS, sod, addDaysMs, diffDays, isAllDay, segmentForDay } from "../.
13
13
  import { fmtHParts, fmtTime } from "../../core/locale.js";
14
14
  import { createSwipe } from "./swipe.js";
15
15
  import { dateLong } from "../shared/format.js";
16
+ import { openingTop } from "../shared/opening-top.js";
16
17
  let { height = null, events = [], style = "", locale, focusDate, oneventclick, oneventcreate, selectedEventId = null, readOnly = false, visibleHours } = $props();
17
18
  // ── Context ────────────────────────────────────────
18
19
  const ctx = useCalendarContext();
@@ -459,13 +460,18 @@ function onResizeCancel() {
459
460
  cleanupResize();
460
461
  }
461
462
  // ── Auto-scroll to now ─────────────────────────────
463
+ // The hour before now, its label clear of the top, no event sliced there
464
+ // (openingTop). The grid's 8px padding sits above hour 0.
462
465
  let gridEl;
463
466
  $effect(() => {
464
467
  void dayMs;
465
468
  const el = gridEl;
466
469
  if (!el) return;
467
470
  untrack(() => {
468
- if (nowOffset >= 0) el.scrollTop = Math.max(0, nowOffset - 120);
471
+ if (nowOffset < 0) return;
472
+ const line = Math.max(0, Math.floor(nowOffset / HOUR_HEIGHT - 1)) * HOUR_HEIGHT;
473
+ const y = openingTop(positionedEvents, line, HOUR_HEIGHT, 8, nowOffset, el.clientHeight - 8);
474
+ el.scrollTop = y && y + 8;
469
475
  });
470
476
  });
471
477
  </script>
@@ -962,6 +968,9 @@ $effect(() => {
962
968
  border-radius: 0;
963
969
  cursor: pointer;
964
970
  background: transparent;
971
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
972
+ --_fill: color-mix(in srgb, var(--ev-color) var(--dt-ev-fill, 0%), transparent);
973
+ background-image: linear-gradient(var(--_fill), var(--_fill));
965
974
  border: none;
966
975
  border-bottom: 1px solid var(--dt-border, rgba(15, 15, 15, 0.3));
967
976
  box-sizing: border-box;
@@ -976,7 +985,7 @@ $effect(() => {
976
985
  }
977
986
  .mb-event:hover,
978
987
  .mb-event:active {
979
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
988
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
980
989
  }
981
990
  /* Short blocks keep their duration-proportional height, but get a 44px
982
991
  transparent hit-slop so taps still land. */
@@ -1003,6 +1012,7 @@ $effect(() => {
1003
1012
  laid over the paper, opaque, so the line runs beneath its text. */
1004
1013
  .mb-event.mb-event--current {
1005
1014
  background:
1015
+ linear-gradient(var(--_fill), var(--_fill)),
1006
1016
  linear-gradient(var(--dt-accent-dim, rgba(29, 79, 215, 0.12)), var(--dt-accent-dim, rgba(29, 79, 215, 0.12))),
1007
1017
  var(--dt-paper, none),
1008
1018
  var(--dt-bg, #efebe9);
@@ -1010,6 +1020,7 @@ $effect(() => {
1010
1020
  .mb-event.mb-event--current:hover,
1011
1021
  .mb-event.mb-event--current:active {
1012
1022
  background:
1023
+ linear-gradient(var(--_fill), var(--_fill)),
1013
1024
  linear-gradient(var(--dt-hover, rgba(15, 15, 15, 0.04)), var(--dt-hover, rgba(15, 15, 15, 0.04))),
1014
1025
  var(--dt-paper, none),
1015
1026
  var(--dt-bg, #efebe9);
@@ -1121,7 +1132,7 @@ $effect(() => {
1121
1132
  }
1122
1133
 
1123
1134
  .mb-ev-stripe {
1124
- width: 3px;
1135
+ width: var(--dt-ev-bar, 3px);
1125
1136
  background: var(--ev-color);
1126
1137
  flex-shrink: 0;
1127
1138
  border-radius: 0;
@@ -422,6 +422,9 @@ function handleDayKeydown(e, dayMs) {
422
422
  min-height: 44px;
423
423
  border-radius: 0;
424
424
  background: transparent;
425
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
426
+ --_fill: color-mix(in srgb, var(--ev-color) var(--dt-ev-fill, 0%), transparent);
427
+ background-image: linear-gradient(var(--_fill), var(--_fill));
425
428
  overflow: hidden;
426
429
  cursor: pointer;
427
430
  transition: background 120ms;
@@ -434,13 +437,13 @@ function handleDayKeydown(e, dayMs) {
434
437
  }
435
438
  .mw-ev:hover,
436
439
  .mw-ev:active {
437
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
440
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
438
441
  }
439
442
  .mw-ev--selected {
440
443
  box-shadow: inset 0 0 0 1px var(--ev-color);
441
444
  }
442
445
  .mw-ev--current {
443
- background: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
446
+ background-color: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
444
447
  }
445
448
  /* All day: a solid band on the event colour, no tint. */
446
449
  .mw-ev--allday {
@@ -486,7 +489,7 @@ function handleDayKeydown(e, dayMs) {
486
489
  }
487
490
 
488
491
  .mw-ev-stripe {
489
- width: 3px;
492
+ width: var(--dt-ev-bar, 3px);
490
493
  align-self: stretch;
491
494
  background: var(--ev-color, var(--dt-accent));
492
495
  flex-shrink: 0;
@@ -2,7 +2,7 @@
2
2
  MonthGrid — classic month overview.
3
3
 
4
4
  Week-aligned grid (4–6 rows, driven by the view-state 'month' range).
5
- Each cell lists up to MAX_CHIPS events plus a "+N more" overflow; clicking
5
+ Each cell lists as many events as fit plus a "+N more" overflow; clicking
6
6
  an event fires oneventclick, clicking a day (or the overflow) fires
7
7
  ondayclick — the natural month → day drill-down.
8
8
  -->
@@ -29,7 +29,7 @@ const todayMs = $derived(clock.today);
29
29
  // cell tap (day drill-down) carry the interaction.
30
30
  const dotsMode = $derived(isMobile);
31
31
  const MAX_CHIPS = 3;
32
- // How many chips a cell has room for, measured, never more than MAX_CHIPS.
32
+ // How many chips a cell has room for, measured (MAX_CHIPS until then).
33
33
  // A cell that overflows must also show its "+N more", so it gets one row
34
34
  // fewer when that row would not fit beside its chips: a hidden event is
35
35
  // always counted, never clipped away. Auto height and dots grow instead.
@@ -47,8 +47,8 @@ function measureFit() {
47
47
  const chipH = chip.offsetHeight;
48
48
  const moreH = bodyEl?.querySelector(".mg-more")?.offsetHeight ?? chipH;
49
49
  if (!chipH) return;
50
- fitAll = Math.min(MAX_CHIPS, Math.floor((room + gap) / (chipH + gap)));
51
- fitMore = Math.max(0, Math.min(MAX_CHIPS, Math.floor((room - moreH) / (chipH + gap))));
50
+ fitAll = Math.max(0, Math.floor((room + gap) / (chipH + gap)));
51
+ fitMore = Math.max(0, Math.floor((room - moreH) / (chipH + gap)));
52
52
  }
53
53
  $effect(() => {
54
54
  if (!bodyEl || autoHeight || dotsMode) {
@@ -456,7 +456,10 @@ function chipTime(e) {
456
456
  gap: 5px;
457
457
  border: none;
458
458
  background: none;
459
- padding: 2px 4px;
459
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
460
+ --_fill: color-mix(in srgb, var(--mg-chip-color) var(--dt-ev-fill, 0%), transparent);
461
+ background-image: linear-gradient(var(--_fill), var(--_fill));
462
+ padding: 1px 4px;
460
463
  border-radius: 0;
461
464
  font: 400 12.5px/1.3 var(--dt-sans, system-ui, sans-serif);
462
465
  color: var(--dt-text);
@@ -465,14 +468,14 @@ function chipTime(e) {
465
468
  text-align: left;
466
469
  }
467
470
  .mg-chip:hover {
468
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
471
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
469
472
  }
470
473
  .mg-chip:focus-visible {
471
474
  box-shadow: 0 0 0 2px var(--dt-accent, #1d4fd7);
472
475
  outline: none;
473
476
  }
474
477
  .mg-chip--selected {
475
- background: var(--dt-accent-dim);
478
+ background-color: var(--dt-accent-dim);
476
479
  }
477
480
  .mg-chip--cancelled {
478
481
  text-decoration: line-through;
@@ -486,7 +489,7 @@ function chipTime(e) {
486
489
  .mg-chip-dot {
487
490
  flex: none;
488
491
  align-self: stretch;
489
- width: 3px;
492
+ width: var(--dt-ev-bar, 3px);
490
493
  border-radius: 0;
491
494
  background: var(--mg-chip-color);
492
495
  }
@@ -558,6 +561,7 @@ function chipTime(e) {
558
561
  }
559
562
  .mg--dots .mg-chip {
560
563
  padding: 3px;
564
+ background-image: none; /* a mark, not a chip: no eventFill wash */
561
565
  min-height: 0;
562
566
  }
563
567
  .mg--dots .mg-chip-title,
@@ -1372,6 +1372,9 @@ function onCellKeydown(e, ms) {
1372
1372
  padding: 0 3px 0 9px;
1373
1373
  border-radius: 0;
1374
1374
  background: transparent;
1375
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
1376
+ --_fill: color-mix(in srgb, var(--ev-color) var(--dt-ev-fill, 0%), transparent);
1377
+ background-image: linear-gradient(var(--_fill), var(--_fill));
1375
1378
  cursor: pointer;
1376
1379
  overflow: hidden;
1377
1380
  transition: background 0.12s, box-shadow 0.12s;
@@ -1381,12 +1384,12 @@ function onCellKeydown(e, ms) {
1381
1384
  content: '';
1382
1385
  position: absolute;
1383
1386
  inset: 0 auto 0 0;
1384
- width: 3px;
1387
+ width: var(--dt-ev-bar, 3px);
1385
1388
  background: var(--ev-color);
1386
1389
  }
1387
1390
 
1388
1391
  .wg-ev:hover {
1389
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
1392
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
1390
1393
  }
1391
1394
 
1392
1395
  .wg-ev--drag-preview {
@@ -1396,7 +1399,7 @@ function onCellKeydown(e, ms) {
1396
1399
  z-index: 8;
1397
1400
  opacity: 0.95;
1398
1401
  pointer-events: none;
1399
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
1402
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
1400
1403
  box-shadow: inset 0 0 0 1px var(--ev-color);
1401
1404
  cursor: grabbing;
1402
1405
  }
@@ -1406,7 +1409,7 @@ function onCellKeydown(e, ms) {
1406
1409
  }
1407
1410
 
1408
1411
  .wg-ev--current {
1409
- background: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
1412
+ background-color: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
1410
1413
  }
1411
1414
 
1412
1415
  /* Printed states: struck through, grey, or a dashed bar — never a tint */
@@ -1497,7 +1500,7 @@ function onCellKeydown(e, ms) {
1497
1500
 
1498
1501
  .wg-ev--dragging {
1499
1502
  cursor: grabbing;
1500
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
1503
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
1501
1504
  box-shadow: inset 0 0 0 1px var(--ev-color);
1502
1505
  }
1503
1506
  </style>
@@ -23,6 +23,7 @@ import { HOUR_MS, sod, addDaysMs, diffDays } from "../../core/time.js";
23
23
  import { startOfWeek as sowFn, isAllDay, segmentForDay } from "../../core/time.js";
24
24
  import { fmtHParts, fmtTime, fmtDayNum, fmtDuration, fmtWeekRange, weekdayShort, weekdayLong } from "../../core/locale.js";
25
25
  import { dateLong } from "../shared/format.js";
26
+ import { openingTop } from "../shared/opening-top.js";
26
27
  let { mode = "week", mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
27
28
  // ── Context ────────────────────────────────────────
28
29
  const ctx = useCalendarContext();
@@ -302,9 +303,8 @@ const weekIsEmpty = $derived(!events.some((ev) => ev.start.getTime() < weekEndMs
302
303
  // ── Auto-scroll (mount + week change) ──────────────
303
304
  // Week with today → current time minus ~1h of context; otherwise the
304
305
  // visibleHours start or 08:00, whichever is later. Lands on a whole hour,
305
- // with room for its label (centred on the line) below the sticky top.
306
- // An event running across that hour line would be sliced under the sticky
307
- // top, so the scroll starts at that event's hour instead.
306
+ // with room for its label (centred on the line) below the sticky top,
307
+ // moved so no event is sliced under it (openingTop).
308
308
  // A server-rendered grid sits at midnight until this runs: it stays
309
309
  // hidden until placed (.tw-body--unplaced), and shows anyway after 1.5s.
310
310
  let placed = $state(false);
@@ -316,32 +316,15 @@ $effect(() => {
316
316
  if (!el) return;
317
317
  untrack(() => {
318
318
  let targetHour;
319
+ let anchorHour;
319
320
  if (weekHasToday) {
320
- const clamped = Math.min(Math.max(nowFracHour, startHour), endHour);
321
- targetHour = Math.max(startHour, clamped - 1);
321
+ anchorHour = Math.min(Math.max(nowFracHour, startHour), endHour);
322
+ targetHour = Math.max(startHour, anchorHour - 1);
322
323
  } else {
323
- targetHour = Math.max(startHour, Math.min(8, endHour - 1));
324
+ targetHour = anchorHour = Math.max(startHour, Math.min(8, endHour - 1));
324
325
  }
325
- let hour = Math.floor(targetHour);
326
- // Moving up can slice an earlier event on another day, so repeat
327
- // until no block crosses the line.
328
- // ponytail: looks back at most 2 h, so a long block can't drag the view to dawn.
329
- const floorY = (hour - 2 - startHour) * HOUR_H;
330
- for (let moved = true; moved;) {
331
- moved = false;
332
- const line = (hour - startHour) * HOUR_H;
333
- for (const day of layoutByDay.values()) {
334
- for (const p of day) {
335
- if (p.top < line && p.top + p.height > line && p.top >= floorY) {
336
- hour = Math.floor(startHour + p.top / HOUR_H);
337
- moved = true;
338
- break;
339
- }
340
- }
341
- if (moved) break;
342
- }
343
- }
344
- el.scrollTop = Math.max(0, (hour - startHour) * HOUR_H - LABEL_ROOM);
326
+ const topH = el.querySelector(".tw-top")?.offsetHeight ?? 0;
327
+ el.scrollTop = openingTop([...layoutByDay.values()].flat(), (Math.floor(targetHour) - startHour) * HOUR_H, HOUR_H, LABEL_ROOM, (anchorHour - startHour) * HOUR_H, el.clientHeight - topH);
345
328
  placed = true;
346
329
  });
347
330
  });
@@ -1245,7 +1228,7 @@ function onWindowKeydown(e) {
1245
1228
  .tw-scroll::-webkit-scrollbar { width: 5px; height: 5px; }
1246
1229
  .tw-scroll::-webkit-scrollbar-thumb {
1247
1230
  background: var(--dt-scrollbar, rgba(15, 15, 15, 0.1));
1248
- border-radius: 4px;
1231
+ border-radius: 0;
1249
1232
  }
1250
1233
  .tw-scroll::-webkit-scrollbar-track { background: transparent; }
1251
1234
 
@@ -1673,6 +1656,9 @@ function onWindowKeydown(e) {
1673
1656
  z-index: 6;
1674
1657
  border-radius: 0;
1675
1658
  background: transparent;
1659
+ /* eventFill: a wash in the event's colour; hover/current set background-color under it */
1660
+ --_fill: color-mix(in srgb, var(--ev-color) var(--dt-ev-fill, 0%), transparent);
1661
+ background-image: linear-gradient(var(--_fill), var(--_fill));
1676
1662
  display: flex;
1677
1663
  align-items: stretch;
1678
1664
  overflow: hidden;
@@ -1685,7 +1671,7 @@ function onWindowKeydown(e) {
1685
1671
  -webkit-tap-highlight-color: transparent;
1686
1672
  }
1687
1673
  .tw-ev:hover {
1688
- background: var(--dt-hover, rgba(15, 15, 15, 0.04));
1674
+ background-color: var(--dt-hover, rgba(15, 15, 15, 0.04));
1689
1675
  z-index: 8;
1690
1676
  }
1691
1677
  /* Short blocks keep duration-proportional height, but get a 44px
@@ -1705,12 +1691,13 @@ function onWindowKeydown(e) {
1705
1691
  z-index: 9;
1706
1692
  }
1707
1693
  .tw-ev--current {
1708
- background: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
1694
+ background-color: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
1709
1695
  }
1710
1696
  /* An event the now line crosses lays its column's ground (and its own
1711
1697
  wash) over the paper, so the line passes beneath its text. */
1712
1698
  .tw-ev.tw-ev--on-now {
1713
1699
  background:
1700
+ linear-gradient(var(--_fill), var(--_fill)),
1714
1701
  linear-gradient(var(--tw-ev-wash, transparent), var(--tw-ev-wash, transparent)),
1715
1702
  linear-gradient(var(--tw-col-wash, transparent), var(--tw-col-wash, transparent)),
1716
1703
  var(--dt-paper, none),
@@ -1754,7 +1741,7 @@ function onWindowKeydown(e) {
1754
1741
  .tw-ev--readonly { cursor: default; }
1755
1742
 
1756
1743
  .tw-ev-stripe {
1757
- width: 3px;
1744
+ width: var(--dt-ev-bar, 3px);
1758
1745
  background: var(--ev-color);
1759
1746
  flex-shrink: 0;
1760
1747
  }
@@ -1846,8 +1833,8 @@ function onWindowKeydown(e) {
1846
1833
  position: absolute;
1847
1834
  top: 4px;
1848
1835
  right: 4px;
1849
- width: 6px;
1850
- height: 6px;
1836
+ width: 7px;
1837
+ height: 7px;
1851
1838
  border-radius: 50%;
1852
1839
  background: var(--ev-color, var(--dt-accent));
1853
1840
  animation: tw-pulse 2s ease-in-out infinite;
@@ -1895,7 +1882,7 @@ function onWindowKeydown(e) {
1895
1882
  transform: translateX(-50%);
1896
1883
  width: 20px;
1897
1884
  height: 3px;
1898
- border-radius: 2px;
1885
+ border-radius: 0;
1899
1886
  background: var(--ev-color);
1900
1887
  opacity: 0;
1901
1888
  transition: opacity 120ms;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Where a time grid opens: the y (grid px, 0 = first visible hour) to put at
3
+ * the top of the scroller so no event is sliced there.
4
+ *
5
+ * Starts at the hour line `line`, moves up an hour at a time while an event
6
+ * crosses it (looking back at most 2 h), then leaves `room` above for the
7
+ * hour label. If an event still crosses the top, the top goes to that event's
8
+ * start while `anchor` (the now line) stays on screen, else to the first hour
9
+ * after it ends, else the room is dropped; only a block too long for all of
10
+ * those is cut.
11
+ */
12
+ export declare function openingTop(blocks: readonly {
13
+ top: number;
14
+ height: number;
15
+ }[], line: number, hourH: number, room: number, anchor: number, viewH: number): number;