@open-webapp/calendar 0.2.1 → 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
@@ -158,6 +158,14 @@ Call at top level of a vitest file; it registers a `describe('StorageAdapter con
158
158
  | `onClose?` | `() => void` | |
159
159
  | `popupName?` | `string` | `window.open` name for the event popup (default `notesdiary-cal-event`) |
160
160
 
161
+ #### RSVP chips
162
+
163
+ The chips appear in order: Accepted, Maybe, Declined, Unaccepted. Accepted, Maybe, and Declined toggle individual statuses; an empty selection falls back to `DEFAULT_RSVP_STATUSES` (accepted, tentative, needsAction).
164
+
165
+ Unaccepted is an exclusive shortcut: clicking it selects only `needsAction`, saving the previous selection locally in the pane. Clicking it again restores that selection, or the defaults when no saved selection is available. Toggling another chip while exclusive mode is active applies that toggle to the saved selection (or defaults). An external nonexclusive selection clears the saved selection. Unaccepted is pressed only when the selection is exactly `needsAction`; a mixed selection containing `needsAction` does not press it.
166
+
167
+ The Unaccepted badge is always visible, including at zero. It counts `needsAction` events from the controller's selected-calendar, rolling ±30-day snapshot before RSVP filtering. Visible counts cap at `99+`; the chip's `aria-label` and `title` expose the full count.
168
+
161
169
  Built-in copy (overridable only through the slots above):
162
170
 
163
171
  - Not connected: "Not connected to Google Calendar." with "Connect Google Calendar".
@@ -166,10 +174,16 @@ Built-in copy (overridable only through the slots above):
166
174
  - Loading: "Loading calendar…".
167
175
  - Empty: "No matching events in the next 30 days".
168
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
+
169
181
  ### CalendarSelector props
170
182
 
171
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".
172
184
 
185
+ The exported standalone selector keeps its single-list props and behavior; Select all adds only search matches while retaining existing selections.
186
+
173
187
  ### Host CSS
174
188
 
175
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:
@@ -178,6 +192,18 @@ Built-in copy (overridable only through the slots above):
178
192
  - `--chip-row-height` (fallback `44px`; host may set it to match its chip row for sticky headers)
179
193
  - `--rsvp-bar` (set internally per RSVP status; do not override)
180
194
 
195
+ The Unaccepted count badge uses `.calendar-rsvp-chip-badge`, with light and dark styling included in `styles.css`.
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
+
201
+ ## 0.3.0 (minor, additive)
202
+
203
+ - Adds the Unaccepted exclusive shortcut after Accepted, Maybe, and Declined, with local selection restoration and default fallback as described above.
204
+ - Adds an always-visible Unaccepted count badge, including zero, computed before RSVP filtering from the controller's selected-calendar ±30-day snapshot. Counts above 99 display `99+`, with the full count in the chip's `aria-label` and `title`.
205
+ - Adds the `.calendar-rsvp-chip-badge` host styling hook. Public props and default RSVP statuses remain compatible.
206
+
181
207
  ## 0.2.1 (patch)
182
208
 
183
209
  Fixes only, no behavior contract change: `getState().loading` is now published while a sync pass is in flight; a selection-change (partial) pass no longer clears `deferred` set by an earlier full pass; `CalendarPane` tolerates controllers without `getState`/`hydrate`.
@@ -1,7 +1,17 @@
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
+ function toggleRsvpStatus(selection, status) {
6
+ const next = new Set(selection);
7
+ if (next.has(status)) {
8
+ next.delete(status);
9
+ }
10
+ else {
11
+ next.add(status);
12
+ }
13
+ return next.size === 0 ? new Set(DEFAULT_RSVP_STATUSES) : next;
14
+ }
5
15
  function useWindowWidth() {
6
16
  const [width, setWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1024);
7
17
  useEffect(() => {
@@ -23,16 +33,16 @@ const RSVP_CHIPS = [
23
33
  label: 'Maybe',
24
34
  icon: (_jsxs("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: [_jsx("path", { d: "M5.2 5.6C5.2 4.2 6.2 3.2 7.5 3.2C8.8 3.2 9.8 4.2 9.8 5.4C9.8 6.8 7.6 6.9 7.6 8.8", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("circle", { cx: "7.6", cy: "11.2", r: "0.9", fill: "currentColor" })] })),
25
35
  },
26
- {
27
- status: 'needsAction',
28
- label: 'Unaccepted',
29
- icon: (_jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("circle", { cx: "7.5", cy: "7.5", r: "4.3", stroke: "currentColor", strokeWidth: "1.4" }) })),
30
- },
31
36
  {
32
37
  status: 'declined',
33
38
  label: 'Declined',
34
39
  icon: (_jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("path", { d: "M4 4L11 11M11 4L4 11", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) })),
35
40
  },
41
+ {
42
+ status: 'needsAction',
43
+ label: 'Unaccepted',
44
+ icon: (_jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: _jsx("circle", { cx: "7.5", cy: "7.5", r: "4.3", stroke: "currentColor", strokeWidth: "1.4" }) })),
45
+ },
36
46
  ];
37
47
  const DEFAULT_POPUP_NAME = 'notesdiary-cal-event';
38
48
  const POPUP_W = 560;
@@ -122,6 +132,13 @@ export function CalendarPane(props) {
122
132
  const chipsRef = useRef(null);
123
133
  const popupRef = useRef(null);
124
134
  const [now, setNow] = useState(() => new Date());
135
+ const [savedSelection, setSavedSelection] = useState(null);
136
+ const isExclusive = selectedRsvpStatuses.size === 1 && selectedRsvpStatuses.has('needsAction');
137
+ useEffect(() => {
138
+ if (!isExclusive && savedSelection !== null)
139
+ setSavedSelection(null);
140
+ }, [isExclusive, savedSelection]);
141
+ const unacceptedCount = useMemo(() => events.filter((event) => event.selfResponseStatus === 'needsAction').length, [events]);
125
142
  const filteredEvents = useMemo(() => filterByRsvp(events, selectedRsvpStatuses), [events, selectedRsvpStatuses]);
126
143
  const calendarsByKey = useMemo(() => {
127
144
  const map = new Map();
@@ -143,19 +160,21 @@ export function CalendarPane(props) {
143
160
  const nextUpcomingEvent = findNextUpcomingEvent(filteredEvents, now);
144
161
  const selectedEventKey = currentEventKeys.values().next().value ??
145
162
  (nextUpcomingEvent ? getCalendarEventKey(nextUpcomingEvent) : undefined);
146
- function handleToggle(status) {
147
- const next = new Set(selectedRsvpStatuses);
148
- if (next.has(status)) {
149
- next.delete(status);
150
- if (next.size === 0) {
151
- props.onRsvpStatusesChange(new Set(DEFAULT_RSVP_STATUSES));
152
- return;
153
- }
163
+ function handleUnaccepted() {
164
+ if (isExclusive) {
165
+ props.onRsvpStatusesChange(savedSelection ?? new Set(DEFAULT_RSVP_STATUSES));
166
+ setSavedSelection(null);
154
167
  }
155
168
  else {
156
- next.add(status);
169
+ setSavedSelection(new Set(selectedRsvpStatuses));
170
+ props.onRsvpStatusesChange(new Set(['needsAction']));
157
171
  }
158
- props.onRsvpStatusesChange(next);
172
+ }
173
+ function handleToggle(status) {
174
+ const base = isExclusive ? savedSelection ?? DEFAULT_RSVP_STATUSES : selectedRsvpStatuses;
175
+ if (isExclusive)
176
+ setSavedSelection(null);
177
+ props.onRsvpStatusesChange(toggleRsvpStatus(base, status));
159
178
  }
160
179
  const dayGroups = groupByDay(filteredEvents, todayIso);
161
180
  function scrollToToday(behavior = 'auto') {
@@ -279,7 +298,7 @@ export function CalendarPane(props) {
279
298
  const loading = ctrlState.loading && filteredEvents.length === 0;
280
299
  const emptyStateText = 'No matching events in the next 30 days';
281
300
  const showCalendarSelector = calendarSources.length > 0;
282
- return (_jsxs("div", { className: "calendar-pane", ref: paneRef, children: [_jsxs("div", { className: "calendar-pane-scroll", children: [_jsxs("div", { className: "calendar-rsvp-chips", ref: chipsRef, children: [_jsx("button", { type: "button", className: "calendar-rsvp-chip calendar-scroll-today-btn", "aria-label": "Scroll to today", title: "Scroll to today", onClick: () => recenter('smooth'), children: _jsxs("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: [_jsx("rect", { x: "2", y: "3", width: "11", height: "10", rx: "1.5", stroke: "currentColor", strokeWidth: "1.2" }), _jsx("path", { d: "M2 6H13", stroke: "currentColor", strokeWidth: "1.2" }), _jsx("path", { d: "M5 2V4M10 2V4", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round" }), _jsx("circle", { cx: "7.5", cy: "9.5", r: "1.3", fill: "currentColor" })] }) }), RSVP_CHIPS.map((chip) => (_jsx("button", { type: "button", className: `calendar-rsvp-chip${selectedRsvpStatuses.has(chip.status) ? ' selected' : ''}`, "aria-pressed": selectedRsvpStatuses.has(chip.status), "aria-label": chip.label, title: chip.label, onClick: () => handleToggle(chip.status), children: chip.icon }, chip.status)))] }), needsScopeSources.map((s) => (_jsx("div", { children: props.sourceNotice?.(s) }, s.id))), error && (_jsx("div", { className: "empty-state calendar-empty-state", role: "alert", children: _jsx("p", { children: "Couldn't load calendar events. Try again shortly." }) })), !(error && filteredEvents.length === 0) && (_jsxs("div", { className: "calendar-day-groups", children: [dayGroups.map((group) => (_jsxs("div", { className: "calendar-day-group", children: [_jsx("div", { className: "calendar-day-header", ref: (el) => {
301
+ return (_jsxs("div", { className: "calendar-pane", ref: paneRef, children: [_jsxs("div", { className: "calendar-pane-scroll", children: [_jsxs("div", { className: "calendar-rsvp-chips", ref: chipsRef, children: [_jsx("button", { type: "button", className: "calendar-rsvp-chip calendar-scroll-today-btn", "aria-label": "Scroll to today", title: "Scroll to today", onClick: () => recenter('smooth'), children: _jsxs("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", children: [_jsx("rect", { x: "2", y: "3", width: "11", height: "10", rx: "1.5", stroke: "currentColor", strokeWidth: "1.2" }), _jsx("path", { d: "M2 6H13", stroke: "currentColor", strokeWidth: "1.2" }), _jsx("path", { d: "M5 2V4M10 2V4", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round" }), _jsx("circle", { cx: "7.5", cy: "9.5", r: "1.3", fill: "currentColor" })] }) }), RSVP_CHIPS.map((chip) => (_jsxs("button", { type: "button", className: `calendar-rsvp-chip${(chip.status === 'needsAction' ? isExclusive : selectedRsvpStatuses.has(chip.status)) ? ' selected' : ''}`, "aria-pressed": chip.status === 'needsAction' ? isExclusive : selectedRsvpStatuses.has(chip.status), "aria-label": chip.status === 'needsAction' ? `${chip.label} (${unacceptedCount})` : chip.label, title: chip.status === 'needsAction' ? `${chip.label} (${unacceptedCount})` : chip.label, onClick: () => chip.status === 'needsAction' ? handleUnaccepted() : handleToggle(chip.status), children: [chip.icon, chip.status === 'needsAction' && (_jsx("span", { className: "calendar-rsvp-chip-badge", "aria-hidden": "true", children: unacceptedCount > 99 ? '99+' : unacceptedCount }))] }, chip.status)))] }), needsScopeSources.map((s) => (_jsx("div", { children: props.sourceNotice?.(s) }, s.id))), error && (_jsx("div", { className: "empty-state calendar-empty-state", role: "alert", children: _jsx("p", { children: "Couldn't load calendar events. Try again shortly." }) })), !(error && filteredEvents.length === 0) && (_jsxs("div", { className: "calendar-day-groups", children: [dayGroups.map((group) => (_jsxs("div", { className: "calendar-day-group", children: [_jsx("div", { className: "calendar-day-header", ref: (el) => {
283
302
  if (el) {
284
303
  dayHeaderRefs.current.set(group.dateKey, el);
285
304
  }
@@ -324,7 +343,13 @@ export function CalendarPane(props) {
324
343
  openEventPopup(event.htmlLink, popupRef, popupName);
325
344
  }
326
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));
327
- })] }, 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) => {
328
- void controller.setSelectedIds(s.id, ids);
329
- }, 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
+ })) }) }))] }));
330
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/dist/styles.css CHANGED
@@ -68,12 +68,16 @@
68
68
  }
69
69
 
70
70
  .calendar-rsvp-chip {
71
- width: 32px;
71
+ width: auto;
72
+ min-width: 32px;
72
73
  height: 32px;
74
+ box-sizing: border-box;
75
+ flex-shrink: 0;
73
76
  display: flex;
74
77
  align-items: center;
75
78
  justify-content: center;
76
- padding: 0;
79
+ gap: 4px;
80
+ padding: 0 8px;
77
81
  background: transparent;
78
82
  color: #333;
79
83
  border: 1px solid #ccc;
@@ -97,6 +101,19 @@
97
101
  border-color: #081a59;
98
102
  }
99
103
 
104
+ .calendar-rsvp-chip-badge {
105
+ min-width: 16px;
106
+ padding: 1px 4px;
107
+ border-radius: 999px;
108
+ background: #e8eefb;
109
+ color: #081a59;
110
+ font-size: 11px;
111
+ font-weight: 600;
112
+ line-height: 16px;
113
+ text-align: center;
114
+ white-space: nowrap;
115
+ }
116
+
100
117
  .calendar-empty-state {
101
118
  padding: 24px 16px;
102
119
  color: #999;
@@ -306,6 +323,11 @@
306
323
  border-color: #22d0ef;
307
324
  }
308
325
 
326
+ .calendar-rsvp-chip-badge {
327
+ background: #081a59;
328
+ color: #e8eefb;
329
+ }
330
+
309
331
  .calendar-day-header {
310
332
  background: #222;
311
333
  color: #eee;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-webapp/calendar",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",