@open-webapp/calendar 0.3.0 → 0.4.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
@@ -174,10 +174,16 @@ Built-in copy (overridable only through the slots above):
174
174
  - Loading: "Loading calendar…".
175
175
  - Empty: "No matching events in the next 30 days".
176
176
 
177
+ #### Footer calendar selection
178
+
179
+ One footer combobox opens a shared searchable dropdown, with calendars grouped under account headings in multi-source use. Each account has its own Clear (primary, else first calendar) and filtered Select all, which adds matching calendars while retaining existing selections. Actions affect only their account; its last selected calendar cannot be deselected, including a non-primary selection.
180
+
177
181
  ### CalendarSelector props
178
182
 
179
183
  `calendars: CalendarInfo[]`, `selectedIds: string[]`, `onChange(ids)`, `heading?: string` (account heading in multi-source use). It has no slot props or `popupName`. At least one calendar stays selected (deselecting the last one is ignored). "Clear" resets to the primary calendar. Built-in copy: "Search calendars", "Select all", "Clear", "Done", "No calendars found", "No calendars available".
180
184
 
185
+ The exported standalone selector keeps its single-list props and behavior; Select all adds only search matches while retaining existing selections.
186
+
181
187
  ### Host CSS
182
188
 
183
189
  `styles.css` is self-contained (plain colors, dark mode via `prefers-color-scheme`). It requires no host CSS variables. Variables it reads, all with defaults:
@@ -188,6 +194,10 @@ Built-in copy (overridable only through the slots above):
188
194
 
189
195
  The Unaccepted count badge uses `.calendar-rsvp-chip-badge`, with light and dark styling included in `styles.css`.
190
196
 
197
+ ## 0.4.0 (BREAKING pre-1.0 minor)
198
+
199
+ - CalendarPane replaces per-account footer triggers with one shared dropdown grouped by account. Hosts relying on multiple footer triggers must adapt. Account-local selection rules and the exported standalone CalendarSelector remain compatible.
200
+
191
201
  ## 0.3.0 (minor, additive)
192
202
 
193
203
  - Adds the Unaccepted exclusive shortcut after Accepted, Maybe, and Declined, with local selection restoration and default fallback as described above.
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { filterByRsvp, groupByDay, getOrganizerLabel, formatDayHeader, isEventCurrent, findNextUpcomingEvent, getCalendarEventKey, DEFAULT_RSVP_STATUSES, } from '../core/events.js';
4
- import { CalendarSelector } from './CalendarSelector.js';
4
+ import { GroupedCalendarSelector } from './CalendarSelector.js';
5
5
  function toggleRsvpStatus(selection, status) {
6
6
  const next = new Set(selection);
7
7
  if (next.has(status)) {
@@ -343,7 +343,13 @@ export function CalendarPane(props) {
343
343
  openEventPopup(event.htmlLink, popupRef, popupName);
344
344
  }
345
345
  }, children: _jsx("span", { className: "calendar-event-title", children: event.summary }) })) : (_jsx("span", { className: "calendar-event-title", children: event.summary })), props.renderEventAction?.(event)] }), showCalendarLabel && (_jsxs("span", { className: "calendar-event-calendar", children: [_jsx("span", { className: "calendar-event-calendar-dot", style: { backgroundColor: calendar.backgroundColor } }), calendar.summary] })), showOrganizerLabel && (_jsxs("span", { className: "calendar-event-organizer", children: [multi && (_jsx("span", { className: "calendar-event-source-dot", style: { backgroundColor: sourceColor(event.sourceId) } })), organizerText] }))] }, eventKey));
346
- })] }, group.dateKey))), loading && _jsx("p", { className: "calendar-empty-state-text", children: "Loading calendar\u2026" }), filteredEvents.length === 0 && !loading && !error && needsScopeSources.length === 0 && _jsx("p", { className: "calendar-empty-state-text", children: emptyStateText })] }))] }), showCalendarSelector && (_jsx("footer", { className: "calendar-pane-footer", children: calendarSources.map((s) => (_jsx(CalendarSelector, { calendars: controller.getCalendars(s.id), selectedIds: controller.getSelectedIds(s.id), onChange: (ids) => {
347
- void controller.setSelectedIds(s.id, ids);
348
- }, heading: multi ? s.email : undefined }, s.id))) }))] }));
346
+ })] }, group.dateKey))), loading && _jsx("p", { className: "calendar-empty-state-text", children: "Loading calendar\u2026" }), filteredEvents.length === 0 && !loading && !error && needsScopeSources.length === 0 && _jsx("p", { className: "calendar-empty-state-text", children: emptyStateText })] }))] }), showCalendarSelector && (_jsx("footer", { className: "calendar-pane-footer", children: _jsx(GroupedCalendarSelector, { groups: calendarSources.map((s) => ({
347
+ id: s.id,
348
+ calendars: controller.getCalendars(s.id),
349
+ selectedIds: controller.getSelectedIds(s.id),
350
+ onChange: (ids) => {
351
+ void controller.setSelectedIds(s.id, ids);
352
+ },
353
+ heading: multi ? s.email : undefined,
354
+ })) }) }))] }));
349
355
  }
@@ -7,4 +7,10 @@ interface CalendarSelectorProps {
7
7
  heading?: string;
8
8
  }
9
9
  export declare function CalendarSelector({ calendars, selectedIds, onChange, heading }: CalendarSelectorProps): import("react").JSX.Element;
10
+ /** Internal pane renderer; intentionally not re-exported from the package barrel. */
11
+ export declare function GroupedCalendarSelector({ groups }: {
12
+ groups: (CalendarSelectorProps & {
13
+ id: string;
14
+ })[];
15
+ }): import("react").JSX.Element;
10
16
  export {};
@@ -1,7 +1,11 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useId, useRef, useState } from 'react';
3
3
  import { filterCalendarsByName, summarizeCalendarSelection } from '../core/events.js';
4
4
  export function CalendarSelector({ calendars, selectedIds, onChange, heading }) {
5
+ return _jsx(GroupedCalendarSelector, { groups: [{ id: '', calendars, selectedIds, onChange, heading }] });
6
+ }
7
+ /** Internal pane renderer; intentionally not re-exported from the package barrel. */
8
+ export function GroupedCalendarSelector({ groups }) {
5
9
  const [open, setOpen] = useState(false);
6
10
  const [query, setQuery] = useState('');
7
11
  const [activeIndex, setActiveIndex] = useState(0);
@@ -9,8 +13,9 @@ export function CalendarSelector({ calendars, selectedIds, onChange, heading })
9
13
  const searchRef = useRef(null);
10
14
  const rootRef = useRef(null);
11
15
  const listboxId = useId();
12
- const visibleCalendars = filterCalendarsByName(calendars, query);
13
- const primaryId = calendars.find((calendar) => calendar.primary)?.id ?? calendars[0]?.id;
16
+ const visibleGroups = groups.map((group) => ({ ...group, visible: filterCalendarsByName(group.calendars, query) }));
17
+ const visibleCalendars = visibleGroups.flatMap((group) => group.visible.map((calendar) => ({ calendar, group })));
18
+ const optionId = (sourceId, calendarId) => `${listboxId}-${encodeURIComponent(JSON.stringify([sourceId, calendarId]))}`;
14
19
  useEffect(() => {
15
20
  if (open)
16
21
  searchRef.current?.focus();
@@ -37,7 +42,8 @@ export function CalendarSelector({ calendars, selectedIds, onChange, heading })
37
42
  setOpen(false);
38
43
  buttonRef.current?.focus();
39
44
  }
40
- function toggle(id) {
45
+ function toggle(group, id) {
46
+ const { selectedIds, onChange } = group;
41
47
  if (selectedIds.includes(id)) {
42
48
  if (selectedIds.length === 1)
43
49
  return;
@@ -46,11 +52,13 @@ export function CalendarSelector({ calendars, selectedIds, onChange, heading })
46
52
  }
47
53
  onChange([...selectedIds, id]);
48
54
  }
49
- function selectAll() {
50
- const visibleIds = visibleCalendars.map((calendar) => calendar.id);
55
+ function selectAll(group) {
56
+ const { selectedIds, onChange } = group;
57
+ const visibleIds = group.visible.map((calendar) => calendar.id);
51
58
  onChange([...selectedIds, ...visibleIds.filter((id) => !selectedIds.includes(id))]);
52
59
  }
53
- function clear() {
60
+ function clear({ calendars, onChange }) {
61
+ const primaryId = calendars.find((calendar) => calendar.primary)?.id ?? calendars[0]?.id;
54
62
  onChange(primaryId ? [primaryId] : []);
55
63
  }
56
64
  function handleListboxKeyDown(event) {
@@ -75,16 +83,19 @@ export function CalendarSelector({ calendars, selectedIds, onChange, heading })
75
83
  event.preventDefault();
76
84
  const activeCalendar = visibleCalendars[activeIndex];
77
85
  if (activeCalendar)
78
- toggle(activeCalendar.id);
86
+ toggle(activeCalendar.group, activeCalendar.calendar.id);
79
87
  }
80
88
  }
81
- const label = summarizeCalendarSelection(selectedIds, calendars);
89
+ const label = groups.length === 1
90
+ ? summarizeCalendarSelection(groups[0].selectedIds, groups[0].calendars)
91
+ : `${groups.reduce((count, group) => count + group.selectedIds.length, 0)} calendars selected`;
92
+ const activeCalendar = visibleCalendars[activeIndex];
82
93
  return (_jsxs("div", { className: "calendar-selector", ref: rootRef, children: [_jsx("button", { ref: buttonRef, type: "button", role: "combobox", "aria-label": label, "aria-expanded": open, "aria-controls": listboxId, "aria-haspopup": "listbox", onClick: () => {
83
94
  setOpen((isOpen) => !isOpen);
84
95
  setQuery('');
85
96
  setActiveIndex(0);
86
- }, children: label }), open && (_jsxs("div", { className: "calendar-selector-dropdown calendar-selector-dropdown--upward", onKeyDown: handleListboxKeyDown, children: [heading && (_jsx("div", { role: "heading", "aria-level": 3, className: "calendar-selector-heading", children: heading })), _jsx("input", { ref: searchRef, type: "search", role: "searchbox", "aria-label": "Search calendars", value: query, onChange: (event) => {
97
+ }, children: label }), open && (_jsxs("div", { className: "calendar-selector-dropdown calendar-selector-dropdown--upward", onKeyDown: handleListboxKeyDown, children: [groups.length === 1 && groups[0].heading && (_jsx("div", { role: "heading", "aria-level": 3, className: "calendar-selector-heading", children: groups[0].heading })), _jsx("input", { ref: searchRef, type: "search", role: "searchbox", "aria-label": "Search calendars", value: query, onChange: (event) => {
87
98
  setQuery(event.target.value);
88
99
  setActiveIndex(0);
89
- } }), _jsxs("div", { className: "calendar-selector-actions", children: [_jsx("button", { type: "button", onClick: selectAll, children: "Select all" }), _jsx("button", { type: "button", onClick: clear, children: "Clear" }), _jsx("button", { type: "button", onClick: close, children: "Done" })] }), _jsx("div", { id: listboxId, role: "listbox", "aria-multiselectable": "true", "aria-activedescendant": visibleCalendars[activeIndex] ? `${listboxId}-${visibleCalendars[activeIndex].id}` : undefined, tabIndex: -1, children: calendars.length === 0 ? (_jsx("p", { children: "No calendars available" })) : visibleCalendars.length === 0 ? (_jsx("p", { children: "No calendars found" })) : (visibleCalendars.map((calendar, index) => (_jsxs("button", { type: "button", id: `${listboxId}-${calendar.id}`, role: "option", title: calendar.summary, "aria-selected": selectedIds.includes(calendar.id), "data-active": index === activeIndex || undefined, onClick: () => toggle(calendar.id), children: [_jsx("input", { type: "checkbox", className: "calendar-selector-checkbox", checked: selectedIds.includes(calendar.id), readOnly: true, tabIndex: -1, "aria-hidden": "true" }), _jsx("span", { "aria-hidden": "true", className: "calendar-selector-dot", "data-testid": `calendar-color-${calendar.id}`, style: { backgroundColor: calendar.backgroundColor } }), _jsx("span", { className: "calendar-selector-name", title: calendar.summary, children: calendar.summary })] }, calendar.id)))) })] }))] }));
100
+ } }), _jsxs("div", { className: "calendar-selector-actions", children: [groups.length === 1 && _jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => selectAll(visibleGroups[0]), children: "Select all" }), _jsx("button", { type: "button", onClick: () => clear(groups[0]), children: "Clear" })] }), _jsx("button", { type: "button", onClick: close, children: "Done" })] }), _jsx("div", { id: listboxId, role: "listbox", "aria-multiselectable": "true", "aria-activedescendant": activeCalendar ? optionId(activeCalendar.group.id, activeCalendar.calendar.id) : undefined, tabIndex: -1, children: visibleGroups.map((group) => (_jsxs("div", { role: groups.length > 1 ? 'group' : undefined, "aria-label": group.heading, children: [groups.length > 1 && _jsxs(_Fragment, { children: [_jsx("div", { role: "heading", "aria-level": 3, className: "calendar-selector-heading", children: group.heading }), _jsxs("div", { className: "calendar-selector-actions", children: [_jsx("button", { type: "button", onClick: () => selectAll(group), children: "Select all" }), _jsx("button", { type: "button", onClick: () => clear(group), children: "Clear" })] })] }), group.calendars.length === 0 ? (_jsx("p", { children: "No calendars available" })) : group.visible.length === 0 ? (_jsx("p", { children: "No calendars found" })) : (group.visible.map((calendar) => (_jsxs("button", { type: "button", id: optionId(group.id, calendar.id), role: "option", title: calendar.summary, "aria-selected": group.selectedIds.includes(calendar.id), "data-active": (activeCalendar?.group.id === group.id && activeCalendar.calendar.id === calendar.id) || undefined, onClick: () => toggle(group, calendar.id), children: [_jsx("input", { type: "checkbox", className: "calendar-selector-checkbox", checked: group.selectedIds.includes(calendar.id), readOnly: true, tabIndex: -1, "aria-hidden": "true" }), _jsx("span", { "aria-hidden": "true", className: "calendar-selector-dot", "data-testid": `calendar-color-${calendar.id}`, style: { backgroundColor: calendar.backgroundColor } }), _jsx("span", { className: "calendar-selector-name", title: calendar.summary, children: calendar.summary })] }, optionId(group.id, calendar.id)))))] }, group.id))) })] }))] }));
90
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-webapp/calendar",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",