@open-webapp/calendar 0.6.0 → 0.7.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 +8 -2
- package/dist/core/events.js +2 -4
- package/dist/react/CalendarPane.js +22 -10
- package/dist/styles.css +21 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ The exported standalone selector keeps its single-list props and behavior; Selec
|
|
|
211
211
|
|
|
212
212
|
### Host CSS
|
|
213
213
|
|
|
214
|
-
Day ribbon styling:
|
|
214
|
+
Day ribbon styling: each day's ribbon alternates between two tones by day parity (`data-parity="even|odd"`), set with `--calendar-day-ribbon-a` and `--calendar-day-ribbon-b`. Parity is stable across scroll, filtering and month boundaries, and the ribbon does not mark today. `--calendar-day-header-height` tunes the sticky label offset.
|
|
215
215
|
|
|
216
216
|
`styles.css` is self-contained, using component-scoped semantic palette tokens and native-control `color-scheme`. Each pane/selector root resets the full palette; explicit dark overrides and system-only `prefers-color-scheme` rules keep instances isolated. It requires no host CSS variables. Layout/status variables:
|
|
217
217
|
|
|
@@ -223,9 +223,15 @@ The Unaccepted count badge uses `.calendar-rsvp-chip-badge`, with light and dark
|
|
|
223
223
|
|
|
224
224
|
The shared `.calendar-selector-trigger` uses border-box inline-flex, centered alignment, an 8px gap, 36px minimum height, 8px 12px padding, 6px radius, a 1px solid control border, inherited font and pointer cursor. `.calendar-selector-trigger-label` bounds long labels with ellipsis; `.calendar-selector-chevron` stays 12px and does not shrink or add a tab stop. Existing scoped surface/text/control-border tokens style the resting trigger, the hover token supplies its hover background, and the focus token supplies a 2px `:focus-visible` outline with 2px offset. This approved visual-only exception changes trigger geometry and its derived dropdown position, preserving surrounding layout and theme isolation. Import the shipped stylesheet for both footer and standalone use.
|
|
225
225
|
|
|
226
|
+
## 0.7.0 (minor, breaking-visual)
|
|
227
|
+
|
|
228
|
+
- Day ribbons alternate between two tones by day parity (`data-parity="even|odd"`, days since 1970 of the date key mod 2) instead of highlighting today. Parity is stable across scroll, filter and month boundaries.
|
|
229
|
+
- "Today" is now the browser's local date (previously UTC, which could be off by a day for users away from UTC). Sticky day header text ("Today · Oct 26") is unchanged.
|
|
230
|
+
- Migration: `--calendar-today-accent` was removed in 0.7.0; use `--calendar-day-ribbon-a` / `--calendar-day-ribbon-b`. The ribbon no longer marks today (`data-today` is now `data-parity`).
|
|
231
|
+
|
|
226
232
|
## 0.6.0 (minor, additive-visual)
|
|
227
233
|
|
|
228
|
-
- Each day has a thin vertical ribbon left of its events with a rotated `Mmm D` label;
|
|
234
|
+
- Each day has a thin vertical ribbon left of its events with a rotated `Mmm D` label; ribbons alternate tones by day parity (see 0.7.0). The sticky day header is unchanged.
|
|
229
235
|
- The RSVP status bar moved from the left edge to the right edge of event rows.
|
|
230
236
|
- New helper `formatRibbonLabel`.
|
|
231
237
|
|
package/dist/core/events.js
CHANGED
|
@@ -39,9 +39,6 @@ export function getRollingWindow(now) {
|
|
|
39
39
|
const end = new Date(now.getTime() + 30 * MS_PER_DAY);
|
|
40
40
|
return { start: start.toISOString(), end: end.toISOString() };
|
|
41
41
|
}
|
|
42
|
-
function toIsoDate(d) {
|
|
43
|
-
return d.toISOString().slice(0, 10);
|
|
44
|
-
}
|
|
45
42
|
/**
|
|
46
43
|
* Returns events whose `selfResponseStatus` is a member of `selected` —
|
|
47
44
|
* declined is a normal, independently-selectable status now, not hard-excluded.
|
|
@@ -176,7 +173,8 @@ function addDaysToIso(dateKey, days) {
|
|
|
176
173
|
const { year, month, day } = parseIsoDateParts(dateKey);
|
|
177
174
|
const d = new Date(Date.UTC(year, month - 1, day));
|
|
178
175
|
d.setUTCDate(d.getUTCDate() + days);
|
|
179
|
-
|
|
176
|
+
// d is built from pure UTC parts (not "now"), so the UTC date is the right one.
|
|
177
|
+
return d.toISOString().slice(0, 10);
|
|
180
178
|
}
|
|
181
179
|
/**
|
|
182
180
|
* "Today · Sept 26" / "Tomorrow · Sept 27" / else "Sept 28"-style.
|
|
@@ -76,7 +76,12 @@ function openEventPopup(url, popupRef, popupName) {
|
|
|
76
76
|
popup.focus();
|
|
77
77
|
}
|
|
78
78
|
function toIsoDate(d) {
|
|
79
|
-
|
|
79
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
80
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
81
|
+
}
|
|
82
|
+
function dayParity(dateKey) {
|
|
83
|
+
const [y, m, d] = dateKey.split('-').map(Number);
|
|
84
|
+
return Math.floor(Date.UTC(y, m - 1, d) / 86400000) % 2 === 0 ? 'even' : 'odd';
|
|
80
85
|
}
|
|
81
86
|
function formatTimeOfDay(dateTime) {
|
|
82
87
|
const d = new Date(dateTime);
|
|
@@ -130,6 +135,7 @@ export function CalendarPane(props) {
|
|
|
130
135
|
const hasScrolledForEventsRef = useRef(false);
|
|
131
136
|
const paneRef = useRef(null);
|
|
132
137
|
const chipsRef = useRef(null);
|
|
138
|
+
const scrollRef = useRef(null);
|
|
133
139
|
const popupRef = useRef(null);
|
|
134
140
|
const [now, setNow] = useState(() => new Date());
|
|
135
141
|
const [savedSelection, setSavedSelection] = useState(null);
|
|
@@ -274,15 +280,21 @@ export function CalendarPane(props) {
|
|
|
274
280
|
if (!chipsEl || !paneEl)
|
|
275
281
|
return;
|
|
276
282
|
const observer = new ResizeObserver((entries) => {
|
|
277
|
-
const entry
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
for (const entry of entries) {
|
|
284
|
+
if (entry.target === scrollRef.current) {
|
|
285
|
+
// The day ribbon centers its label in the visible scroll area, so it needs that height.
|
|
286
|
+
paneEl.style.setProperty('--calendar-scroll-height', `${entry.target.getBoundingClientRect().height}px`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// contentRect excludes padding; the sticky day-header offset needs the
|
|
290
|
+
// chip row's full border-box height (chips have vertical padding).
|
|
291
|
+
const height = entry.borderBoxSize?.[0]?.blockSize ?? entry.target.getBoundingClientRect().height;
|
|
292
|
+
paneEl.style.setProperty('--chip-row-height', `${height}px`);
|
|
293
|
+
}
|
|
284
294
|
});
|
|
285
295
|
observer.observe(chipsEl);
|
|
296
|
+
if (scrollRef.current)
|
|
297
|
+
observer.observe(scrollRef.current);
|
|
286
298
|
return () => observer.disconnect();
|
|
287
299
|
}, [connected, needsReauth]);
|
|
288
300
|
if (!connected) {
|
|
@@ -298,14 +310,14 @@ export function CalendarPane(props) {
|
|
|
298
310
|
const loading = ctrlState.loading && filteredEvents.length === 0;
|
|
299
311
|
const emptyStateText = 'No matching events in the next 30 days';
|
|
300
312
|
const showCalendarSelector = calendarSources.length > 0;
|
|
301
|
-
return (_jsxs("div", { className: "calendar-pane", "data-calendar-theme": theme, 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) => {
|
|
313
|
+
return (_jsxs("div", { className: "calendar-pane", "data-calendar-theme": theme, ref: paneRef, children: [_jsxs("div", { className: "calendar-pane-scroll", ref: scrollRef, 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) => {
|
|
302
314
|
if (el) {
|
|
303
315
|
dayHeaderRefs.current.set(group.dateKey, el);
|
|
304
316
|
}
|
|
305
317
|
else {
|
|
306
318
|
dayHeaderRefs.current.delete(group.dateKey);
|
|
307
319
|
}
|
|
308
|
-
}, children: formatDayHeader(group.dateKey, todayIso) }), group.events.length > 0 && (_jsxs("div", { className: "calendar-day-body", children: [_jsx("div", { className: "calendar-day-ribbon", "aria-hidden": "true", "data-
|
|
320
|
+
}, children: formatDayHeader(group.dateKey, todayIso) }), group.events.length > 0 && (_jsxs("div", { className: "calendar-day-body", children: [_jsx("div", { className: "calendar-day-ribbon", "aria-hidden": "true", "data-parity": dayParity(group.dateKey), children: _jsx("div", { className: "calendar-day-ribbon-window", children: _jsx("span", { className: "calendar-day-ribbon-label", children: formatRibbonLabel(group.dateKey) }) }) }), _jsx("div", { className: "calendar-day-events", children: group.events.map((event) => {
|
|
309
321
|
const eventKey = getCalendarEventKey(event);
|
|
310
322
|
const calendar = calendarsByKey.get(`${event.sourceId}\u0000${event.calendarId}`);
|
|
311
323
|
const showCalendarLabel = calendar && !calendar.primary;
|
package/dist/styles.css
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
--calendar-chip-text: #333;
|
|
12
12
|
--calendar-chip-border: #ccc;
|
|
13
13
|
--calendar-chip-selected: #081a59;
|
|
14
|
-
--calendar-
|
|
14
|
+
--calendar-day-ribbon-a: #e4e8ef;
|
|
15
|
+
--calendar-day-ribbon-b: #c8d6f0;
|
|
15
16
|
--calendar-chip-selected-text: #ffffff;
|
|
16
17
|
--calendar-badge: #e8eefb;
|
|
17
18
|
--calendar-badge-text: #081a59;
|
|
@@ -217,16 +218,26 @@
|
|
|
217
218
|
align-items: flex-start;
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
.calendar-day-ribbon[data-
|
|
221
|
-
background: var(--calendar-
|
|
222
|
-
|
|
221
|
+
.calendar-day-ribbon[data-parity='even'] {
|
|
222
|
+
background: var(--calendar-day-ribbon-a);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.calendar-day-ribbon[data-parity='odd'] {
|
|
226
|
+
background: var(--calendar-day-ribbon-b);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.calendar-day-ribbon-window {
|
|
230
|
+
position: sticky;
|
|
231
|
+
top: calc(var(--chip-row-height, 44px) + var(--calendar-day-header-height, 33px));
|
|
232
|
+
/* Visible part of the ribbon: as tall as the scroll area below the sticky headers, capped at the ribbon. */
|
|
233
|
+
height: min(100%, calc(var(--calendar-scroll-height, 100vh) - var(--chip-row-height, 44px) - var(--calendar-day-header-height, 33px)));
|
|
234
|
+
display: flex;
|
|
235
|
+
align-items: center;
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
.calendar-day-ribbon-label {
|
|
226
239
|
writing-mode: vertical-rl;
|
|
227
240
|
transform: rotate(180deg);
|
|
228
|
-
position: sticky;
|
|
229
|
-
top: calc(var(--chip-row-height, 44px) + var(--calendar-day-header-height, 33px));
|
|
230
241
|
white-space: nowrap;
|
|
231
242
|
padding: 6px 0;
|
|
232
243
|
}
|
|
@@ -388,7 +399,8 @@
|
|
|
388
399
|
--calendar-chip-text: #eee;
|
|
389
400
|
--calendar-chip-border: #444;
|
|
390
401
|
--calendar-chip-selected: #22d0ef;
|
|
391
|
-
--calendar-
|
|
402
|
+
--calendar-day-ribbon-a: #2b3342;
|
|
403
|
+
--calendar-day-ribbon-b: #1c3a5e;
|
|
392
404
|
--calendar-chip-selected-text: #081a59;
|
|
393
405
|
--calendar-badge: #081a59;
|
|
394
406
|
--calendar-badge-text: #e8eefb;
|
|
@@ -430,7 +442,8 @@
|
|
|
430
442
|
--calendar-chip-text: #eee;
|
|
431
443
|
--calendar-chip-border: #444;
|
|
432
444
|
--calendar-chip-selected: #22d0ef;
|
|
433
|
-
|
|
445
|
+
--calendar-day-ribbon-a: #2b3342;
|
|
446
|
+
--calendar-day-ribbon-b: #1c3a5e;
|
|
434
447
|
--calendar-chip-selected-text: #081a59;
|
|
435
448
|
--calendar-badge: #081a59;
|
|
436
449
|
--calendar-badge-text: #e8eefb;
|