@open-webapp/calendar 0.2.0 → 0.3.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 +20 -0
- package/dist/core/controller.js +6 -1
- package/dist/react/CalendarPane.js +38 -17
- package/dist/styles.css +24 -2
- package/package.json +1 -1
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".
|
|
@@ -178,6 +186,18 @@ Built-in copy (overridable only through the slots above):
|
|
|
178
186
|
- `--chip-row-height` (fallback `44px`; host may set it to match its chip row for sticky headers)
|
|
179
187
|
- `--rsvp-bar` (set internally per RSVP status; do not override)
|
|
180
188
|
|
|
189
|
+
The Unaccepted count badge uses `.calendar-rsvp-chip-badge`, with light and dark styling included in `styles.css`.
|
|
190
|
+
|
|
191
|
+
## 0.3.0 (minor, additive)
|
|
192
|
+
|
|
193
|
+
- Adds the Unaccepted exclusive shortcut after Accepted, Maybe, and Declined, with local selection restoration and default fallback as described above.
|
|
194
|
+
- 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`.
|
|
195
|
+
- Adds the `.calendar-rsvp-chip-badge` host styling hook. Public props and default RSVP statuses remain compatible.
|
|
196
|
+
|
|
197
|
+
## 0.2.1 (patch)
|
|
198
|
+
|
|
199
|
+
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`.
|
|
200
|
+
|
|
181
201
|
## Breaking changes in 0.2.0
|
|
182
202
|
|
|
183
203
|
BREAKING (0.1.0 to 0.2.0):
|
package/dist/core/controller.js
CHANGED
|
@@ -178,10 +178,15 @@ export function createCalendarController(opts) {
|
|
|
178
178
|
}
|
|
179
179
|
async function runPass(only) {
|
|
180
180
|
loadingCount++;
|
|
181
|
+
publish();
|
|
181
182
|
try {
|
|
182
183
|
const ids = sources.map((s) => s.id).filter((id) => !only || only[id]);
|
|
183
184
|
const results = await Promise.all(ids.map((id) => refreshSource(id, only?.[id])));
|
|
184
|
-
|
|
185
|
+
const any = results.some(Boolean);
|
|
186
|
+
if (!only)
|
|
187
|
+
deferred = any;
|
|
188
|
+
else if (any)
|
|
189
|
+
deferred = true;
|
|
185
190
|
}
|
|
186
191
|
catch {
|
|
187
192
|
/* never throws */
|
|
@@ -2,6 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
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
4
|
import { CalendarSelector } 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;
|
|
@@ -106,7 +116,9 @@ export function CalendarPane(props) {
|
|
|
106
116
|
const events = useMemo(() => controller.getEvents(), [controller, version]);
|
|
107
117
|
const statuses = controller.getSourceStatus();
|
|
108
118
|
const connected = props.connected ?? sources.length > 0;
|
|
109
|
-
const ctrlState = controller.getState
|
|
119
|
+
const ctrlState = typeof controller.getState === 'function'
|
|
120
|
+
? controller.getState()
|
|
121
|
+
: { loading: false, error: false, deferred: false, lastSyncAt: null };
|
|
110
122
|
const multi = sources.length >= 2;
|
|
111
123
|
const needsScopeSources = sources.filter((s) => statuses[s.id] === 'needs-scope');
|
|
112
124
|
const needsReauth = connected && (props.needsReauth ?? (sources.length > 0 && needsScopeSources.length === sources.length));
|
|
@@ -120,6 +132,13 @@ export function CalendarPane(props) {
|
|
|
120
132
|
const chipsRef = useRef(null);
|
|
121
133
|
const popupRef = useRef(null);
|
|
122
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]);
|
|
123
142
|
const filteredEvents = useMemo(() => filterByRsvp(events, selectedRsvpStatuses), [events, selectedRsvpStatuses]);
|
|
124
143
|
const calendarsByKey = useMemo(() => {
|
|
125
144
|
const map = new Map();
|
|
@@ -141,19 +160,21 @@ export function CalendarPane(props) {
|
|
|
141
160
|
const nextUpcomingEvent = findNextUpcomingEvent(filteredEvents, now);
|
|
142
161
|
const selectedEventKey = currentEventKeys.values().next().value ??
|
|
143
162
|
(nextUpcomingEvent ? getCalendarEventKey(nextUpcomingEvent) : undefined);
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (next.size === 0) {
|
|
149
|
-
props.onRsvpStatusesChange(new Set(DEFAULT_RSVP_STATUSES));
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
163
|
+
function handleUnaccepted() {
|
|
164
|
+
if (isExclusive) {
|
|
165
|
+
props.onRsvpStatusesChange(savedSelection ?? new Set(DEFAULT_RSVP_STATUSES));
|
|
166
|
+
setSavedSelection(null);
|
|
152
167
|
}
|
|
153
168
|
else {
|
|
154
|
-
|
|
169
|
+
setSavedSelection(new Set(selectedRsvpStatuses));
|
|
170
|
+
props.onRsvpStatusesChange(new Set(['needsAction']));
|
|
155
171
|
}
|
|
156
|
-
|
|
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));
|
|
157
178
|
}
|
|
158
179
|
const dayGroups = groupByDay(filteredEvents, todayIso);
|
|
159
180
|
function scrollToToday(behavior = 'auto') {
|
|
@@ -277,7 +298,7 @@ export function CalendarPane(props) {
|
|
|
277
298
|
const loading = ctrlState.loading && filteredEvents.length === 0;
|
|
278
299
|
const emptyStateText = 'No matching events in the next 30 days';
|
|
279
300
|
const showCalendarSelector = calendarSources.length > 0;
|
|
280
|
-
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) => (
|
|
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) => {
|
|
281
302
|
if (el) {
|
|
282
303
|
dayHeaderRefs.current.set(group.dateKey, el);
|
|
283
304
|
}
|
package/dist/styles.css
CHANGED
|
@@ -68,12 +68,16 @@
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
.calendar-rsvp-chip {
|
|
71
|
-
width:
|
|
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
|
-
|
|
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;
|