@agent-native/core 0.176.4 → 0.176.5-nightly-20260902205402

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.
Files changed (69) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
  3. package/corpus/templates/calendar/AGENTS.md +4 -0
  4. package/corpus/templates/calendar/actions/delete-event.ts +2 -2
  5. package/corpus/templates/calendar/actions/event-action-helpers.ts +7 -0
  6. package/corpus/templates/calendar/actions/get-event.ts +40 -3
  7. package/corpus/templates/calendar/actions/list-events.ts +49 -2
  8. package/corpus/templates/calendar/actions/list-google-calendars.ts +24 -0
  9. package/corpus/templates/calendar/actions/update-calendar-visual-preferences.ts +52 -1
  10. package/corpus/templates/calendar/actions/update-event.ts +2 -2
  11. package/corpus/templates/calendar/actions/view-screen.ts +54 -2
  12. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +33 -19
  13. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +31 -5
  14. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +102 -0
  15. package/corpus/templates/calendar/app/hooks/use-events.ts +45 -10
  16. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +8 -1
  17. package/corpus/templates/calendar/app/hooks/use-google-calendars.ts +41 -0
  18. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +56 -4
  19. package/corpus/templates/calendar/app/lib/shared-calendar-demo.ts +187 -0
  20. package/corpus/templates/calendar/app/pages/CalendarView.tsx +50 -5
  21. package/corpus/templates/calendar/changelog/2026-09-01-calendars-shared-with-your-connected-google-accounts-can-now.md +6 -0
  22. package/corpus/templates/calendar/changelog/2026-09-02-updated-the-booking-link-og-preview-image-with-the-new-monoc.md +6 -0
  23. package/corpus/templates/calendar/server/lib/booking-og-image.ts +8 -18
  24. package/corpus/templates/calendar/server/lib/google-api.ts +10 -0
  25. package/corpus/templates/calendar/server/lib/google-calendar.ts +215 -21
  26. package/corpus/templates/calendar/server/plugins/agent-chat.ts +2 -0
  27. package/corpus/templates/calendar/server/plugins/feature-flags.ts +5 -0
  28. package/corpus/templates/calendar/shared/api.ts +20 -0
  29. package/corpus/templates/calendar/shared/calendar-view-preferences.ts +28 -1
  30. package/corpus/templates/calendar/shared/feature-flags.ts +8 -0
  31. package/corpus/templates/calendar/shared/google-calendar-sources.ts +46 -0
  32. package/corpus/templates/factory/.env.example +5 -0
  33. package/corpus/templates/factory/README.md +9 -0
  34. package/corpus/templates/factory/server/connectors/credentials.ts +28 -6
  35. package/corpus/templates/factory/server/triage/github-client.ts +144 -7
  36. package/corpus/templates/forms/server/lib/form-og-image.ts +7 -17
  37. package/corpus/templates/slides/actions/add-slide.ts +1 -0
  38. package/corpus/templates/slides/actions/create-deck.ts +1 -0
  39. package/corpus/templates/slides/actions/get-deck.ts +1 -0
  40. package/corpus/templates/slides/actions/navigate.ts +1 -0
  41. package/corpus/templates/slides/actions/patch-deck.ts +1 -0
  42. package/corpus/templates/slides/actions/update-slide.ts +1 -0
  43. package/corpus/templates/slides/actions/view-screen.ts +1 -0
  44. package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
  45. package/dist/action.d.ts +4 -0
  46. package/dist/action.js +3 -0
  47. package/dist/agent/types.d.ts +1 -0
  48. package/dist/automation/index.d.ts +1 -1
  49. package/dist/client/webmcp.js +7 -2
  50. package/dist/collab/awareness.d.ts +2 -2
  51. package/dist/mcp/build-server.d.ts +2 -0
  52. package/dist/mcp/build-server.js +4 -0
  53. package/dist/notifications/routes.d.ts +3 -3
  54. package/dist/observability/routes.d.ts +6 -6
  55. package/dist/provider-api/actions/provider-api.d.ts +1 -1
  56. package/dist/secrets/routes.d.ts +9 -9
  57. package/dist/server/action-routes.d.ts +1 -0
  58. package/dist/server/action-routes.js +4 -0
  59. package/dist/server/agent-chat/mcp-options.d.ts +4 -0
  60. package/dist/server/agent-chat/mcp-options.js +1 -0
  61. package/dist/server/agent-chat-plugin.js +2 -0
  62. package/dist/server/realtime-token.d.ts +1 -1
  63. package/dist/server/social-og-image.js +6 -20
  64. package/dist/server/transcribe-voice.d.ts +1 -1
  65. package/dist/shared/agent-mcp-metadata.d.ts +3 -0
  66. package/dist/shared/agent-mcp-metadata.js +18 -0
  67. package/dist/shared/social-meta.d.ts +1 -1
  68. package/dist/shared/social-meta.js +1 -1
  69. package/package.json +1 -1
@@ -501,11 +501,17 @@ export function EventDetailPopover({
501
501
  const workingLocationLabels = createWorkingLocationDisplayLabels(t);
502
502
  const isMobile = useIsMobile();
503
503
  const eventTimezone = timezone || event.startTimeZone || getLocalTimezone();
504
+ const isReadOnlySource =
505
+ !!event.overlayEmail ||
506
+ event.calendarPrimary === false ||
507
+ event.calendarReadOnly === true;
504
508
  const [open, setOpen] = useState(defaultOpen);
505
509
  const [editingTitle, setEditingTitle] = useState(
506
- defaultOpen ? getEditableEventTitle(event) : "",
510
+ defaultOpen && !isReadOnlySource ? getEditableEventTitle(event) : "",
511
+ );
512
+ const [isEditingTitle, setIsEditingTitle] = useState(
513
+ defaultOpen && !isReadOnlySource,
507
514
  );
508
- const [isEditingTitle, setIsEditingTitle] = useState(defaultOpen);
509
515
  const isNewEventRef = useRef(defaultOpen);
510
516
  const titleInputRef = useRef<HTMLInputElement>(null);
511
517
  const popoverTokenRef = useRef<symbol | null>(null);
@@ -570,13 +576,13 @@ export function EventDetailPopover({
570
576
  string | null
571
577
  >(() => getStoredZoomAfterConnectEventId());
572
578
  const [showConferencingOptions, setShowConferencingOptions] = useState(false);
573
- const isOverlay = !!event.overlayEmail;
579
+ const isOverlay = isReadOnlySource;
574
580
  const ownerLabel = event.ownerName || event.overlayEmail;
575
581
 
576
582
  const updateEvent = useUpdateEvent();
577
583
  const masterEventId =
578
584
  open && event.recurringEventId ? `google-${event.recurringEventId}` : "";
579
- const masterEvent = useEvent(masterEventId);
585
+ const masterEvent = useEvent(masterEventId, event.calendarSourceKey);
580
586
  const recurrenceRules =
581
587
  event.recurrence && event.recurrence.length > 0
582
588
  ? event.recurrence
@@ -1810,7 +1816,7 @@ export function EventDetailPopover({
1810
1816
  <div ref={detailsScrollRef} className="flex-1 overflow-y-auto">
1811
1817
  <div className="px-2 py-2">
1812
1818
  {/* Title — always editable */}
1813
- {isEditingTitle && !isWorkingLocation ? (
1819
+ {isEditingTitle && !isWorkingLocation && !isOverlay ? (
1814
1820
  <input
1815
1821
  ref={titleInputRef}
1816
1822
  value={editingTitle}
@@ -2738,6 +2744,26 @@ export function EventDetailPopover({
2738
2744
  </>
2739
2745
  )}
2740
2746
 
2747
+ {(event.calendarPrimary === false || event.calendarReadOnly) &&
2748
+ event.calendarName &&
2749
+ !event.overlayEmail && (
2750
+ <>
2751
+ <div className={eventPopoverDivider} />
2752
+ <div className="flex items-center gap-2 px-4 py-1.5">
2753
+ <span
2754
+ aria-hidden="true"
2755
+ className="ml-1 size-2 shrink-0 rounded-full ring-1 ring-border"
2756
+ style={{ backgroundColor: event.color }}
2757
+ />
2758
+ <span className="truncate text-muted-foreground">
2759
+ {t("eventForm.viewingOwnerCalendar", {
2760
+ owner: `${event.calendarName} · ${event.accountEmail ?? "Google"}`,
2761
+ })}
2762
+ </span>
2763
+ </div>
2764
+ </>
2765
+ )}
2766
+
2741
2767
  {/* Bottom padding */}
2742
2768
  <div className="h-3" />
2743
2769
  </div>
@@ -73,6 +73,7 @@ import {
73
73
  useGoogleAuthStatus,
74
74
  useGoogleDesktopAuth,
75
75
  } from "@/hooks/use-google-auth";
76
+ import { useGoogleCalendars } from "@/hooks/use-google-calendars";
76
77
  import {
77
78
  useOverlayPeople,
78
79
  useRemoveOverlayPerson,
@@ -635,6 +636,106 @@ function GoogleAccountsSection({
635
636
  );
636
637
  }
637
638
 
639
+ function SharedGoogleCalendarsGroup() {
640
+ const t = useT();
641
+ const { data: calendars, enabled } = useGoogleCalendars();
642
+ const {
643
+ prefs: { googleCalendarVisibility },
644
+ updateGoogleCalendarVisibility,
645
+ } = useViewPreferences();
646
+ const sharedCalendars = (calendars ?? []).filter(
647
+ (calendar) => !calendar.primary && calendar.accessRole !== "freeBusyReader",
648
+ );
649
+ const grouped = useMemo(() => {
650
+ const groups = new Map<string, typeof sharedCalendars>();
651
+ for (const calendar of sharedCalendars) {
652
+ const current = groups.get(calendar.accountEmail) ?? [];
653
+ current.push(calendar);
654
+ groups.set(calendar.accountEmail, current);
655
+ }
656
+ return Array.from(groups.entries());
657
+ }, [sharedCalendars]);
658
+
659
+ if (!enabled || grouped.length === 0) return null;
660
+
661
+ return (
662
+ <div className="space-y-1">
663
+ {grouped.map(([accountEmail, accountCalendars]) => (
664
+ <div key={accountEmail}>
665
+ <div className="px-3 py-1 text-[10px] font-medium text-muted-foreground/60">
666
+ {accountEmail}
667
+ </div>
668
+ <div className="space-y-0.5">
669
+ {accountCalendars.map((calendar) => {
670
+ const visible =
671
+ googleCalendarVisibility[calendar.sourceKey] ??
672
+ calendar.selected;
673
+ return (
674
+ <div
675
+ key={calendar.sourceKey}
676
+ className="group flex min-h-7 items-center gap-2 px-3 text-xs"
677
+ >
678
+ <span
679
+ aria-hidden="true"
680
+ className={cn(
681
+ "block size-2.5 shrink-0 rounded-full ring-1 ring-border",
682
+ !visible && "opacity-40",
683
+ )}
684
+ style={{
685
+ backgroundColor: calendar.color || CALENDAR_COLORS[6],
686
+ }}
687
+ />
688
+ <span
689
+ className={cn(
690
+ "min-w-0 flex-1 truncate",
691
+ visible
692
+ ? "text-muted-foreground"
693
+ : "text-muted-foreground/40",
694
+ )}
695
+ >
696
+ {calendar.name}
697
+ </span>
698
+ <Tooltip>
699
+ <TooltipTrigger asChild>
700
+ <button
701
+ type="button"
702
+ onClick={() =>
703
+ updateGoogleCalendarVisibility(
704
+ calendar.sourceKey,
705
+ !visible,
706
+ )
707
+ }
708
+ className="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground/60 opacity-0 hover:text-foreground group-hover:opacity-100 focus-visible:opacity-100"
709
+ aria-label={
710
+ visible
711
+ ? t("sidebar.hideCalendar")
712
+ : t("sidebar.showCalendar")
713
+ }
714
+ aria-pressed={visible}
715
+ >
716
+ {visible ? (
717
+ <IconEye className="size-3" />
718
+ ) : (
719
+ <IconEyeOff className="size-3" />
720
+ )}
721
+ </button>
722
+ </TooltipTrigger>
723
+ <TooltipContent side="right">
724
+ {visible
725
+ ? t("sidebar.hideCalendar")
726
+ : t("sidebar.showCalendar")}
727
+ </TooltipContent>
728
+ </Tooltip>
729
+ </div>
730
+ );
731
+ })}
732
+ </div>
733
+ </div>
734
+ ))}
735
+ </div>
736
+ );
737
+ }
738
+
638
739
  export function Sidebar({
639
740
  open,
640
741
  onClose,
@@ -929,6 +1030,7 @@ export function Sidebar({
929
1030
  <IconPlus className="h-3.5 w-3.5" />
930
1031
  </button>
931
1032
  </div>
1033
+ <SharedGoogleCalendarsGroup />
932
1034
  {(overlayPeople.length > 0 || externalCalendars.length > 0) && (
933
1035
  <div className="mt-1 space-y-1">
934
1036
  {overlayPeople.length > 0 && (
@@ -6,6 +6,7 @@ import {
6
6
  import type { CalendarEvent, UpdateEventScope } from "@shared/api";
7
7
  import {
8
8
  useQueryClient,
9
+ useQuery,
9
10
  keepPreviousData,
10
11
  type QueryKey,
11
12
  } from "@tanstack/react-query";
@@ -13,6 +14,10 @@ import { nanoid } from "nanoid";
13
14
  import { useMemo } from "react";
14
15
 
15
16
  import { dateTimeInTimezoneToIso } from "@/lib/event-form-utils";
17
+ import {
18
+ isSharedCalendarDemo,
19
+ SHARED_CALENDAR_DEMO_EVENTS,
20
+ } from "@/lib/shared-calendar-demo";
16
21
  import {
17
22
  buildWorkingLocationProperties,
18
23
  getWorkingLocationEditableLabel,
@@ -83,13 +88,17 @@ function buildEventsParams(
83
88
  from?: string,
84
89
  to?: string,
85
90
  overlayEmails?: string[],
86
- ): Record<string, string> {
87
- const params: Record<string, string> = {};
91
+ calendarSourceKeys?: string[],
92
+ ): Record<string, string | string[]> {
93
+ const params: Record<string, string | string[]> = {};
88
94
  if (from) params.from = from;
89
95
  if (to) params.to = to;
90
96
  if (overlayEmails && overlayEmails.length > 0) {
91
97
  params.overlayEmails = overlayEmails.join(",");
92
98
  }
99
+ if (calendarSourceKeys && calendarSourceKeys.length > 0) {
100
+ params.calendarSourceKeys = calendarSourceKeys;
101
+ }
93
102
  return params;
94
103
  }
95
104
 
@@ -103,8 +112,12 @@ function getListEventsParams(
103
112
  return params as Record<string, string>;
104
113
  }
105
114
 
106
- function getEventQueryKey(id: string) {
107
- return ["action", "get-event", { id }] as const;
115
+ function getEventQueryKey(id: string, calendarSourceKey?: string) {
116
+ return [
117
+ "action",
118
+ "get-event",
119
+ calendarSourceKey ? { id, calendarSourceKey } : { id },
120
+ ] as const;
108
121
  }
109
122
 
110
123
  export function getOptimisticTitleIsGenerated(
@@ -279,15 +292,31 @@ export function useEvents(
279
292
  from?: string,
280
293
  to?: string,
281
294
  overlayEmails?: string[],
295
+ calendarSourceKeys?: string[],
282
296
  ) {
283
- const params = buildEventsParams(from, to, overlayEmails);
284
-
285
- return useActionQuery<CalendarEvent[]>("list-events", params, {
297
+ const params = buildEventsParams(from, to, overlayEmails, calendarSourceKeys);
298
+ const demo = isSharedCalendarDemo();
299
+ const live = useActionQuery<CalendarEvent[]>("list-events", params, {
300
+ enabled: !demo,
286
301
  retry: false,
287
302
  staleTime: 30_000,
288
303
  gcTime: 30 * 60 * 1000,
289
304
  placeholderData: keepPreviousData,
290
305
  });
306
+ const fixture = useQuery({
307
+ queryKey: ["shared-calendar-demo-events", params],
308
+ queryFn: async () => {
309
+ const selected = new Set(calendarSourceKeys ?? []);
310
+ return SHARED_CALENDAR_DEMO_EVENTS.filter((event) => {
311
+ if (!event.calendarSourceKey) return true;
312
+ return selected.size === 0 || selected.has(event.calendarSourceKey);
313
+ });
314
+ },
315
+ enabled: demo,
316
+ staleTime: Infinity,
317
+ });
318
+
319
+ return demo ? fixture : live;
291
320
  }
292
321
 
293
322
  type OverlaySourceCoverage = {
@@ -351,8 +380,10 @@ export function prefetchEvents(
351
380
  from: string,
352
381
  to: string,
353
382
  overlayEmails?: string[],
383
+ calendarSourceKeys?: string[],
354
384
  ) {
355
- const params = buildEventsParams(from, to, overlayEmails);
385
+ if (isSharedCalendarDemo()) return;
386
+ const params = buildEventsParams(from, to, overlayEmails, calendarSourceKeys);
356
387
  return queryClient.prefetchQuery({
357
388
  queryKey: ["action", "list-events", params],
358
389
  queryFn: () =>
@@ -362,8 +393,12 @@ export function prefetchEvents(
362
393
  });
363
394
  }
364
395
 
365
- export function useEvent(id: string) {
366
- return useActionQuery<CalendarEvent>("get-event", { id }, { enabled: !!id });
396
+ export function useEvent(id: string, calendarSourceKey?: string) {
397
+ return useActionQuery<CalendarEvent>(
398
+ "get-event",
399
+ calendarSourceKey ? { id, calendarSourceKey } : { id },
400
+ { enabled: !!id },
401
+ );
367
402
  }
368
403
 
369
404
  export function useCreateEvent() {
@@ -7,6 +7,11 @@ import type { GoogleAuthStatus } from "@shared/api";
7
7
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8
8
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
9
 
10
+ import {
11
+ isSharedCalendarDemo,
12
+ SHARED_CALENDAR_DEMO_STATUS,
13
+ } from "@/lib/shared-calendar-demo";
14
+
10
15
  export interface DesktopAuthIssue {
11
16
  error?: string;
12
17
  message?: string;
@@ -149,14 +154,16 @@ async function fetchJson<T>(input: string, init?: RequestInit): Promise<T> {
149
154
  }
150
155
 
151
156
  export function useGoogleAuthStatus() {
157
+ const demo = isSharedCalendarDemo();
152
158
  return useQuery<GoogleAuthStatus>({
153
159
  queryKey: ["google-status"],
154
160
  queryFn: async () => {
161
+ if (demo) return SHARED_CALENDAR_DEMO_STATUS;
155
162
  return fetchJson<GoogleAuthStatus>(
156
163
  agentNativePath("/_agent-native/google/status"),
157
164
  );
158
165
  },
159
- staleTime: 30_000,
166
+ staleTime: demo ? Infinity : 30_000,
160
167
  });
161
168
  }
162
169
 
@@ -0,0 +1,41 @@
1
+ import { useFeatureFlag } from "@agent-native/core/client/feature-flags";
2
+ import { useActionQuery } from "@agent-native/core/client/hooks";
3
+ import type { GoogleCalendarSource } from "@shared/api";
4
+ import { SHARED_GOOGLE_CALENDARS } from "@shared/feature-flags";
5
+ import { useQuery } from "@tanstack/react-query";
6
+
7
+ import {
8
+ isSharedCalendarDemo,
9
+ SHARED_CALENDAR_DEMO_SOURCES,
10
+ } from "@/lib/shared-calendar-demo";
11
+
12
+ export function useGoogleCalendars() {
13
+ const demo = isSharedCalendarDemo();
14
+ const flagEnabled = useFeatureFlag(SHARED_GOOGLE_CALENDARS.key);
15
+ const enabled = demo || flagEnabled;
16
+ const live = useActionQuery<{
17
+ calendars: GoogleCalendarSource[];
18
+ errors: Array<{ email: string; error: string }>;
19
+ }>(
20
+ "list-google-calendars",
21
+ {},
22
+ {
23
+ enabled: enabled && !demo,
24
+ retry: false,
25
+ staleTime: 30_000,
26
+ },
27
+ );
28
+ const fixture = useQuery({
29
+ queryKey: ["shared-calendar-demo-sources"],
30
+ queryFn: async () => SHARED_CALENDAR_DEMO_SOURCES,
31
+ enabled: demo,
32
+ staleTime: Infinity,
33
+ });
34
+ return {
35
+ ...(demo ? fixture : live),
36
+ data: demo ? fixture.data : live.data?.calendars,
37
+ sourceErrors: demo ? [] : (live.data?.errors ?? []),
38
+ enabled,
39
+ demo,
40
+ };
41
+ }
@@ -13,6 +13,7 @@ import {
13
13
  type CalendarColorMode,
14
14
  type CalendarViewPreferences,
15
15
  } from "@/lib/calendar-view-preferences";
16
+ import { isSharedCalendarDemo } from "@/lib/shared-calendar-demo";
16
17
 
17
18
  export type ViewPreferences = CalendarViewPreferences;
18
19
 
@@ -129,6 +130,7 @@ async function readAppStatePreferences(): Promise<CalendarViewPreferences | null
129
130
  }
130
131
 
131
132
  export function useViewPreferences() {
133
+ const demo = isSharedCalendarDemo();
132
134
  const [prefs, setPrefs] = useState<ViewPreferences>(load);
133
135
  const accountColorRequestIds = useRef<Record<string, number>>({});
134
136
  const accountModeRequestIds = useRef<Record<string, number>>({});
@@ -150,6 +152,7 @@ export function useViewPreferences() {
150
152
  }, []);
151
153
 
152
154
  useEffect(() => {
155
+ if (demo) return;
153
156
  let cancelled = false;
154
157
  let timeout: number | undefined;
155
158
 
@@ -220,7 +223,7 @@ export function useViewPreferences() {
220
223
  document.removeEventListener("visibilitychange", handleVisibilityChange);
221
224
  if (timeout) window.clearTimeout(timeout);
222
225
  };
223
- }, []);
226
+ }, [demo]);
224
227
 
225
228
  const update = useCallback((patch: Partial<ViewPreferences>) => {
226
229
  setPrefs((prev) => {
@@ -258,6 +261,8 @@ export function useViewPreferences() {
258
261
  return next;
259
262
  });
260
263
 
264
+ if (demo) return;
265
+
261
266
  callAction("update-calendar-visual-preferences", {
262
267
  accountEmail,
263
268
  accountColor,
@@ -333,7 +338,7 @@ export function useViewPreferences() {
333
338
  }
334
339
  });
335
340
  },
336
- [],
341
+ [demo],
337
342
  );
338
343
 
339
344
  const updateAccountColorMode = useCallback(
@@ -356,6 +361,8 @@ export function useViewPreferences() {
356
361
  return next;
357
362
  });
358
363
 
364
+ if (demo) return;
365
+
359
366
  callAction("update-calendar-visual-preferences", {
360
367
  accountEmail,
361
368
  accountColorMode,
@@ -416,8 +423,53 @@ export function useViewPreferences() {
416
423
  }
417
424
  });
418
425
  },
419
- [],
426
+ [demo],
427
+ );
428
+
429
+ const updateGoogleCalendarVisibility = useCallback(
430
+ (sourceKey: string, visible: boolean) => {
431
+ const rollbackPrefs = prefs;
432
+ const next = normalizeCalendarViewPreferences({
433
+ ...prefs,
434
+ googleCalendarVisibility: {
435
+ ...prefs.googleCalendarVisibility,
436
+ [sourceKey]: visible,
437
+ },
438
+ });
439
+ setPrefs(next);
440
+ save(next);
441
+ window.dispatchEvent(new Event(CALENDAR_VIEW_PREFERENCES_CHANGE_EVENT));
442
+
443
+ if (demo) return;
444
+
445
+ callAction("update-calendar-visual-preferences", {
446
+ googleCalendarSourceKey: sourceKey,
447
+ googleCalendarVisible: visible,
448
+ }).catch(() => {
449
+ setPrefs((current) => {
450
+ if (current.googleCalendarVisibility[sourceKey] !== visible) {
451
+ return current;
452
+ }
453
+ const next = normalizeCalendarViewPreferences({
454
+ ...current,
455
+ googleCalendarVisibility: rollbackPrefs.googleCalendarVisibility,
456
+ });
457
+ save(next);
458
+ window.dispatchEvent(
459
+ new Event(CALENDAR_VIEW_PREFERENCES_CHANGE_EVENT),
460
+ );
461
+ return next;
462
+ });
463
+ });
464
+ },
465
+ [demo, prefs],
420
466
  );
421
467
 
422
- return { prefs, update, updateAccountColor, updateAccountColorMode };
468
+ return {
469
+ prefs,
470
+ update,
471
+ updateAccountColor,
472
+ updateAccountColorMode,
473
+ updateGoogleCalendarVisibility,
474
+ };
423
475
  }
@@ -0,0 +1,187 @@
1
+ import type {
2
+ CalendarEvent,
3
+ GoogleAuthStatus,
4
+ GoogleCalendarSource,
5
+ } from "@shared/api";
6
+ import { CALENDAR_COLORS } from "@shared/calendar-view-preferences";
7
+
8
+ export function isSharedCalendarDemo(): boolean {
9
+ return (
10
+ import.meta.env.DEV &&
11
+ new URLSearchParams(window.location.search).get("calendarDemo") === "shared"
12
+ );
13
+ }
14
+
15
+ export const SHARED_CALENDAR_DEMO_STATUS: GoogleAuthStatus = {
16
+ connected: true,
17
+ configured: true,
18
+ accounts: [
19
+ { email: "alice@builder.io" },
20
+ { email: "emdistal@gmail.com" },
21
+ { email: "tempoimmaterial@gmail.com" },
22
+ ],
23
+ };
24
+
25
+ export const SHARED_CALENDAR_DEMO_SOURCES: GoogleCalendarSource[] = [
26
+ {
27
+ sourceKey: "demo:builder-primary",
28
+ accountEmail: "alice@builder.io",
29
+ calendarId: "alice@builder.io",
30
+ name: "Alice · Builder",
31
+ color: CALENDAR_COLORS[1],
32
+ selected: true,
33
+ primary: true,
34
+ accessRole: "owner",
35
+ readOnly: false,
36
+ },
37
+ {
38
+ sourceKey: "demo:personal-primary",
39
+ accountEmail: "emdistal@gmail.com",
40
+ calendarId: "emdistal@gmail.com",
41
+ name: "Alice Alexandra",
42
+ color: CALENDAR_COLORS[2],
43
+ selected: true,
44
+ primary: true,
45
+ accessRole: "owner",
46
+ readOnly: false,
47
+ },
48
+ {
49
+ sourceKey: "demo:friends",
50
+ accountEmail: "emdistal@gmail.com",
51
+ calendarId: "friends@example.com",
52
+ name: "Friends",
53
+ color: CALENDAR_COLORS[3],
54
+ selected: true,
55
+ primary: false,
56
+ accessRole: "reader",
57
+ readOnly: true,
58
+ },
59
+ {
60
+ sourceKey: "demo:tempo-primary",
61
+ accountEmail: "tempoimmaterial@gmail.com",
62
+ calendarId: "tempoimmaterial@gmail.com",
63
+ name: "Tempo Immaterial",
64
+ color: CALENDAR_COLORS[5],
65
+ selected: true,
66
+ primary: true,
67
+ accessRole: "owner",
68
+ readOnly: false,
69
+ },
70
+ {
71
+ sourceKey: "demo:studio",
72
+ accountEmail: "tempoimmaterial@gmail.com",
73
+ calendarId: "studio@example.com",
74
+ name: "Studio schedule",
75
+ color: CALENDAR_COLORS[4],
76
+ selected: false,
77
+ primary: false,
78
+ accessRole: "reader",
79
+ readOnly: true,
80
+ },
81
+ ];
82
+
83
+ const now = "2026-09-01T12:00:00.000Z";
84
+
85
+ export const SHARED_CALENDAR_DEMO_EVENTS: CalendarEvent[] = [
86
+ demoEvent({
87
+ id: "google-builder-enablement",
88
+ title: "GTM Enablement",
89
+ start: "2026-09-02T12:00:00-04:00",
90
+ end: "2026-09-02T13:00:00-04:00",
91
+ accountEmail: "alice@builder.io",
92
+ calendarId: "alice@builder.io",
93
+ calendarName: "Alice · Builder",
94
+ color: CALENDAR_COLORS[1],
95
+ }),
96
+ demoEvent({
97
+ id: "google-personal-dermatology",
98
+ title: "dermatology appt",
99
+ start: "2026-09-01T15:20:00-04:00",
100
+ end: "2026-09-01T16:20:00-04:00",
101
+ accountEmail: "emdistal@gmail.com",
102
+ calendarId: "emdistal@gmail.com",
103
+ calendarName: "Alice Alexandra",
104
+ color: CALENDAR_COLORS[2],
105
+ }),
106
+ demoEvent({
107
+ id: "google-friends-cottage",
108
+ title: "Magpie Cottage Sale",
109
+ start: "2026-09-05T10:00:00-04:00",
110
+ end: "2026-09-05T16:00:00-04:00",
111
+ accountEmail: "emdistal@gmail.com",
112
+ calendarId: "friends@example.com",
113
+ calendarName: "Friends",
114
+ calendarAccessRole: "reader",
115
+ calendarReadOnly: true,
116
+ color: CALENDAR_COLORS[3],
117
+ }),
118
+ demoEvent({
119
+ id: "google-friends-birthday",
120
+ title: "Matt’s 33⅓ Birthday",
121
+ start: "2026-09-04T18:00:00-04:00",
122
+ end: "2026-09-04T19:00:00-04:00",
123
+ accountEmail: "emdistal@gmail.com",
124
+ calendarId: "friends@example.com",
125
+ calendarName: "Friends",
126
+ calendarAccessRole: "reader",
127
+ calendarReadOnly: true,
128
+ color: CALENDAR_COLORS[3],
129
+ }),
130
+ demoEvent({
131
+ id: "google-personal-dinner",
132
+ title: "dinner with sarah",
133
+ start: "2026-09-03T17:30:00-04:00",
134
+ end: "2026-09-03T20:00:00-04:00",
135
+ accountEmail: "emdistal@gmail.com",
136
+ calendarId: "emdistal@gmail.com",
137
+ calendarName: "Alice Alexandra",
138
+ color: CALENDAR_COLORS[2],
139
+ }),
140
+ demoEvent({
141
+ id: "google-tempo-studio",
142
+ title: "Open studio",
143
+ start: "2026-09-03T14:00:00-04:00",
144
+ end: "2026-09-03T16:00:00-04:00",
145
+ accountEmail: "tempoimmaterial@gmail.com",
146
+ calendarId: "studio@example.com",
147
+ calendarName: "Studio schedule",
148
+ calendarAccessRole: "reader",
149
+ calendarReadOnly: true,
150
+ color: CALENDAR_COLORS[4],
151
+ }),
152
+ ];
153
+
154
+ function demoEvent(
155
+ event: Pick<
156
+ CalendarEvent,
157
+ | "id"
158
+ | "title"
159
+ | "start"
160
+ | "end"
161
+ | "accountEmail"
162
+ | "calendarId"
163
+ | "calendarName"
164
+ | "calendarAccessRole"
165
+ | "calendarReadOnly"
166
+ | "color"
167
+ >,
168
+ ): CalendarEvent {
169
+ const source = SHARED_CALENDAR_DEMO_SOURCES.find(
170
+ (candidate) =>
171
+ candidate.accountEmail === event.accountEmail &&
172
+ candidate.calendarId === event.calendarId,
173
+ );
174
+ return {
175
+ ...event,
176
+ calendarSourceKey: source?.sourceKey ?? "",
177
+ calendarPrimary: source?.primary,
178
+ description: "",
179
+ location: "",
180
+ allDay: false,
181
+ source: "google",
182
+ googleEventId: event.id.replace(/^google-/, ""),
183
+ htmlLink: "https://calendar.google.com/calendar/u/0/r",
184
+ createdAt: now,
185
+ updatedAt: now,
186
+ };
187
+ }