frappe-ui 1.0.0-beta.71 → 1.0.0-beta.72

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.
Files changed (46) hide show
  1. package/experimental/Calendar/Calendar.api.md +20 -4
  2. package/experimental/Calendar/Calendar.md +93 -42
  3. package/experimental/Calendar/Calendar.vue +110 -44
  4. package/experimental/Calendar/CalendarAgenda.vue +618 -0
  5. package/experimental/Calendar/CalendarDaily.vue +281 -73
  6. package/experimental/Calendar/CalendarEventPopover.vue +75 -0
  7. package/experimental/Calendar/CalendarEventRow.vue +327 -0
  8. package/experimental/Calendar/CalendarMonthEvent.vue +68 -93
  9. package/experimental/Calendar/CalendarMonthly.vue +275 -55
  10. package/experimental/Calendar/CalendarTimeMarker.vue +15 -7
  11. package/experimental/Calendar/CalendarWeekDayEvent.vue +608 -168
  12. package/experimental/Calendar/CalendarWeekly.vue +312 -154
  13. package/experimental/Calendar/Icon/AgendaIcon.vue +14 -0
  14. package/experimental/Calendar/agendaDays.ts +206 -0
  15. package/experimental/Calendar/calendarUtils.ts +89 -130
  16. package/experimental/Calendar/composables/useCalendarData.ts +11 -5
  17. package/experimental/Calendar/composables/useEventModal.ts +3 -1
  18. package/experimental/Calendar/composables/useHourGrid.ts +71 -0
  19. package/experimental/Calendar/eventRow.ts +199 -0
  20. package/experimental/Calendar/eventSpan.ts +127 -3
  21. package/experimental/Calendar/index.ts +6 -0
  22. package/experimental/Calendar/monthStrip.ts +4 -51
  23. package/experimental/Calendar/style.css +90 -22
  24. package/experimental/Calendar/types.ts +47 -7
  25. package/experimental/Calendar/useEventBase.ts +63 -32
  26. package/experimental/TextEditor/extensions/image-group/ImageGroupNodeView.vue +2 -2
  27. package/experimental/TextEditor/extensions/image-group/ImageGroupUploadDialog.vue +3 -3
  28. package/experimental/TextEditor/extensions/toc-node/TocNodeView.vue +1 -1
  29. package/package.json +2 -1
  30. package/src/components/BottomSheet/BottomSheet.vue +6 -0
  31. package/src/components/PageHeader/PageHeaderMobile.vue +1 -1
  32. package/src/components/Tree/Tree.api.md +4 -4
  33. package/src/components/Tree/Tree.md +40 -14
  34. package/src/components/Tree/Tree.vue +92 -51
  35. package/src/components/Tree/index.ts +1 -0
  36. package/src/components/Tree/types.ts +27 -10
  37. package/src/components/Tree/useWarnLegacyExpanded.ts +64 -0
  38. package/src/molecules/editor/extensions/image-group/ImageGroupGrid.vue +1 -1
  39. package/src/molecules/editor/extensions/image-group/ImageGroupGridCell.vue +2 -2
  40. package/src/molecules/editor/extensions/image-group/ImageGroupNodeView.vue +2 -2
  41. package/src/molecules/editor/extensions/toc-node/TocNodeView.vue +1 -1
  42. package/src/utils/warnDeprecated.ts +6 -1
  43. package/tailwind/content.js +1 -0
  44. package/tailwind/iconPackPlugin.js +6 -1
  45. package/tailwind/preset.js +7 -0
  46. package/experimental/Calendar/CalendarMonthStack.vue +0 -182
@@ -12,6 +12,12 @@
12
12
  type: 'CalendarEvent[]',
13
13
  default: '[]'
14
14
  },
15
+ {
16
+ name: 'loading',
17
+ description: 'Whether the events for the visible range are still on their way.\n\nOnly the Agenda reads it, and only to tell an empty list apart from one that\nhas not arrived: a grid with nothing in it still draws the days, where a list\nwith nothing in it is a blank panel, and saying "nothing on" of a range still\nbeing fetched is saying something that may not be true.',
18
+ required: false,
19
+ type: 'boolean'
20
+ },
15
21
  {
16
22
  name: 'config',
17
23
  description: 'Behavior overrides, merged over the defaults.',
@@ -23,7 +29,7 @@
23
29
  name: 'onClick',
24
30
  description: 'Replaces the default single-click behavior (opening the event\npopover) with your own handler.',
25
31
  required: false,
26
- type: '((data: { e: MouseEvent; calendarEvent: CalendarEvent; }) => void)'
32
+ type: '((data: { e: MouseEvent | KeyboardEvent; calendarEvent: CalendarEvent; }) => void)'
27
33
  },
28
34
  {
29
35
  name: 'onDblClick',
@@ -48,12 +54,22 @@
48
54
  {
49
55
  name: 'event-popover-content',
50
56
  description: '',
51
- type: '{ calendarEvent: { [x: string]: unknown; id?: string | number | undefined; name?: string | number | '
57
+ type: '{ calendarEvent: CalendarEvent; date: Date; isEditMode: boolean; close: () => void; }'
58
+ },
59
+ {
60
+ name: 'event-description',
61
+ description: '',
62
+ type: '{ calendarEvent: CalendarEvent; date: Date; description: string; timing: CalendarRowTag | null; }'
63
+ },
64
+ {
65
+ name: 'event-suffix',
66
+ description: '',
67
+ type: '{ calendarEvent: CalendarEvent; date: Date; description: string; timing: CalendarRowTag | null; }'
52
68
  },
53
69
  {
54
- name: 'daily-header',
70
+ name: 'event-participant',
55
71
  description: '',
56
- type: '{ parseDateWithDay: any; currentDate: any; fullDay: any; }'
72
+ type: '{ calendarEvent: CalendarEvent; date: Date; description: string; timing: CalendarRowTag | null; }'
57
73
  }
58
74
  ]
59
75
 
@@ -1,6 +1,6 @@
1
1
  # Calendar
2
2
 
3
- A date and event view for schedules, with Month, Week, and Day modes.
3
+ A date and event view for schedules, with Month, Week, Day, and Agenda modes.
4
4
 
5
5
  > **Parked** — `Calendar` left the root export in `1.0.0` and now ships from
6
6
  > [`frappe-ui/experimental`](/docs/experimental) with its public API unchanged.
@@ -19,10 +19,9 @@ import type { CalendarEvent, CalendarConfig } from 'frappe-ui/experimental'
19
19
  ## Custom Header
20
20
 
21
21
  Pass a `#header` slot to replace the default toolbar. The slot receives the
22
- current title (`currentMonthYear`), the active view (`activeView`), the
23
- enabled view options (`enabledModes`), and navigation functions
24
- (`increment`, `decrement`, `updateActiveView`, `setCalendarDate`,
25
- `onMonthYearChange`).
22
+ current title (`currentMonthYear`), the active view (`activeView`), the enabled
23
+ view options (`enabledModes`), and navigation functions (`increment`,
24
+ `decrement`, `updateActiveView`, `setCalendarDate`, `onMonthYearChange`).
26
25
 
27
26
  <ComponentPreview name="Calendar-CustomHeader" csr="true" />
28
27
 
@@ -45,43 +44,86 @@ Each entry in `events` is a `CalendarEvent`:
45
44
  }
46
45
  ```
47
46
 
48
- An event runs from `fromDate fromTime` to `toDate toTime`, dates inclusive.
49
- One whose `toDate` is later than its `fromDate` spans those days: the Month
50
- view draws it as a single bar across them, and the Week view puts it in the
51
- all-day row. A timed event that crosses midnight but is shorter than a day
52
- (an evening running late) stays in the time grid, cut at midnight into a
53
- piece per day. A timed event ending at `00:00` stops as that day begins, so
54
- it does not occupy it. Dragging a spanning event moves both ends together.
47
+ An event runs from `fromDate fromTime` to `toDate toTime`, dates inclusive. One
48
+ whose `toDate` is later than its `fromDate` spans those days: the Month view
49
+ draws it as a single bar across them, and the Week view puts it in the all-day
50
+ row. A timed event that crosses midnight but is shorter than a day (an evening
51
+ running late) stays in the time grid, cut at midnight into a piece per day. A
52
+ timed event ending at `00:00` stops as that day begins, so it does not occupy
53
+ it. Dragging a spanning event moves both ends together.
55
54
 
56
55
  `isFullDay` events ignore their times and cover `fromDate`..`toDate` whole.
57
56
 
58
- The calendar keeps an internal copy of `events` and refreshes it when the
59
- prop changes. Edits made inside the calendar (create, drag, resize, delete)
60
- mutate the copy and come back through the `create`, `update`, and `delete`
61
- emits — persist them and refresh your source of truth from there.
57
+ The calendar keeps an internal copy of `events` and refreshes it when the prop
58
+ changes. Edits made inside the calendar (create, drag, resize, delete) mutate
59
+ the copy and come back through the `create`, `update`, and `delete` emits —
60
+ persist them and refresh your source of truth from there.
62
61
 
63
- `CalendarColorMap` exports the color palette (`amber`, `violet`, `pink`,
64
- `cyan`, `blue`, `orange`, `green`) with the CSS variables used per state, for
65
- building matching UI such as a color picker.
62
+ `CalendarColorMap` exports the color palette (`amber`, `violet`, `pink`, `cyan`,
63
+ `blue`, `orange`, `green`) with the CSS variables used per state, for building
64
+ matching UI such as a color picker.
66
65
 
67
66
  ## Month view
68
67
 
69
68
  The Month view is a strip of week rows covering the month in view, and it
70
- scrolls when the rows outgrow the calendar's height. The arrows, Today, and
71
- the month picker move the month and scroll the strip to their date.
69
+ scrolls when the rows outgrow the calendar's height. The arrows, Today, and the
70
+ month picker move the month and scroll the strip to their date.
72
71
 
73
72
  Each row is as tall as its busiest day needs. Multi-day events run as bars
74
- across the top of the row; single-day events sit beneath them in their
75
- cells with the title wrapping to a second line, so every event is shown —
76
- there is no "n more".
77
-
78
- Below the `sm` breakpoint the days stack instead: a row per day, a heading
79
- where each month begins, and a week strip above to keep your place. Clicking a date number in either layout
80
- opens that day in the Day view.
81
-
82
- `rangeChange` reports the strip's full extent for the Month view — the
83
- padding days of the first and last weeks included — so a data source that
84
- fetches by range has events for every cell.
73
+ across the top of the row; single-day events sit beneath them in their cells
74
+ with the title wrapping to a second line, so every event is shown — there is no
75
+ "n more".
76
+
77
+ Below the `sm` breakpoint it is the same grid a size down: shorter pills, a
78
+ tighter lane, and a cell that shows what fits in it with a `+n` count for the
79
+ rest — a full week trades its last lane of bars for those counts. Clicking a
80
+ date number at either width opens that day in the Day view.
81
+
82
+ `rangeChange` reports the strip's full extent for the Month view — the padding
83
+ days of the first and last weeks included — so a data source that fetches by
84
+ range has events for every cell.
85
+
86
+ ## Agenda view
87
+
88
+ The Agenda view is three months as a list of days, each day a card: its name,
89
+ its date, how much is on it once that is more than one thing, then its events as
90
+ rows. A day with nothing on it is not listed — an empty row says nothing the
91
+ dates either side of it do not, and neither does a line counting how many were
92
+ skipped. Cards are grouped under the week they fall in, which replaces the month
93
+ dividers a flat list needs. Today's card carries a dot before its name; it, the
94
+ day before it and the day after say so in words beside their dates, and a day
95
+ already spent fades its header. An event that has ended is dimmed, the way a
96
+ pill in the grid is once its time has passed. An event under way says so with
97
+ its own tag.
98
+
99
+ The window covers the month in view and the two after it, padded out to whole
100
+ weeks at either end the way the Month view's strip is — the list groups its days
101
+ under the week they fall in, and a week is either listed or it is not. A single
102
+ month would be 31 days on the 1st and one day on the 31st, which is why it is
103
+ three. The arrows step a month at a time, so each move keeps two thirds of what
104
+ was on screen, and `rangeChange` reports exactly the span listed, padding
105
+ included, so a data source fetching by range agrees with it. The header names
106
+ the three months themselves, not the days the padding reaches into.
107
+
108
+ Rows have the room a grid pill does not, so they carry a description line and
109
+ tags. `Calendar` fills in the description itself — where the event is, and which
110
+ day of a stay the row is (`Day 2/3`) — and marks a draft as a pill does, an
111
+ outline in its colour on the plain ground rather than a tinted block. Where it stands against the clock
112
+ reads right after that: `Now` in blue while it runs and `Soon` in amber in the
113
+ hour before it, and nothing beyond that: an hour count would only restate the
114
+ time written beside it. Who is coming (`participant`) stands at the row's far
115
+ end, the one edge a card aligns on other than the time column, so the counts of
116
+ a day's rows line up — and the `#event-description` and `#event-suffix` slots
117
+ let you say the rest.
118
+
119
+ Drawn narrower than 640px — a phone, or a pane a sidebar has squeezed — the list
120
+ stacks. A day's name becomes a band over its rows rather than a column beside
121
+ them, and each row takes two lines: the title, its tags and who is coming on the
122
+ first, the time and the description on the second. The list measures its own
123
+ width for this, so a calendar in a narrow pane reads the same way a phone's
124
+ does.
125
+
126
+ <ComponentPreview name="Calendar-Agenda" csr="true" />
85
127
 
86
128
  ## Config
87
129
 
@@ -90,7 +132,7 @@ defaults:
90
132
 
91
133
  ```ts
92
134
  {
93
- defaultMode: 'Month', // 'Day' | 'Week' | 'Month'
135
+ defaultMode: 'Month', // 'Day' | 'Week' | 'Month' | 'Agenda'
94
136
  disableModes: [], // views removed from the view switcher
95
137
  isEditMode: false, // create / drag / resize / delete
96
138
  enableShortcuts: true, // keyboard shortcuts (below)
@@ -108,8 +150,8 @@ defaults:
108
150
 
109
151
  ## Keyboard shortcuts
110
152
 
111
- With `enableShortcuts` on: `m` / `w` / `d` switch views, `t` jumps to today,
112
- `←` / `→` navigate, and `Delete` removes the event whose popover is open
153
+ With `enableShortcuts` on: `m` / `w` / `d` / `a` switch views, `t` jumps to
154
+ today, `←` / `→` navigate, and `Delete` removes the event whose popover is open
113
155
  (edit mode only).
114
156
 
115
157
  They stay out of the way of whatever is on top: nothing fires while a field has
@@ -119,17 +161,26 @@ focus, or while a dialog, popover, menu or select is open anywhere on the page.
119
161
 
120
162
  By default, a single click on an event opens its detail popover and a double
121
163
  click opens the edit modal (edit mode only). Clicking an empty cell opens the
122
- new-event modal in edit mode. Each behavior is replaceable with the
123
- `onClick`, `onDblClick`, and `onCellClick` callback props — passing one turns
124
- the default off for that interaction.
164
+ new-event modal in edit mode. Each behavior is replaceable with the `onClick`,
165
+ `onDblClick`, and `onCellClick` callback props — passing one turns the default
166
+ off for that interaction.
125
167
 
126
168
  The popover's content is replaceable with the `#event-popover-content` slot,
127
169
  which receives `{ calendarEvent, date, isEditMode, close }`.
128
170
 
129
- `CalendarActiveEvent` exports the ref holding the id of the event whose
130
- popover is open. Set it from outside to highlight an event, or clear it with
131
- an empty string. The ref is module-level: every `<Calendar>` on the page
132
- shares it.
171
+ The Agenda's rows take three more slots: `#event-description` for the line
172
+ under the title, `#event-suffix` for the tags beside it, and
173
+ `#event-participant` for the row's far end, where the event's `participant`
174
+ string is shown unless you fill it — with faces in front of the count, say. All
175
+ three receive `{ calendarEvent, date, description, timing }`, where
176
+ `description` and `timing` are what the calendar derived itself, so you can add
177
+ to them rather than work them out again. The suffix has no tags of its own to
178
+ hand you — it is yours to fill. Grid pills have no room for any of these and
179
+ ignore the slots.
180
+
181
+ `CalendarActiveEvent` exports the ref holding the id of the event whose popover
182
+ is open. Set it from outside to highlight an event, or clear it with an empty
183
+ string. The ref is module-level: every `<Calendar>` on the page shares it.
133
184
 
134
185
  ## Template ref
135
186
 
@@ -2,7 +2,13 @@
2
2
  <!-- No overflow-hidden here: each view scrolls itself, and clipping at the
3
3
  root only cut the focus outline off the header's buttons. -->
4
4
  <div class="flex h-full flex-col">
5
+ <!-- v-if on the slot, not a fallback inside it: `renderSlot` falls back whenever a
6
+ slot renders nothing, so a consumer passing an empty <template #header /> — a
7
+ phone, which draws its own — got this header anyway and had to cover it with an
8
+ element that renders nothing. Asking whether the slot was passed at all is the
9
+ question that was being asked. -->
5
10
  <slot
11
+ v-if="$slots.header"
6
12
  name="header"
7
13
  v-bind="{
8
14
  currentMonthYear,
@@ -17,7 +23,8 @@
17
23
  onMonthYearChange,
18
24
  selectedMonthDate,
19
25
  }"
20
- >
26
+ />
27
+ <template v-else>
21
28
  <div class="mb-2 flex justify-between">
22
29
  <!-- left side -->
23
30
  <!-- Year, Month -->
@@ -63,7 +70,7 @@
63
70
  />
64
71
  </div>
65
72
  </div>
66
- </slot>
73
+ </template>
67
74
 
68
75
  <CalendarMonthly
69
76
  v-if="activeView === 'Month'"
@@ -96,17 +103,32 @@
96
103
  :current-date="selectedDay"
97
104
  :config="overrideConfig"
98
105
  >
99
- <template #header="{ parseDateWithDay, currentDate, fullDay }">
100
- <slot
101
- name="daily-header"
102
- v-bind="{ parseDateWithDay, currentDate, fullDay }"
103
- />
104
- </template>
105
106
  <template #event-popover-content="slotProps">
106
107
  <slot name="event-popover-content" v-bind="slotProps" />
107
108
  </template>
108
109
  </CalendarDaily>
109
110
 
111
+ <CalendarAgenda
112
+ v-else-if="activeView === 'Agenda'"
113
+ :events="events"
114
+ :anchor="agendaAnchor"
115
+ :jump="agendaJump"
116
+ :loading="loading"
117
+ >
118
+ <template #event-description="slotProps">
119
+ <slot name="event-description" v-bind="slotProps" />
120
+ </template>
121
+ <template #event-suffix="slotProps">
122
+ <slot name="event-suffix" v-bind="slotProps" />
123
+ </template>
124
+ <template #event-participant="slotProps">
125
+ <slot name="event-participant" v-bind="slotProps" />
126
+ </template>
127
+ <template #event-popover-content="slotProps">
128
+ <slot name="event-popover-content" v-bind="slotProps" />
129
+ </template>
130
+ </CalendarAgenda>
131
+
110
132
  <NewEventModal
111
133
  v-if="showEventModal"
112
134
  v-model="showEventModal"
@@ -123,7 +145,6 @@ import {
123
145
  ref,
124
146
  watch,
125
147
  nextTick,
126
- type Component,
127
148
  } from 'vue'
128
149
  import { Button } from '#components/Button'
129
150
  import { TabButtons } from '#components/TabButtons'
@@ -136,18 +157,18 @@ import {
136
157
  } from './calendarUtils'
137
158
  import { dayjs } from '#utils/dayjs'
138
159
  import { isTargetEditable } from '#composables/useKeyboardShortcut'
139
- import DayIcon from './Icon/DayIcon.vue'
140
- import WeekIcon from './Icon/WeekIcon.vue'
141
- import MonthIcon from './Icon/MonthIcon.vue'
142
160
  import DatePicker from '#components/DatePicker/DatePicker.vue'
143
161
  import CalendarMonthly from './CalendarMonthly.vue'
144
162
  import CalendarWeekly from './CalendarWeekly.vue'
145
163
  import CalendarDaily from './CalendarDaily.vue'
164
+ import CalendarAgenda from './CalendarAgenda.vue'
146
165
  import NewEventModal from './NewEventModal.vue'
147
166
  import useEventModal from './composables/useEventModal'
148
167
  import { isAnyPopoverOpen } from './useEventBase'
149
168
  import { stripPlacement } from './eventSpan'
150
169
  import { stripRange } from './monthStrip'
170
+ import { activeEvent } from './composables/useCalendarData'
171
+ import { agendaMonths, agendaRange } from './agendaDays'
151
172
  import {
152
173
  ACTIVE_VIEW_KEY,
153
174
  CALENDAR_ACTIONS_KEY,
@@ -173,6 +194,9 @@ const emit = defineEmits<{
173
194
  ]
174
195
  }>()
175
196
 
197
+ /** Bumped by setCalendarDate; the Agenda scrolls back to its anchor on every bump. */
198
+ const agendaJump = ref(0)
199
+
176
200
  const defaultConfig: CalendarConfig = {
177
201
  scrollToHour: 15,
178
202
  disableModes: [],
@@ -266,24 +290,17 @@ function handleShortcuts(e: KeyboardEvent) {
266
290
  if (isOverlayOpen()) return
267
291
  if (isTargetEditable(e)) return
268
292
 
269
- if (e.key.toLowerCase() === 'm') {
270
- activeView.value = 'Month'
271
- }
272
- if (e.key.toLowerCase() === 'w') {
273
- activeView.value = 'Week'
274
- }
275
- if (e.key.toLowerCase() === 'd') {
276
- activeView.value = 'Day'
277
- }
278
- if (e.key.toLowerCase() === 't') {
279
- setCalendarDate()
280
- }
281
- if (e.key === 'ArrowLeft') {
282
- decrement()
283
- }
284
- if (e.key === 'ArrowRight') {
285
- increment()
286
- }
293
+ const view = viewShortcuts[e.key.toLowerCase()]
294
+ if (view) activeView.value = view
295
+ if (e.key.toLowerCase() === 't') setCalendarDate()
296
+ if (e.key === 'ArrowLeft') decrement()
297
+ if (e.key === 'ArrowRight') increment()
298
+ }
299
+ const viewShortcuts: Record<string, CalendarMode> = {
300
+ m: 'Month',
301
+ w: 'Week',
302
+ a: 'Agenda',
303
+ d: 'Day',
287
304
  }
288
305
 
289
306
  provide(ACTIVE_VIEW_KEY, activeView)
@@ -388,6 +405,9 @@ function handleCellClick(
388
405
  return
389
406
  }
390
407
 
408
+ // Clicking the list or the grid itself is how you let go of the event.
409
+ activeEvent.value = ''
410
+
391
411
  const data: CalendarCellClickData = {
392
412
  e,
393
413
  view: activeView.value,
@@ -406,14 +426,21 @@ function handleCellClick(
406
426
  type CalendarActionOption = {
407
427
  label: CalendarMode
408
428
  value: CalendarMode
409
- iconLeft: Component
429
+ iconLeft: string
410
430
  }
411
431
 
412
- // Calendar View Options
432
+ // Calendar View Options.
433
+ //
434
+ // Named lucide icons rather than four SVGs of the library's own: the set draws each
435
+ // view as the shape of what it lays out — one pane, columns, a grid, a stack of
436
+ // rows. Being lucide they also carry the same weight and
437
+ // optical size as every other icon on the page, which hand-drawn glyphs at a fixed
438
+ // 16px did not.
413
439
  const actionOptions: CalendarActionOption[] = [
414
- { label: 'Day', value: 'Day', iconLeft: DayIcon },
415
- { label: 'Week', value: 'Week', iconLeft: WeekIcon },
416
- { label: 'Month', value: 'Month', iconLeft: MonthIcon },
440
+ { label: 'Day', value: 'Day', iconLeft: 'lucide-square-square' },
441
+ { label: 'Week', value: 'Week', iconLeft: 'lucide-columns-3' },
442
+ { label: 'Month', value: 'Month', iconLeft: 'lucide-grid-3x3' },
443
+ { label: 'Agenda', value: 'Agenda', iconLeft: 'lucide-rows-3' },
417
444
  ]
418
445
  let enabledModes = actionOptions.filter(
419
446
  (mode) => !overrideConfig.disableModes.includes(mode.value),
@@ -458,16 +485,19 @@ let date = ref(
458
485
  )
459
486
  let selectedDay = computed(() => currentMonthDates.value[date.value])
460
487
 
488
+ /**
489
+ * The day the calendar is on, whichever view it is in: the one `date` points
490
+ * at, which every view moves — a step of the month or the week lands it on the
491
+ * first day in the period, a step of the day on the next one, and a date the
492
+ * host sets on that date. Reported the same way in every view so a host's mini
493
+ * month circles the day the reader picked. It used to answer the Month view
494
+ * with 1 and the Week view with the week's Sunday, and a mini month handed
495
+ * those circled the 1st, or the Sunday, while the calendar sat on the 11th the
496
+ * reader had just clicked in it.
497
+ */
461
498
  function computeCurrentDay(): number | null {
462
- if (activeView.value === 'Week') {
463
- const weekDates = datesInWeeks.value[week.value] || []
464
- return weekDates[0] ? weekDates[0].getDate() : null
465
- }
466
- if (activeView.value === 'Day') {
467
- const day = selectedDay.value
468
- return day ? new Date(day).getDate() : null
469
- }
470
- return 1
499
+ const day = selectedDay.value
500
+ return day ? new Date(day).getDate() : null
471
501
  }
472
502
 
473
503
  let currentDay = ref(computeCurrentDay())
@@ -500,8 +530,14 @@ const incrementClickEvents: Record<CalendarMode, () => void> = {
500
530
  Month: incrementMonth,
501
531
  Week: incrementWeek,
502
532
  Day: incrementDay,
533
+ // The Agenda's window is anchored on a month and covers three, so its arrows
534
+ // step one — each move keeps two thirds of what was on screen.
535
+ Agenda: incrementMonth,
503
536
  }
504
537
 
538
+ /** The day the Agenda is anchored on: whatever day the calendar is sitting on. */
539
+ const agendaAnchor = computed(() => selectedDay.value ?? new Date())
540
+
505
541
  // decrementMonth lands on the month's last day, which is right for stepping
506
542
  // back a day across a month edge but scrolls the Month strip to its bottom;
507
543
  // the Month view's own arrow lands on the first day, as the other arrow does.
@@ -515,6 +551,9 @@ const decrementClickEvents: Record<CalendarMode, () => void> = {
515
551
  Month: decrementMonthView,
516
552
  Week: decrementWeek,
517
553
  Day: decrementDay,
554
+ // Lands on the 1st rather than the last day, which is where the window's
555
+ // first month wants to start reading.
556
+ Agenda: decrementMonthView,
518
557
  }
519
558
 
520
559
  function incrementMonth() {
@@ -666,6 +705,18 @@ const currentMonthYear = computed(() => {
666
705
  }
667
706
  }
668
707
 
708
+ // The Agenda is anchored on whole months, so it names them rather than the
709
+ // dates its ends happen to land on — `agendaMonths`, not the listed range,
710
+ // which pads to whole weeks and would have the title naming a month for the
711
+ // sake of the two days of it the first week reaches back into.
712
+ if (activeView.value === 'Agenda') {
713
+ const { start, end } = agendaMonths(agendaAnchor.value)
714
+ const short = (d: Date) => monthList[d.getMonth()].slice(0, 3)
715
+ return start.getFullYear() === end.getFullYear()
716
+ ? `${short(start)} – ${short(end)} ${end.getFullYear()}`
717
+ : `${short(start)} ${start.getFullYear()} – ${short(end)} ${end.getFullYear()}`
718
+ }
719
+
669
720
  // Non-week views or empty week fallback
670
721
  if (activeView.value !== 'Week')
671
722
  return formatMonthYear(currentMonth.value, currentYear.value)
@@ -695,6 +746,10 @@ function isCurrentMonthDate(date?: Date) {
695
746
  function setCalendarDate(d?: Date | string) {
696
747
  const dt = d ? new Date(d) : new Date()
697
748
  if (dt.toString() === 'Invalid Date') return
749
+ // Counted, not compared: the Agenda reads this to know it has been sent somewhere,
750
+ // which is not the same as its anchor changing — Today, pressed halfway down the
751
+ // list on the day it is already anchored on, moves nothing and means everything.
752
+ agendaJump.value++
698
753
  currentYear.value = dt.getFullYear()
699
754
  currentMonth.value = dt.getMonth()
700
755
  currentDate.value = dt
@@ -735,6 +790,17 @@ function getVisibleRange() {
735
790
  }
736
791
  }
737
792
 
793
+ // The Agenda reports what it actually lists — three months from where it is
794
+ // anchored, less the days already spent — so a consumer's fetch window and
795
+ // its "new event" anchor agree with what is on screen.
796
+ if (activeView.value === 'Agenda') {
797
+ const { start, end } = agendaRange(agendaAnchor.value)
798
+ return {
799
+ startDate: dayjs(start).format('YYYY-MM-DD'),
800
+ endDate: dayjs(end).format('YYYY-MM-DD'),
801
+ }
802
+ }
803
+
738
804
  // The Month strip's first and last weeks reach into the neighbouring months.
739
805
  const range = stripRange(currentMonth.value, currentYear.value)
740
806
  return {