@agent-native/core 0.159.5 → 0.160.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/corpus/README.md +1 -1
- package/corpus/templates/assets/server/lib/generation.ts +10 -6
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +18 -4
- package/corpus/templates/calendar/actions/create-event.ts +4 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +56 -9
- package/corpus/templates/calendar/actions/update-event.ts +1 -0
- package/corpus/templates/calendar/agent-native.config.ts +1 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +8 -2
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +102 -68
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +150 -49
- package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +57 -23
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +4 -1
- package/corpus/templates/calendar/app/components/calendar/TimezoneSwitchDialog.tsx +70 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +127 -88
- package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +59 -25
- package/corpus/templates/calendar/app/hooks/use-event-drag.ts +55 -18
- package/corpus/templates/calendar/app/i18n-data.ts +60 -0
- package/corpus/templates/calendar/app/lib/all-day-layout.ts +16 -8
- package/corpus/templates/calendar/app/lib/calendar-drafts.ts +56 -0
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +351 -0
- package/corpus/templates/calendar/app/lib/event-layout.ts +14 -8
- package/corpus/templates/calendar/app/lib/out-of-office.ts +29 -61
- package/corpus/templates/calendar/app/lib/working-location.ts +34 -2
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +317 -139
- package/corpus/templates/calendar/changelog/2026-08-13-calendar-stays-pinned-to-the-saved-timezone-and-asks-before-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-13-working-locations-can-be-added-from-calendar-days-with-a-ful.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-14-choosing-office-or-other-when-adding-a-working-location-creates.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-14-creating-an-other-working-location-now-keeps-the-custom-name.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-keep-home-office-or-custom-titles.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-now-keep-a-home-office-or-custom-title.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-14-turning-a-midnight-ending-working-location-back-to-all-day.md +6 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +22 -1
- package/dist/agent/engine/builder-engine.d.ts +16 -2
- package/dist/agent/engine/builder-engine.js +178 -115
- package/dist/agent/engine/builtin.js +13 -4
- package/dist/agent/engine/credential-errors.d.ts +23 -0
- package/dist/agent/engine/credential-errors.js +19 -0
- package/dist/agent/engine/error-detail.d.ts +15 -0
- package/dist/agent/engine/error-detail.js +29 -0
- package/dist/agent/engine/registry.d.ts +14 -3
- package/dist/agent/engine/registry.js +106 -61
- package/dist/agent/engine/types.d.ts +16 -0
- package/dist/agent/engine/types.js +9 -0
- package/dist/agent/production-agent.d.ts +7 -0
- package/dist/agent/production-agent.js +58 -20
- package/dist/agent/run-manager.js +30 -0
- package/dist/agent/thread-data-builder.js +4 -0
- package/dist/agent/types.d.ts +9 -0
- package/dist/client/agent-chat-adapter.js +46 -15
- package/dist/client/error-format.js +25 -2
- package/dist/client/sse-event-processor.d.ts +2 -0
- package/dist/client/sse-event-processor.js +12 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/background-automation-runner.js +9 -15
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/register-framework-secrets.js +25 -0
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat-plugin.js +9 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/credential-provider.d.ts +80 -0
- package/dist/server/credential-provider.js +168 -1
- package/dist/server/google-realtime-session.d.ts +1 -1
- package/dist/server/google-realtime-session.js +6 -6
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +6 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/realtime-voice.js +12 -6
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/server/transcribe-voice.js +18 -16
- package/dist/transcription/builder-transcription.js +8 -4
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import {
|
|
4
4
|
FeatureNotConfiguredError,
|
|
5
5
|
getBuilderImageGenerationBaseUrl,
|
|
6
|
-
|
|
6
|
+
resolveBuilderGatewayCredentials,
|
|
7
7
|
resolveSecret,
|
|
8
8
|
} from "@agent-native/core/server";
|
|
9
9
|
import { and, eq, inArray } from "drizzle-orm";
|
|
@@ -265,16 +265,20 @@ interface BuilderImageGenerationResponse {
|
|
|
265
265
|
export async function generateWithBuilderImageApi(
|
|
266
266
|
input: GenerateProviderInput,
|
|
267
267
|
): Promise<GenerateProviderOutput> {
|
|
268
|
-
|
|
268
|
+
// Gateway lane: a published site paying with Builder credits carries the
|
|
269
|
+
// injected gateway pair and no identity credential at all, and image
|
|
270
|
+
// generation is metered rather than identity-bearing. The resolver still
|
|
271
|
+
// prefers a connected Builder account, so a site that has one is unaffected.
|
|
272
|
+
const builderCredentials = await resolveBuilderGatewayCredentials();
|
|
269
273
|
if (!builderCredentials.privateKey || !builderCredentials.publicKey) {
|
|
270
274
|
const detail =
|
|
271
275
|
!builderCredentials.privateKey && !builderCredentials.publicKey
|
|
272
|
-
? "Builder
|
|
276
|
+
? "Builder credentials are missing"
|
|
273
277
|
: !builderCredentials.privateKey
|
|
274
|
-
? "Builder
|
|
275
|
-
: "Builder
|
|
278
|
+
? "Builder token is missing"
|
|
279
|
+
: "Builder space id is missing";
|
|
276
280
|
throw new BuilderImageGenerationError(
|
|
277
|
-
"Builder.io is not
|
|
281
|
+
"Builder.io is not connected for managed image generation. Connect Builder.io (free tier available), or publish with Builder credits so the site is issued its own gateway credential.",
|
|
278
282
|
401,
|
|
279
283
|
detail,
|
|
280
284
|
);
|
|
@@ -91,7 +91,8 @@ pnpm action create-event \
|
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
Required for ordinary events: `--title`, `--start`, `--end` (ISO datetime
|
|
94
|
-
format). Out-of-office events default the title to `Out of office
|
|
94
|
+
format). Out-of-office events default the title to `Out of office`, and
|
|
95
|
+
working-location events use Google's generated display title when `--title` is
|
|
95
96
|
omitted.
|
|
96
97
|
Optional: `--description`, `--location`, `--attendees`, `--addGoogleMeet`, `--addZoom`, `--sendUpdates`, `--accountEmail`.
|
|
97
98
|
|
|
@@ -156,9 +157,22 @@ pnpm action create-event \
|
|
|
156
157
|
|
|
157
158
|
Working-location events sync from Google with `workingLocationProperties` and
|
|
158
159
|
render as native working locations in the UI instead of generic all-day events.
|
|
159
|
-
They are transparent/non-blocking for availability.
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
They are transparent/non-blocking for availability. All-day working locations
|
|
161
|
+
use an exclusive `--end` date and can span multiple days; timed working
|
|
162
|
+
locations use ISO datetime start and end values.
|
|
163
|
+
|
|
164
|
+
Creating from a calendar day uses the selected Home, Office, or Other type —
|
|
165
|
+
do not leave the draft as Home and create that instead. Office does not need a
|
|
166
|
+
custom building name; Other does. The Other name is `workingLocationLabel`
|
|
167
|
+
(drafts keep `location` empty), so create must send that label — not an empty
|
|
168
|
+
`location`. Create and update reject a blank Other name instead of storing
|
|
169
|
+
`Working`. If that day already has a working location
|
|
170
|
+
on the same account, update that day's occurrence (`scope: "single"`) instead
|
|
171
|
+
of creating a second event. Timed (not all-day) working locations need a
|
|
172
|
+
summary of Home, Office, or the custom label — never the generated
|
|
173
|
+
`Working location` placeholder. All-day ones omit summary so Google can derive
|
|
174
|
+
the title. Converting a timed location that ends at local midnight back to
|
|
175
|
+
all-day keeps that single day; do not add another exclusive day.
|
|
162
176
|
|
|
163
177
|
`--fullDay true` is semantic only for out-of-office creation. It does not send
|
|
164
178
|
Google an all-day `date` event, which Google rejects for this event type.
|
|
@@ -37,7 +37,9 @@ export default defineAction({
|
|
|
37
37
|
title: z
|
|
38
38
|
.string()
|
|
39
39
|
.optional()
|
|
40
|
-
.describe(
|
|
40
|
+
.describe(
|
|
41
|
+
"Event title. Defaults to 'Out of office' for OOO events. Working-location events use Google's generated display title and do not require one.",
|
|
42
|
+
),
|
|
41
43
|
start: z
|
|
42
44
|
.string()
|
|
43
45
|
.describe(
|
|
@@ -46,7 +48,7 @@ export default defineAction({
|
|
|
46
48
|
end: z
|
|
47
49
|
.string()
|
|
48
50
|
.describe(
|
|
49
|
-
"End time in ISO format,
|
|
51
|
+
"End time in ISO format, the last inclusive YYYY-MM-DD date for a full-day OOO event, or the exclusive YYYY-MM-DD date for an all-day working-location event.",
|
|
50
52
|
),
|
|
51
53
|
startTimeZone: z
|
|
52
54
|
.string()
|
|
@@ -311,8 +311,17 @@ export function buildStatusEventFields(args: {
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
const type = args.workingLocationType ?? "customLocation";
|
|
314
|
-
const label =
|
|
315
|
-
args.workingLocationLabel ||
|
|
314
|
+
const label = (
|
|
315
|
+
args.workingLocationLabel ||
|
|
316
|
+
args.location ||
|
|
317
|
+
args.title ||
|
|
318
|
+
""
|
|
319
|
+
).trim();
|
|
320
|
+
if (type === "customLocation" && !label) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
"Other working locations require a name. Pass workingLocationLabel.",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
316
325
|
return {
|
|
317
326
|
eventType: args.eventType,
|
|
318
327
|
transparency: "transparent" as const,
|
|
@@ -321,7 +330,7 @@ export function buildStatusEventFields(args: {
|
|
|
321
330
|
type === "homeOffice"
|
|
322
331
|
? { type, homeOffice: {} }
|
|
323
332
|
: type === "officeLocation"
|
|
324
|
-
? { type, officeLocation: { label } }
|
|
333
|
+
? { type, officeLocation: label ? { label } : {} }
|
|
325
334
|
: { type, customLocation: { label } },
|
|
326
335
|
};
|
|
327
336
|
}
|
|
@@ -359,6 +368,16 @@ function normalizedIso(value: string, label: "start" | "end"): string {
|
|
|
359
368
|
return date.toISOString();
|
|
360
369
|
}
|
|
361
370
|
|
|
371
|
+
function normalizeWorkingLocationAllDayDate(value: string): string {
|
|
372
|
+
const date = allDayDatePart(value);
|
|
373
|
+
if (!isValidDateOnly(date)) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
"All-day working location events require valid YYYY-MM-DD dates.",
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return date;
|
|
379
|
+
}
|
|
380
|
+
|
|
362
381
|
export function normalizeCreateEventInput(args: {
|
|
363
382
|
title?: string;
|
|
364
383
|
eventType?: "default" | "outOfOffice" | "focusTime" | "workingLocation";
|
|
@@ -376,7 +395,27 @@ export function normalizeCreateEventInput(args: {
|
|
|
376
395
|
const title =
|
|
377
396
|
args.title?.trim() ||
|
|
378
397
|
(args.eventType === "outOfOffice" ? "Out of office" : "");
|
|
379
|
-
if (!title
|
|
398
|
+
if (!title && args.eventType !== "workingLocation") {
|
|
399
|
+
throw new Error("Event title is required.");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (args.eventType === "workingLocation" && args.allDay === true) {
|
|
403
|
+
const start = normalizeWorkingLocationAllDayDate(args.start);
|
|
404
|
+
const end = normalizeWorkingLocationAllDayDate(args.end);
|
|
405
|
+
if (end <= start) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
"All-day working location end date must be after its start date.",
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
title,
|
|
412
|
+
start,
|
|
413
|
+
end,
|
|
414
|
+
startTimeZone: undefined,
|
|
415
|
+
endTimeZone: undefined,
|
|
416
|
+
allDay: true,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
380
419
|
|
|
381
420
|
if (args.eventType === "outOfOffice" && args.fullDay === true) {
|
|
382
421
|
const timezone = requireIanaTimezone(
|
|
@@ -435,15 +474,21 @@ export function normalizeCreateEventInput(args: {
|
|
|
435
474
|
}
|
|
436
475
|
|
|
437
476
|
function allDayDatePart(value: string): string {
|
|
438
|
-
|
|
439
|
-
|
|
477
|
+
if (DATE_ONLY_PATTERN.test(value)) {
|
|
478
|
+
if (isValidDateOnly(value)) return value;
|
|
479
|
+
throw new Error("All-day status events must use valid YYYY-MM-DD dates.");
|
|
480
|
+
}
|
|
440
481
|
const date = new Date(value);
|
|
441
482
|
if (Number.isNaN(date.getTime())) {
|
|
442
483
|
throw new Error(
|
|
443
484
|
"All-day status events must use valid date or datetime start and end values.",
|
|
444
485
|
);
|
|
445
486
|
}
|
|
446
|
-
|
|
487
|
+
const datePart = date.toISOString().slice(0, 10);
|
|
488
|
+
if (!isValidDateOnly(datePart)) {
|
|
489
|
+
throw new Error("All-day status events must use valid YYYY-MM-DD dates.");
|
|
490
|
+
}
|
|
491
|
+
return datePart;
|
|
447
492
|
}
|
|
448
493
|
|
|
449
494
|
function allDaySpanDays(start: string, end: string): number {
|
|
@@ -477,8 +522,10 @@ export function validateStatusEventTiming(args: {
|
|
|
477
522
|
|
|
478
523
|
if (args.eventType === "workingLocation" && args.allDay === true) {
|
|
479
524
|
const days = allDaySpanDays(args.start, args.end);
|
|
480
|
-
if (days
|
|
481
|
-
throw new Error(
|
|
525
|
+
if (days < 1) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
"All-day working location end date must be after its start date.",
|
|
528
|
+
);
|
|
482
529
|
}
|
|
483
530
|
}
|
|
484
531
|
}
|
|
@@ -412,6 +412,7 @@ export default defineAction({
|
|
|
412
412
|
start: args.start ?? existingEvent.start,
|
|
413
413
|
end: args.end ?? existingEvent.end,
|
|
414
414
|
});
|
|
415
|
+
updates.allDay = args.allDay ?? existingEvent.allDay;
|
|
415
416
|
if (
|
|
416
417
|
existingEvent.eventType === "workingLocation" &&
|
|
417
418
|
existingEvent.workingLocationProperties
|
|
@@ -180,6 +180,10 @@ function dateTimePartsInTimezone(value: string, timezone: string) {
|
|
|
180
180
|
|
|
181
181
|
function allDayEndDate(end: string | undefined, fallback: string) {
|
|
182
182
|
if (!end) return fallback;
|
|
183
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(end)) {
|
|
184
|
+
const previous = addDaysToDateString(end, -1);
|
|
185
|
+
return previous < fallback ? fallback : previous;
|
|
186
|
+
}
|
|
183
187
|
const parsed = new Date(end);
|
|
184
188
|
if (Number.isNaN(parsed.getTime())) return fallback;
|
|
185
189
|
parsed.setDate(parsed.getDate() - 1);
|
|
@@ -323,12 +327,14 @@ export function CreateEventPopover({
|
|
|
323
327
|
|
|
324
328
|
if (draft) {
|
|
325
329
|
const startParts = draft.start
|
|
326
|
-
? draft.fullDay
|
|
330
|
+
? (draft.fullDay || draft.allDay) &&
|
|
331
|
+
/^\d{4}-\d{2}-\d{2}$/.test(draft.start)
|
|
327
332
|
? { date: draft.start, time: "00:00" }
|
|
328
333
|
: dateTimePartsInTimezone(draft.start, draftTimezone)
|
|
329
334
|
: null;
|
|
330
335
|
const endParts = draft.end
|
|
331
|
-
? draft.fullDay
|
|
336
|
+
? (draft.fullDay || draft.allDay) &&
|
|
337
|
+
/^\d{4}-\d{2}-\d{2}$/.test(draft.end)
|
|
332
338
|
? { date: draft.end, time: "00:00" }
|
|
333
339
|
: dateTimePartsInTimezone(
|
|
334
340
|
draft.end,
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
2
|
import type { CalendarEvent } from "@shared/api";
|
|
3
|
-
import { IconAlertTriangleFilled, IconMapPin } from "@tabler/icons-react";
|
|
4
3
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
isSameDay,
|
|
11
|
-
set,
|
|
12
|
-
isToday,
|
|
13
|
-
addMinutes,
|
|
14
|
-
addDays,
|
|
15
|
-
min,
|
|
16
|
-
} from "date-fns";
|
|
4
|
+
IconAlertTriangleFilled,
|
|
5
|
+
IconMapPin,
|
|
6
|
+
IconPlus,
|
|
7
|
+
} from "@tabler/icons-react";
|
|
8
|
+
import { eachHourOfInterval, format, set, addMinutes } from "date-fns";
|
|
17
9
|
import { useState, useEffect, useRef, useMemo, useCallback, memo } from "react";
|
|
18
10
|
|
|
19
11
|
import { useCalendarSetters } from "@/components/layout/AppLayout";
|
|
@@ -29,6 +21,13 @@ import {
|
|
|
29
21
|
type ViewPreferences,
|
|
30
22
|
} from "@/hooks/use-view-preferences";
|
|
31
23
|
import { partitionAllDayEvents } from "@/lib/all-day-layout";
|
|
24
|
+
import {
|
|
25
|
+
dateToCalendarDateKey,
|
|
26
|
+
getBrowserTimezone,
|
|
27
|
+
getDateKeyInTimezone,
|
|
28
|
+
getEventDateKey,
|
|
29
|
+
getEventSegmentForCalendarDay,
|
|
30
|
+
} from "@/lib/calendar-timezone";
|
|
32
31
|
import { getEventDisplayColor, allOtherDeclined } from "@/lib/event-colors";
|
|
33
32
|
import {
|
|
34
33
|
computeTimedEventLayout,
|
|
@@ -58,6 +57,7 @@ import { OutOfOfficeEvent } from "./OutOfOfficeEvent";
|
|
|
58
57
|
interface DayViewProps {
|
|
59
58
|
events: CalendarEvent[];
|
|
60
59
|
date: Date;
|
|
60
|
+
timezone?: string;
|
|
61
61
|
onDeleteEvent: (eventId: string) => void;
|
|
62
62
|
onEventTimeChange?: (eventId: string, newStart: Date, newEnd: Date) => void;
|
|
63
63
|
onClickTimeSlot?: (
|
|
@@ -66,6 +66,7 @@ interface DayViewProps {
|
|
|
66
66
|
endTime: string,
|
|
67
67
|
options?: { explicitDuration?: boolean },
|
|
68
68
|
) => void;
|
|
69
|
+
onCreateWorkingLocation?: (date: Date) => void;
|
|
69
70
|
quickEditEventId?: string | null;
|
|
70
71
|
onQuickEditSave?: (
|
|
71
72
|
eventId: string,
|
|
@@ -118,7 +119,7 @@ function minutesToTimeString(totalMinutes: number): string {
|
|
|
118
119
|
/** Convert minutes-from-START_HOUR on a given day into a Date, for ghost label formatting */
|
|
119
120
|
function minutesToDate(date: Date, totalMinutes: number): Date {
|
|
120
121
|
return addMinutes(
|
|
121
|
-
set(
|
|
122
|
+
set(date, { hours: START_HOUR, minutes: 0, seconds: 0 }),
|
|
122
123
|
totalMinutes,
|
|
123
124
|
);
|
|
124
125
|
}
|
|
@@ -142,18 +143,14 @@ function formatEventTime(start: Date, end: Date): string {
|
|
|
142
143
|
return `${startWithAmPm}–${endStr}`;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
function getEventStyleForDate(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const durationMinutes = Math.max(
|
|
154
|
-
15,
|
|
155
|
-
differenceInMinutes(cappedEnd, segStart),
|
|
156
|
-
);
|
|
146
|
+
function getEventStyleForDate(
|
|
147
|
+
event: CalendarEvent,
|
|
148
|
+
date: Date,
|
|
149
|
+
timezone: string,
|
|
150
|
+
) {
|
|
151
|
+
const segment = getEventSegmentForCalendarDay(event, date, timezone);
|
|
152
|
+
const topMinutes = segment?.topMinutes ?? 0;
|
|
153
|
+
const durationMinutes = Math.max(15, segment?.durationMinutes ?? 15);
|
|
157
154
|
return {
|
|
158
155
|
top: `${(topMinutes / 60) * HOUR_HEIGHT}px`,
|
|
159
156
|
height: `${(durationMinutes / 60) * HOUR_HEIGHT}px`,
|
|
@@ -163,6 +160,7 @@ function getEventStyleForDate(event: CalendarEvent, date: Date) {
|
|
|
163
160
|
interface DayEventCardProps {
|
|
164
161
|
event: CalendarEvent;
|
|
165
162
|
date: Date;
|
|
163
|
+
timezone: string;
|
|
166
164
|
layout: Map<string, TimedEventLayout>;
|
|
167
165
|
now: Date;
|
|
168
166
|
prefs: ViewPreferences;
|
|
@@ -204,6 +202,7 @@ interface DayEventCardProps {
|
|
|
204
202
|
const DayEventCard = memo(function DayEventCard({
|
|
205
203
|
event,
|
|
206
204
|
date,
|
|
205
|
+
timezone,
|
|
207
206
|
layout,
|
|
208
207
|
now,
|
|
209
208
|
prefs,
|
|
@@ -246,32 +245,24 @@ const DayEventCard = memo(function DayEventCard({
|
|
|
246
245
|
top: `${overrides.top}px`,
|
|
247
246
|
height: `${overrides.height}px`,
|
|
248
247
|
}
|
|
249
|
-
: getEventStyleForDate(event, date);
|
|
248
|
+
: getEventStyleForDate(event, date, timezone);
|
|
250
249
|
const color = getEventDisplayColor(event, prefs);
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
const isStart = isSameDay(evStart, date);
|
|
257
|
-
const isEnd = rawEnd <= midnight;
|
|
258
|
-
const dayGridStart = set(startOfDay(date), { hours: START_HOUR });
|
|
259
|
-
// For continuation segments (started a prior day), measure visible
|
|
260
|
-
// duration from the grid start, not from the event's true start.
|
|
261
|
-
const visibleSegStart = isStart ? evStart : dayGridStart;
|
|
250
|
+
const segment = getEventSegmentForCalendarDay(event, date, timezone);
|
|
251
|
+
const isStart =
|
|
252
|
+
getEventDateKey(event, timezone) === dateToCalendarDateKey(date);
|
|
253
|
+
const isOvernightCapped = !(segment?.endsOnDay ?? true);
|
|
254
|
+
const isEnd = segment?.endsOnDay ?? true;
|
|
262
255
|
const durationMin = overrides
|
|
263
256
|
? (overrides.height / HOUR_HEIGHT) * 60
|
|
264
|
-
:
|
|
257
|
+
: (segment?.durationMinutes ?? 15);
|
|
265
258
|
// Compute display times (use drag overrides if active)
|
|
266
259
|
const displayStart = overrides
|
|
267
|
-
?
|
|
268
|
-
:
|
|
260
|
+
? minutesToDate(date, START_HOUR * 60 + (overrides.top / HOUR_HEIGHT) * 60)
|
|
261
|
+
: minutesToDate(date, segment?.startMinutes ?? 0);
|
|
269
262
|
const displayEnd = overrides
|
|
270
263
|
? addMinutes(displayStart, durationMin)
|
|
271
|
-
:
|
|
272
|
-
|
|
273
|
-
: midnight;
|
|
274
|
-
const isPast = parseISO(event.end) < now;
|
|
264
|
+
: minutesToDate(date, segment?.endMinutes ?? durationMin);
|
|
265
|
+
const isPast = new Date(event.end) < now;
|
|
275
266
|
const isDeclined = event.responseStatus === "declined";
|
|
276
267
|
const allOthersOut = allOtherDeclined(event);
|
|
277
268
|
|
|
@@ -446,6 +437,7 @@ const DayEventCard = memo(function DayEventCard({
|
|
|
446
437
|
return (
|
|
447
438
|
<EventDetailPopover
|
|
448
439
|
event={event}
|
|
440
|
+
timezone={timezone}
|
|
449
441
|
onDelete={onDeleteEvent}
|
|
450
442
|
isDraft={isDraft}
|
|
451
443
|
defaultOpen={defaultOpen}
|
|
@@ -492,9 +484,11 @@ const DayCreateGhost = memo(function DayCreateGhost({
|
|
|
492
484
|
export const DayView = memo(function DayView({
|
|
493
485
|
events,
|
|
494
486
|
date,
|
|
487
|
+
timezone = getBrowserTimezone(),
|
|
495
488
|
onDeleteEvent,
|
|
496
489
|
onEventTimeChange,
|
|
497
490
|
onClickTimeSlot,
|
|
491
|
+
onCreateWorkingLocation,
|
|
498
492
|
quickEditEventId,
|
|
499
493
|
onQuickEditSave,
|
|
500
494
|
onQuickEditCancel,
|
|
@@ -584,8 +578,8 @@ export const DayView = memo(function DayView({
|
|
|
584
578
|
[events],
|
|
585
579
|
);
|
|
586
580
|
const layout = useMemo(
|
|
587
|
-
() => computeTimedEventLayout(timedEvents, date),
|
|
588
|
-
[date, timedEvents],
|
|
581
|
+
() => computeTimedEventLayout(timedEvents, date, timezone),
|
|
582
|
+
[date, timedEvents, timezone],
|
|
589
583
|
);
|
|
590
584
|
|
|
591
585
|
// Timezone label: prefer the short generic name (e.g. "PT", "ET")
|
|
@@ -595,7 +589,10 @@ export const DayView = memo(function DayView({
|
|
|
595
589
|
function nameForToken(token: "shortGeneric" | "longGeneric" | "short") {
|
|
596
590
|
try {
|
|
597
591
|
return (
|
|
598
|
-
new Intl.DateTimeFormat("en-US", {
|
|
592
|
+
new Intl.DateTimeFormat("en-US", {
|
|
593
|
+
timeZone: timezone,
|
|
594
|
+
timeZoneName: token,
|
|
595
|
+
})
|
|
599
596
|
.formatToParts(now)
|
|
600
597
|
.find((p) => p.type === "timeZoneName")?.value ?? ""
|
|
601
598
|
);
|
|
@@ -604,10 +601,7 @@ export const DayView = memo(function DayView({
|
|
|
604
601
|
}
|
|
605
602
|
}
|
|
606
603
|
|
|
607
|
-
|
|
608
|
-
try {
|
|
609
|
-
iana = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "";
|
|
610
|
-
} catch {}
|
|
604
|
+
const iana = timezone;
|
|
611
605
|
|
|
612
606
|
const longGeneric = nameForToken("longGeneric");
|
|
613
607
|
let shortGeneric = nameForToken("shortGeneric");
|
|
@@ -625,10 +619,26 @@ export const DayView = memo(function DayView({
|
|
|
625
619
|
tzLong: longGeneric || iana,
|
|
626
620
|
tzIana: iana,
|
|
627
621
|
};
|
|
628
|
-
}, []);
|
|
622
|
+
}, [now, timezone]);
|
|
629
623
|
|
|
630
|
-
const today =
|
|
631
|
-
|
|
624
|
+
const today =
|
|
625
|
+
dateToCalendarDateKey(date) === getDateKeyInTimezone(now, timezone);
|
|
626
|
+
const nowParts = new Intl.DateTimeFormat("en-US", {
|
|
627
|
+
timeZone: timezone,
|
|
628
|
+
hourCycle: "h23",
|
|
629
|
+
hour: "2-digit",
|
|
630
|
+
minute: "2-digit",
|
|
631
|
+
})
|
|
632
|
+
.formatToParts(now)
|
|
633
|
+
.reduce(
|
|
634
|
+
(parts, part) => {
|
|
635
|
+
if (part.type === "hour") parts.hour = Number(part.value);
|
|
636
|
+
if (part.type === "minute") parts.minute = Number(part.value);
|
|
637
|
+
return parts;
|
|
638
|
+
},
|
|
639
|
+
{ hour: 0, minute: 0 },
|
|
640
|
+
);
|
|
641
|
+
const nowMinutes = (nowParts.hour - START_HOUR) * 60 + nowParts.minute;
|
|
632
642
|
const nowTop = (nowMinutes / 60) * HOUR_HEIGHT;
|
|
633
643
|
const showNowIndicator =
|
|
634
644
|
today && nowMinutes >= 0 && nowMinutes <= (END_HOUR - START_HOUR) * 60;
|
|
@@ -653,6 +663,7 @@ export const DayView = memo(function DayView({
|
|
|
653
663
|
scrollContainerRef,
|
|
654
664
|
onEventTimeChange: handleEventTimeChange,
|
|
655
665
|
events,
|
|
666
|
+
timezone,
|
|
656
667
|
});
|
|
657
668
|
|
|
658
669
|
const canDrag = !!onEventTimeChange;
|
|
@@ -745,19 +756,38 @@ export const DayView = memo(function DayView({
|
|
|
745
756
|
{format(date, "MMMM d, yyyy")}
|
|
746
757
|
</div>
|
|
747
758
|
</div>
|
|
748
|
-
<
|
|
749
|
-
|
|
750
|
-
<
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
759
|
+
<div className="flex items-start gap-1">
|
|
760
|
+
{onCreateWorkingLocation && (
|
|
761
|
+
<Tooltip>
|
|
762
|
+
<TooltipTrigger asChild>
|
|
763
|
+
<button
|
|
764
|
+
type="button"
|
|
765
|
+
aria-label={t("calendarView.addWorkingLocation")}
|
|
766
|
+
className="flex size-6 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
767
|
+
onClick={() => onCreateWorkingLocation(date)}
|
|
768
|
+
>
|
|
769
|
+
<IconPlus aria-hidden="true" className="size-3.5" />
|
|
770
|
+
</button>
|
|
771
|
+
</TooltipTrigger>
|
|
772
|
+
<TooltipContent side="bottom">
|
|
773
|
+
{t("calendarView.addWorkingLocation")}
|
|
774
|
+
</TooltipContent>
|
|
775
|
+
</Tooltip>
|
|
776
|
+
)}
|
|
777
|
+
<Tooltip>
|
|
778
|
+
<TooltipTrigger asChild>
|
|
779
|
+
<span className="cursor-default truncate px-1 pt-1 text-[11px] font-medium text-muted-foreground">
|
|
780
|
+
{tzShort}
|
|
781
|
+
</span>
|
|
782
|
+
</TooltipTrigger>
|
|
783
|
+
<TooltipContent side="bottom">
|
|
784
|
+
<p className="text-xs">{tzLong}</p>
|
|
785
|
+
{tzIana && tzIana !== tzLong ? (
|
|
786
|
+
<p className="text-[10px] text-muted-foreground">{tzIana}</p>
|
|
787
|
+
) : null}
|
|
788
|
+
</TooltipContent>
|
|
789
|
+
</Tooltip>
|
|
790
|
+
</div>
|
|
761
791
|
</div>
|
|
762
792
|
|
|
763
793
|
{/* Working locations and ordinary all-day events */}
|
|
@@ -776,6 +806,7 @@ export const DayView = memo(function DayView({
|
|
|
776
806
|
<EventDetailPopover
|
|
777
807
|
key={`${event.overlayEmail ?? event.accountEmail ?? "primary"}:${event.id}`}
|
|
778
808
|
event={event}
|
|
809
|
+
timezone={timezone}
|
|
779
810
|
onDelete={onDeleteEvent}
|
|
780
811
|
isDraft={draftEventIds.includes(event.id)}
|
|
781
812
|
defaultOpen={quickEditEventId === event.id}
|
|
@@ -852,6 +883,7 @@ export const DayView = memo(function DayView({
|
|
|
852
883
|
<EventDetailPopover
|
|
853
884
|
key={`${event.overlayEmail ?? event.accountEmail ?? "primary"}:${event.id}`}
|
|
854
885
|
event={event}
|
|
886
|
+
timezone={timezone}
|
|
855
887
|
onDelete={onDeleteEvent}
|
|
856
888
|
isDraft={draftEventIds.includes(event.id)}
|
|
857
889
|
defaultOpen={quickEditEventId === event.id}
|
|
@@ -1017,6 +1049,7 @@ export const DayView = memo(function DayView({
|
|
|
1017
1049
|
key={event._tempId ?? event.id}
|
|
1018
1050
|
event={event}
|
|
1019
1051
|
day={date}
|
|
1052
|
+
timezone={timezone}
|
|
1020
1053
|
hourHeight={HOUR_HEIGHT}
|
|
1021
1054
|
color={
|
|
1022
1055
|
getEventDisplayColor(event, prefs) ?? "hsl(var(--primary))"
|
|
@@ -1087,6 +1120,7 @@ export const DayView = memo(function DayView({
|
|
|
1087
1120
|
key={event._tempId ?? event.id}
|
|
1088
1121
|
event={event}
|
|
1089
1122
|
date={date}
|
|
1123
|
+
timezone={timezone}
|
|
1090
1124
|
layout={layout}
|
|
1091
1125
|
now={now}
|
|
1092
1126
|
prefs={prefs}
|