@agent-native/core 0.154.4 → 0.155.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.
Files changed (207) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/account-health/SKILL.md +76 -0
  3. package/corpus/templates/analytics/.agents/skills/bigquery/SKILL.md +3 -0
  4. package/corpus/templates/analytics/.agents/skills/cross-source-analysis/SKILL.md +2 -1
  5. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +6 -5
  6. package/corpus/templates/analytics/AGENTS.md +3 -0
  7. package/corpus/templates/analytics/actions/account-deep-dive.ts +3 -2
  8. package/corpus/templates/analytics/actions/bigquery.ts +1 -1
  9. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -1
  10. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  11. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyCombobox.tsx +12 -4
  12. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyValueCombobox.tsx +5 -2
  13. package/corpus/templates/analytics/app/pages/adhoc/explorer/property-label.ts +32 -0
  14. package/corpus/templates/analytics/changelog/2026-08-13-account-health-readouts-verify-customer-scope-and-contract-metrics.md +7 -0
  15. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +43 -17
  16. package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +17 -0
  17. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  18. package/corpus/templates/calendar/AGENTS.md +2 -0
  19. package/corpus/templates/calendar/DEVELOPING.md +4 -4
  20. package/corpus/templates/calendar/actions/get-settings.ts +4 -11
  21. package/corpus/templates/calendar/actions/update-availability.ts +15 -4
  22. package/corpus/templates/calendar/actions/update-settings.ts +15 -4
  23. package/corpus/templates/calendar/app/components/booking/DatePicker.tsx +9 -3
  24. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +21 -19
  25. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +9 -6
  26. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +25 -3
  27. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +10 -3
  28. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +9 -9
  29. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -3
  30. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +10 -4
  31. package/corpus/templates/calendar/app/i18n-data.ts +30 -0
  32. package/corpus/templates/calendar/app/lib/availability-schedule.ts +53 -0
  33. package/corpus/templates/calendar/app/lib/event-form-utils.ts +5 -0
  34. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +90 -28
  35. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +135 -61
  36. package/corpus/templates/calendar/app/pages/BookingPage.tsx +2 -0
  37. package/corpus/templates/calendar/app/pages/CalendarView.tsx +30 -15
  38. package/corpus/templates/calendar/app/pages/Settings.tsx +60 -1
  39. package/corpus/templates/calendar/changelog/2026-08-13-find-a-time-now-closes-from-a-clear-header-action.md +6 -0
  40. package/corpus/templates/calendar/server/handlers/bookings.ts +2 -1
  41. package/corpus/templates/calendar/server/handlers/settings.ts +12 -16
  42. package/corpus/templates/calendar/shared/api.ts +1 -0
  43. package/corpus/templates/calendar/shared/availability-schedule.ts +64 -0
  44. package/corpus/templates/calendar/shared/calendar-week.ts +21 -0
  45. package/corpus/templates/calendar/shared/settings.ts +44 -0
  46. package/corpus/templates/clips/AGENTS.md +2 -0
  47. package/corpus/templates/clips/actions/add-comment.ts +5 -2
  48. package/corpus/templates/clips/actions/finalize-recording.ts +31 -1
  49. package/corpus/templates/clips/actions/get-recording-player-data.ts +9 -0
  50. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +21 -1
  51. package/corpus/templates/clips/actions/reply-to-comment.ts +5 -2
  52. package/corpus/templates/clips/actions/update-comment.ts +9 -2
  53. package/corpus/templates/clips/app/components/library/search-bar.tsx +26 -13
  54. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +135 -3
  55. package/corpus/templates/clips/app/components/player/comments-panel.tsx +5 -99
  56. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +7 -3
  57. package/corpus/templates/clips/app/components/player/video-player.tsx +84 -24
  58. package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +11 -3
  59. package/corpus/templates/clips/app/i18n/en-US.ts +12 -0
  60. package/corpus/templates/clips/app/lib/public-meeting.ts +11 -3
  61. package/corpus/templates/clips/app/lib/search-focus.ts +15 -0
  62. package/corpus/templates/clips/app/root.tsx +2 -1
  63. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +94 -26
  64. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +24 -7
  65. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +8 -4
  66. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -7
  67. package/corpus/templates/clips/app/routes/share.$shareId.tsx +8 -4
  68. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +72 -26
  69. package/corpus/templates/clips/changelog/2026-08-13-inline-markdown-comments.md +6 -0
  70. package/corpus/templates/clips/changelog/2026-08-13-private-meeting-agent-links.md +6 -0
  71. package/corpus/templates/clips/desktop/src/app.tsx +116 -19
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +13 -29
  73. package/corpus/templates/clips/desktop/src/lib/recording-request.ts +39 -0
  74. package/corpus/templates/clips/desktop/src/lib/settings-navigation.ts +11 -0
  75. package/corpus/templates/clips/desktop/src/lib/whisper-model-picker.ts +7 -0
  76. package/corpus/templates/clips/desktop/src/styles.css +117 -0
  77. package/corpus/templates/clips/server/db/index.ts +10 -0
  78. package/corpus/templates/clips/server/db/schema.ts +1 -0
  79. package/corpus/templates/clips/server/lib/recordings.ts +3 -1
  80. package/corpus/templates/clips/server/lib/seekable-media-state.ts +90 -0
  81. package/corpus/templates/clips/server/plugins/db.ts +5 -0
  82. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +37 -4
  83. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +16 -5
  84. package/corpus/templates/clips/shared/clips-ai-prefs.ts +3 -0
  85. package/corpus/templates/clips/shared/meeting-agent-access.ts +5 -0
  86. package/corpus/templates/content/.agents/skills/content/SKILL.md +2 -2
  87. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +20 -4
  88. package/corpus/templates/content/actions/_database-row-mutation.ts +1490 -0
  89. package/corpus/templates/content/actions/_database-utils.ts +44 -0
  90. package/corpus/templates/content/actions/_property-utils.ts +4 -1
  91. package/corpus/templates/content/actions/add-comment.ts +2 -1
  92. package/corpus/templates/content/actions/add-database-item.ts +62 -240
  93. package/corpus/templates/content/actions/configure-document-property.ts +165 -1
  94. package/corpus/templates/content/actions/delete-document-property.ts +18 -0
  95. package/corpus/templates/content/actions/set-document-property.ts +78 -1
  96. package/corpus/templates/content/actions/update-comment.ts +1 -1
  97. package/corpus/templates/content/actions/update-database-item.ts +62 -0
  98. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +43 -796
  99. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +24 -15
  100. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +10 -8
  101. package/corpus/templates/content/app/hooks/use-content-database.ts +33 -32
  102. package/corpus/templates/content/changelog/2026-08-10-database-row-actions-now-validate-exact-schemas-and-safe-ret.md +6 -0
  103. package/corpus/templates/content/parity/matrix.md +1 -1
  104. package/corpus/templates/content/parity/matrix.ts +2 -0
  105. package/corpus/templates/content/server/db/schema.ts +39 -3
  106. package/corpus/templates/content/server/plugins/db.ts +30 -0
  107. package/corpus/templates/content/shared/api.ts +75 -2
  108. package/corpus/templates/design/actions/generate-design.ts +10 -7
  109. package/corpus/templates/design/actions/present-design-variants.ts +35 -6
  110. package/corpus/templates/design/actions/run-design-audit.ts +33 -0
  111. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-catches-a-screen-that-renders-behind-an.md +6 -0
  112. package/corpus/templates/design/shared/design-review.ts +1 -0
  113. package/corpus/templates/design/shared/html-integrity.ts +66 -0
  114. package/corpus/templates/dispatch/.env.example +5 -0
  115. package/corpus/templates/dispatch/AGENTS.md +24 -19
  116. package/corpus/templates/dispatch/app/i18n-data.ts +20 -0
  117. package/corpus/templates/dispatch/server/lib/identity-sso.ts +21 -90
  118. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +3 -12
  119. package/corpus/templates/dispatch/shared/feature-flags.ts +4 -0
  120. package/corpus/templates/factory/AGENTS.md +5 -1
  121. package/corpus/templates/factory/README.md +23 -0
  122. package/corpus/templates/factory/actions/view-screen.ts +17 -0
  123. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +239 -0
  124. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +2 -0
  125. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +130 -7
  126. package/corpus/templates/factory/app/i18n/en-US.ts +20 -0
  127. package/corpus/templates/factory/app/routes/factory.tsx +41 -0
  128. package/corpus/templates/factory/package.json +1 -0
  129. package/corpus/templates/factory/server/factory-graph/contracts.ts +2 -0
  130. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -0
  131. package/corpus/templates/factory/server/plugins/dispatch-db.ts +1 -0
  132. package/corpus/templates/factory/server/plugins/setup-dispatch.ts +3 -0
  133. package/corpus/templates/mail/app/lib/native-mcp-exclusions.ts +3 -0
  134. package/corpus/templates/mail/vite.config.ts +7 -0
  135. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +7 -2
  136. package/corpus/templates/plan/actions/resolve-plan-comment.ts +2 -2
  137. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  138. package/corpus/templates/plan/app/pages/PlansPage.tsx +29 -20
  139. package/corpus/templates/slides/actions/add-slide-comment.ts +1 -1
  140. package/corpus/templates/slides/actions/update-slide-comment.ts +1 -1
  141. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +9 -6
  142. package/dist/action.d.ts +22 -0
  143. package/dist/action.js +27 -0
  144. package/dist/agent/production-agent.js +5 -1
  145. package/dist/agent/thread-data-builder.js +17 -1
  146. package/dist/cli/skills-content/help.js +6 -4
  147. package/dist/cli/skills.js +17 -2
  148. package/dist/cli/workspacify.d.ts +5 -0
  149. package/dist/cli/workspacify.js +65 -8
  150. package/dist/client/AgentPanel.d.ts +5 -0
  151. package/dist/client/AgentPanel.js +20 -4
  152. package/dist/client/AssistantChat.d.ts +21 -0
  153. package/dist/client/AssistantChat.js +66 -68
  154. package/dist/client/active-run-state.d.ts +14 -1
  155. package/dist/client/active-run-state.js +0 -0
  156. package/dist/client/agent-chat-adapter.js +107 -5
  157. package/dist/client/analytics.js +9 -2
  158. package/dist/client/chat/tool-call-display.js +4 -4
  159. package/dist/client/chat-first/apps-rail.js +9 -4
  160. package/dist/client/chat-first/primary-nav.d.ts +2 -1
  161. package/dist/client/chat-first/primary-nav.js +3 -3
  162. package/dist/client/chat-first/types.d.ts +5 -1
  163. package/dist/client/markdown/InlineMarkdown.d.ts +23 -0
  164. package/dist/client/markdown/InlineMarkdown.js +65 -0
  165. package/dist/client/markdown/index.d.ts +1 -0
  166. package/dist/client/markdown/index.js +1 -0
  167. package/dist/client/review/ReviewThreadPanel.js +2 -1
  168. package/dist/client/run-stream-ownership.d.ts +39 -0
  169. package/dist/client/run-stream-ownership.js +65 -0
  170. package/dist/collab/routes.d.ts +1 -1
  171. package/dist/collab/struct-routes.d.ts +1 -1
  172. package/dist/feature-flags/index.d.ts +1 -1
  173. package/dist/feature-flags/index.js +1 -1
  174. package/dist/feature-flags/store.d.ts +10 -0
  175. package/dist/feature-flags/store.js +112 -1
  176. package/dist/guards/no-env-credentials.js +4 -0
  177. package/dist/index.browser.d.ts +1 -1
  178. package/dist/index.browser.js +1 -1
  179. package/dist/index.d.ts +2 -2
  180. package/dist/index.js +2 -2
  181. package/dist/integrations/adapters/slack.js +5 -1
  182. package/dist/localization/default-messages.d.ts +1 -0
  183. package/dist/localization/default-messages.js +1 -0
  184. package/dist/notifications/routes.d.ts +1 -1
  185. package/dist/observability/routes.d.ts +1 -1
  186. package/dist/progress/routes.d.ts +1 -1
  187. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  188. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  189. package/dist/resources/agents.js +40 -5
  190. package/dist/resources/handlers.d.ts +1 -1
  191. package/dist/resources/metadata.d.ts +10 -0
  192. package/dist/resources/metadata.js +7 -3
  193. package/dist/review/actions/create-review-comment.js +1 -1
  194. package/dist/review/actions/reply-review-comment.js +1 -1
  195. package/dist/review/actions/resolve-review-thread.js +1 -1
  196. package/dist/server/action-routes.js +42 -8
  197. package/dist/server/agent-discovery.js +14 -3
  198. package/dist/server/core-routes-plugin.js +1 -1
  199. package/dist/server/identity-sso-migrations.d.ts +7 -0
  200. package/dist/server/identity-sso-migrations.js +36 -0
  201. package/dist/server/identity-sso-store.d.ts +3 -2
  202. package/dist/server/identity-sso-store.js +8 -3
  203. package/dist/server/realtime-token.d.ts +1 -1
  204. package/dist/server/release-migrations.js +4 -0
  205. package/dist/tracking/providers.js +13 -3
  206. package/dist/workspace-files/tool.js +1 -1
  207. package/package.json +3 -2
@@ -1,9 +1,13 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { getRequestUserEmail } from "@agent-native/core/server";
3
- import { putUserSetting, putSetting } from "@agent-native/core/settings";
3
+ import {
4
+ getUserSetting,
5
+ putUserSetting,
6
+ putSetting,
7
+ } from "@agent-native/core/settings";
4
8
  import { z } from "zod";
5
9
 
6
- import type { Settings } from "../shared/api.js";
10
+ import { normalizeCalendarSettings } from "../shared/settings.js";
7
11
 
8
12
  export default defineAction({
9
13
  description: "Update calendar settings",
@@ -20,12 +24,19 @@ export default defineAction({
20
24
  .positive()
21
25
  .optional()
22
26
  .describe("Default event duration in minutes"),
27
+ weekStart: z
28
+ .enum(["sunday", "monday"])
29
+ .optional()
30
+ .describe("First day shown in calendar weeks"),
23
31
  }),
24
32
  run: async (args) => {
25
33
  const email = getRequestUserEmail();
26
34
  if (!email) throw new Error("no authenticated user");
27
- // The frontend sends the full settings object as the body
28
- const settings = args as unknown as Settings;
35
+ const currentSettings = await getUserSetting(email, "calendar-settings");
36
+ const settings = normalizeCalendarSettings({
37
+ ...normalizeCalendarSettings(currentSettings),
38
+ ...args,
39
+ });
29
40
  const settingsRecord = settings as unknown as Record<string, unknown>;
30
41
  await putUserSetting(email, "calendar-settings", settingsRecord);
31
42
  await putSetting("calendar-settings", settingsRecord);
@@ -1,4 +1,5 @@
1
1
  import type { AvailabilityConfig } from "@shared/api";
2
+ import { getWeekdayOrder } from "@shared/calendar-week";
2
3
  import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
3
4
  import {
4
5
  startOfMonth,
@@ -31,6 +32,7 @@ interface DatePickerProps {
31
32
  availabilityLoading?: boolean;
32
33
  viewMonth: Date;
33
34
  onViewMonthChange: (month: Date) => void;
35
+ weekStartsOn?: 0 | 1;
34
36
  }
35
37
 
36
38
  const WEEKDAY_HEADERS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
@@ -53,12 +55,16 @@ export function DatePicker({
53
55
  availabilityLoading = false,
54
56
  viewMonth,
55
57
  onViewMonthChange,
58
+ weekStartsOn = 0,
56
59
  }: DatePickerProps) {
57
60
  const monthStart = startOfMonth(viewMonth);
58
61
  const monthEnd = endOfMonth(viewMonth);
59
- const calendarStart = startOfWeek(monthStart);
60
- const calendarEnd = endOfWeek(monthEnd);
62
+ const calendarStart = startOfWeek(monthStart, { weekStartsOn });
63
+ const calendarEnd = endOfWeek(monthEnd, { weekStartsOn });
61
64
  const days = eachDayOfInterval({ start: calendarStart, end: calendarEnd });
65
+ const weekdayHeaders = getWeekdayOrder(weekStartsOn).map(
66
+ (day) => WEEKDAY_HEADERS[day],
67
+ );
62
68
  const availableDateSet = useMemo(
63
69
  () => new Set(availableDates),
64
70
  [availableDates],
@@ -103,7 +109,7 @@ export function DatePicker({
103
109
 
104
110
  {/* Weekday headers */}
105
111
  <div className="grid grid-cols-7 mb-1">
106
- {WEEKDAY_HEADERS.map((d) => (
112
+ {weekdayHeaders.map((d) => (
107
113
  <div
108
114
  key={d}
109
115
  className="py-1 text-center text-xs font-medium text-muted-foreground"
@@ -76,8 +76,8 @@ import {
76
76
  createReminderDraft,
77
77
  dateTimeInTimezoneToIso,
78
78
  getEventEndValidationMessage,
79
- getLocalTimezone,
80
79
  remindersToDraftState,
80
+ resolveEventTimezone,
81
81
  type AttachmentDraft,
82
82
  type ReminderDraft,
83
83
  type ReminderMode,
@@ -236,7 +236,7 @@ export function CreateEventPopover({
236
236
  const defaultDurationMinutes = Number.isFinite(rawDefaultDuration)
237
237
  ? Math.max(5, rawDefaultDuration)
238
238
  : 30;
239
- const defaultTimezone = settings?.timezone || getLocalTimezone();
239
+ const defaultTimezone = settings?.timezone || resolveEventTimezone();
240
240
  const fallbackStart = "09:00";
241
241
  const fallbackEnd = addMinutesToTimeString(
242
242
  fallbackStart,
@@ -279,6 +279,7 @@ export function CreateEventPopover({
279
279
  const [findTimeOpen, setFindTimeOpen] = useState(false);
280
280
  const isOutOfOffice = eventType === "outOfOffice";
281
281
  const timedOnlyStatus = eventType === "focusTime";
282
+ const eventTimezone = resolveEventTimezone(timezone);
282
283
 
283
284
  const createEvent = useCreateEvent();
284
285
  const delEvent = useDeleteEvent();
@@ -308,8 +309,9 @@ export function CreateEventPopover({
308
309
  }
309
310
 
310
311
  const nextDate = format(defaultDate || new Date(), "yyyy-MM-dd");
311
- const draftTimezone =
312
- draft?.startTimeZone || draft?.endTimeZone || defaultTimezone;
312
+ const draftTimezone = resolveEventTimezone(
313
+ draft?.startTimeZone || draft?.endTimeZone || defaultTimezone,
314
+ );
313
315
  const initKey = buildEventFormInitializationKey({
314
316
  draftId: draft?.id,
315
317
  date: nextDate,
@@ -455,12 +457,12 @@ export function CreateEventPopover({
455
457
  ? date
456
458
  : effectiveAllDay
457
459
  ? new Date(`${date}T00:00:00`).toISOString()
458
- : dateTimeInTimezoneToIso(date, startTime, timezone);
460
+ : dateTimeInTimezoneToIso(date, startTime, eventTimezone);
459
461
  const endValue = fullDayOutOfOffice
460
462
  ? endDate
461
463
  : effectiveAllDay
462
464
  ? allDayEnd.toISOString()
463
- : dateTimeInTimezoneToIso(endDate, endTime, timezone);
465
+ : dateTimeInTimezoneToIso(endDate, endTime, eventTimezone);
464
466
  const attachmentResult = validateAttachmentDrafts(attachments);
465
467
  const reminderPatch = buildReminderPayload(reminderMode, reminders);
466
468
  const nextDraft: CalendarEventDraft = {
@@ -470,8 +472,8 @@ export function CreateEventPopover({
470
472
  description: isOutOfOffice ? "" : description,
471
473
  start: startValue,
472
474
  end: endValue,
473
- startTimeZone: effectiveAllDay ? undefined : timezone,
474
- endTimeZone: effectiveAllDay ? undefined : timezone,
475
+ startTimeZone: effectiveAllDay ? undefined : eventTimezone,
476
+ endTimeZone: effectiveAllDay ? undefined : eventTimezone,
475
477
  location: isOutOfOffice ? "" : location,
476
478
  allDay: effectiveAllDay,
477
479
  fullDay: fullDayOutOfOffice,
@@ -547,7 +549,7 @@ export function CreateEventPopover({
547
549
  declineMessage,
548
550
  availability,
549
551
  visibility,
550
- timezone,
552
+ eventTimezone,
551
553
  colorId,
552
554
  reminderMode,
553
555
  reminders,
@@ -578,7 +580,7 @@ export function CreateEventPopover({
578
580
  time: allDay
579
581
  ? t("eventForm.allDay")
580
582
  : t("eventForm.ai.timeRange", { startTime, endTime }),
581
- timezone,
583
+ timezone: eventTimezone,
582
584
  location: location || t("eventForm.ai.none"),
583
585
  attendees:
584
586
  attendees.map((attendee) => attendee.email).join(", ") ||
@@ -636,11 +638,11 @@ export function CreateEventPopover({
636
638
  const effectiveAllDay = allDay && !isOutOfOffice && !timedOnlyStatus;
637
639
  const currentStartISO =
638
640
  !effectiveAllDay && date && startTime
639
- ? dateTimeInTimezoneToIso(date, startTime, timezone)
641
+ ? dateTimeInTimezoneToIso(date, startTime, eventTimezone)
640
642
  : undefined;
641
643
  const currentEndISO =
642
644
  !effectiveAllDay && endDate && endTime
643
- ? dateTimeInTimezoneToIso(endDate, endTime, timezone)
645
+ ? dateTimeInTimezoneToIso(endDate, endTime, eventTimezone)
644
646
  : undefined;
645
647
  const findTimeDurationMinutes =
646
648
  currentStartISO && currentEndISO
@@ -654,8 +656,8 @@ export function CreateEventPopover({
654
656
  : defaultDurationMinutes;
655
657
 
656
658
  function handleSelectFindTimeSlot(slot: { start: string; end: string }) {
657
- const startParts = dateTimePartsInTimezone(slot.start, timezone);
658
- const endParts = dateTimePartsInTimezone(slot.end, timezone);
659
+ const startParts = dateTimePartsInTimezone(slot.start, eventTimezone);
660
+ const endParts = dateTimePartsInTimezone(slot.end, eventTimezone);
659
661
  if (!startParts || !endParts) return;
660
662
  setAllDay(false);
661
663
  setDate(startParts.date);
@@ -708,12 +710,12 @@ export function CreateEventPopover({
708
710
  ? date
709
711
  : effectiveAllDay
710
712
  ? new Date(`${date}T00:00:00`).toISOString()
711
- : dateTimeInTimezoneToIso(date, startTime, timezone);
713
+ : dateTimeInTimezoneToIso(date, startTime, eventTimezone);
712
714
  const endValue = fullDayOutOfOffice
713
715
  ? endDate
714
716
  : effectiveAllDay
715
717
  ? allDayEnd.toISOString()
716
- : dateTimeInTimezoneToIso(endDate, endTime, timezone);
718
+ : dateTimeInTimezoneToIso(endDate, endTime, eventTimezone);
717
719
 
718
720
  if (
719
721
  fullDayOutOfOffice
@@ -761,8 +763,8 @@ export function CreateEventPopover({
761
763
  description: isOutOfOffice ? "" : description,
762
764
  start: startValue,
763
765
  end: endValue,
764
- startTimeZone: effectiveAllDay ? undefined : timezone,
765
- endTimeZone: effectiveAllDay ? undefined : timezone,
766
+ startTimeZone: effectiveAllDay ? undefined : eventTimezone,
767
+ endTimeZone: effectiveAllDay ? undefined : eventTimezone,
766
768
  location: isOutOfOffice ? "" : location,
767
769
  accountEmail,
768
770
  allDay: effectiveAllDay,
@@ -1459,7 +1461,7 @@ export function CreateEventPopover({
1459
1461
  (draft ? t("eventForm.invite") : t("eventForm.newEventLower"))
1460
1462
  }
1461
1463
  date={date}
1462
- timezone={timezone}
1464
+ timezone={eventTimezone}
1463
1465
  durationMinutes={findTimeDurationMinutes}
1464
1466
  attendees={attendees}
1465
1467
  accountEmail={accountEmail}
@@ -1441,7 +1441,7 @@ export function EventDetailPopover({
1441
1441
  side={isMobile ? "bottom" : "right"}
1442
1442
  sideOffset={isMobile ? 6 : 8}
1443
1443
  collisionPadding={12}
1444
- className="flex max-h-[90vh] w-[calc(100vw-2rem)] flex-col overflow-hidden p-0 sm:w-[420px]"
1444
+ className="flex max-h-[var(--radix-popover-content-available-height)] w-[min(420px,calc(100vw-2rem))] flex-col overflow-hidden p-0"
1445
1445
  onClick={(e) => e.stopPropagation()}
1446
1446
  onOpenAutoFocus={(e) => {
1447
1447
  e.preventDefault();
@@ -1671,12 +1671,15 @@ export function EventDetailPopover({
1671
1671
  />
1672
1672
  </>
1673
1673
  )}
1674
+ <span className="ml-auto">
1675
+ <TimezonePickerPopover
1676
+ compact
1677
+ value={editTimezone}
1678
+ label={t("eventForm.timezone")}
1679
+ onChange={handleInlineTimezoneChange}
1680
+ />
1681
+ </span>
1674
1682
  </div>
1675
- <TimezonePickerPopover
1676
- value={editTimezone}
1677
- label={t("eventForm.timezone")}
1678
- onChange={handleInlineTimezoneChange}
1679
- />
1680
1683
  </>
1681
1684
  )}
1682
1685
  </div>
@@ -5,12 +5,14 @@ import type {
5
5
  FindTimeResult,
6
6
  FindTimeSlot,
7
7
  } from "@shared/api";
8
+ import { getWeekStartsOn } from "@shared/calendar-week";
8
9
  import {
9
10
  IconAlertCircle,
10
11
  IconCalendarTime,
11
12
  IconChevronLeft,
12
13
  IconChevronRight,
13
14
  IconLoader2,
15
+ IconX,
14
16
  IconUsers,
15
17
  } from "@tabler/icons-react";
16
18
  import {
@@ -31,11 +33,13 @@ import { Badge } from "@/components/ui/badge";
31
33
  import { Button } from "@/components/ui/button";
32
34
  import {
33
35
  Dialog,
36
+ DialogClose,
34
37
  DialogContent,
35
38
  DialogDescription,
36
39
  DialogTitle,
37
40
  } from "@/components/ui/dialog";
38
41
  import { Skeleton } from "@/components/ui/skeleton";
42
+ import { useSettings } from "@/hooks/use-settings";
39
43
  import { dateTimeInTimezoneToIso } from "@/lib/event-form-utils";
40
44
  import { cn } from "@/lib/utils";
41
45
 
@@ -188,6 +192,8 @@ export function FindTimePanel({
188
192
  isTakeover = false,
189
193
  }: FindTimePanelProps) {
190
194
  const t = useT();
195
+ const { data: settings } = useSettings();
196
+ const weekStartsOn = getWeekStartsOn(settings?.weekStart);
191
197
  const [anchorDate, setAnchorDate] = useState(() => parseDateOnly(date));
192
198
 
193
199
  useEffect(() => {
@@ -220,7 +226,10 @@ export function FindTimePanel({
220
226
  return () => window.removeEventListener("keydown", handleKeyDown, true);
221
227
  }, [isTakeover]);
222
228
 
223
- const weekStart = useMemo(() => startOfWeek(anchorDate), [anchorDate]);
229
+ const weekStart = useMemo(
230
+ () => startOfWeek(anchorDate, { weekStartsOn }),
231
+ [anchorDate, weekStartsOn],
232
+ );
224
233
  const days = useMemo(
225
234
  () =>
226
235
  eachDayOfInterval({
@@ -663,9 +672,11 @@ export function FindTimeTakeover({
663
672
  <Dialog open={open} onOpenChange={onOpenChange}>
664
673
  <DialogContent
665
674
  data-find-time-takeover
666
- className="flex h-[100dvh] max-h-[100dvh] w-screen max-w-none translate-x-[-50%] translate-y-[-50%] flex-col gap-0 overflow-hidden border-0 p-0 shadow-xl sm:rounded-none"
675
+ hideClose
676
+ overlayClassName="z-[310]"
677
+ className="z-[320] flex h-[100dvh] max-h-[100dvh] w-screen max-w-none translate-x-[-50%] translate-y-[-50%] flex-col gap-0 overflow-hidden border-0 p-0 shadow-xl sm:rounded-none"
667
678
  >
668
- <header className="flex h-14 shrink-0 items-center border-b border-border px-4 pr-12 md:px-6">
679
+ <header className="flex h-14 shrink-0 items-center border-b border-border px-4 md:px-6">
669
680
  <div className="min-w-0">
670
681
  <DialogTitle className="truncate text-base">
671
682
  {dialogTitle}
@@ -679,6 +690,17 @@ export function FindTimeTakeover({
679
690
  {subtitle || t("findTime.takeoverDescription")}
680
691
  </DialogDescription>
681
692
  </div>
693
+ <DialogClose asChild>
694
+ <button
695
+ type="button"
696
+ aria-label={t("eventDialog.close")}
697
+ title={t("eventDialog.close")}
698
+ className="ml-auto flex h-10 shrink-0 items-center gap-2 rounded-md px-3 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
699
+ >
700
+ <IconX className="h-4 w-4" />
701
+ <span>{t("eventDialog.close")}</span>
702
+ </button>
703
+ </DialogClose>
682
704
  </header>
683
705
  <div className="min-h-0 flex-1 overflow-auto p-4 md:p-6">
684
706
  <FindTimePanel
@@ -193,10 +193,12 @@ export function TimezonePickerPopover({
193
193
  value,
194
194
  onChange,
195
195
  label,
196
+ compact = false,
196
197
  }: {
197
198
  value: string;
198
199
  onChange: (value: string) => void;
199
200
  label: string;
201
+ compact?: boolean;
200
202
  }) {
201
203
  const t = useT();
202
204
  const [open, setOpen] = useState(false);
@@ -208,11 +210,16 @@ export function TimezonePickerPopover({
208
210
  type="button"
209
211
  variant="ghost"
210
212
  size="sm"
211
- className="h-auto justify-start rounded px-1.5 py-0.5 text-sm font-normal text-muted-foreground hover:bg-muted hover:text-foreground"
213
+ className={cn(
214
+ compact
215
+ ? "size-7 rounded-full p-0 text-muted-foreground hover:bg-muted hover:text-foreground"
216
+ : "h-auto justify-start rounded px-1.5 py-0.5 text-sm font-normal text-muted-foreground hover:bg-muted hover:text-foreground",
217
+ )}
212
218
  aria-label={label}
219
+ title={compact ? label : undefined}
213
220
  >
214
- <IconWorld className="mr-2 size-4 shrink-0" />
215
- {formatTimezoneLabel(value)}
221
+ <IconWorld className={cn("size-4 shrink-0", !compact && "mr-2")} />
222
+ {!compact && formatTimezoneLabel(value)}
216
223
  </Button>
217
224
  </PopoverTrigger>
218
225
  <PopoverContent
@@ -1,4 +1,5 @@
1
1
  import type { CalendarEvent } from "@shared/api";
2
+ import { getWeekdayOrder } from "@shared/calendar-week";
2
3
  import {
3
4
  startOfMonth,
4
5
  endOfMonth,
@@ -49,6 +50,7 @@ interface MonthViewProps {
49
50
  ) => void;
50
51
  onDraftDiscard?: (eventId: string) => void;
51
52
  isLoading?: boolean;
53
+ weekStartsOn?: 0 | 1;
52
54
  }
53
55
 
54
56
  // Skeleton pill widths per day-of-week (Sun–Sat), empty = no skeletons
@@ -85,6 +87,7 @@ export const MonthView = memo(function MonthView({
85
87
  onDraftCreate,
86
88
  onDraftDiscard,
87
89
  isLoading = false,
90
+ weekStartsOn = 0,
88
91
  }: MonthViewProps) {
89
92
  const isMobile = useIsMobile();
90
93
  const { prefs } = useViewPreferences();
@@ -93,8 +96,8 @@ export const MonthView = memo(function MonthView({
93
96
 
94
97
  const monthStart = startOfMonth(selectedDate);
95
98
  const monthEnd = endOfMonth(selectedDate);
96
- const calendarStart = startOfWeek(monthStart);
97
- const calendarEnd = endOfWeek(monthEnd);
99
+ const calendarStart = startOfWeek(monthStart, { weekStartsOn });
100
+ const calendarEnd = endOfWeek(monthEnd, { weekStartsOn });
98
101
  const allDays = eachDayOfInterval({
99
102
  start: calendarStart,
100
103
  end: calendarEnd,
@@ -103,13 +106,10 @@ export const MonthView = memo(function MonthView({
103
106
  ? allDays.filter((d) => d.getDay() !== 0 && d.getDay() !== 6)
104
107
  : allDays;
105
108
  const colCount = prefs.hideWeekends ? 5 : 7;
106
- const headers = prefs.hideWeekends
107
- ? (isMobile ? WEEKDAY_HEADERS_SHORT : WEEKDAY_HEADERS).filter(
108
- (_, i) => i !== 0 && i !== 6,
109
- )
110
- : isMobile
111
- ? WEEKDAY_HEADERS_SHORT
112
- : WEEKDAY_HEADERS;
109
+ const headerLabels = isMobile ? WEEKDAY_HEADERS_SHORT : WEEKDAY_HEADERS;
110
+ const headers = getWeekdayOrder(weekStartsOn)
111
+ .filter((day) => !prefs.hideWeekends || (day !== 0 && day !== 6))
112
+ .map((day) => headerLabels[day]);
113
113
 
114
114
  // Pre-group events by every day they overlap (not just their start day) so
115
115
  // multi-day events keep appearing as the grid moves past their start date.
@@ -107,6 +107,7 @@ interface WeekViewProps {
107
107
  ) => void;
108
108
  onDraftDiscard?: (eventId: string) => void;
109
109
  isLoading?: boolean;
110
+ weekStartsOn?: 0 | 1;
110
111
  }
111
112
 
112
113
  // [startHour, startMin, durationMin, widthPct] per day column (Sun–Sat)
@@ -559,6 +560,7 @@ export const WeekView = memo(function WeekView({
559
560
  onDraftCreate,
560
561
  onDraftDiscard,
561
562
  isLoading = false,
563
+ weekStartsOn = 0,
562
564
  }: WeekViewProps) {
563
565
  const t = useT();
564
566
  const workingLocationLabels = useMemo(
@@ -606,8 +608,14 @@ export const WeekView = memo(function WeekView({
606
608
  }, []);
607
609
 
608
610
  const { prefs } = useViewPreferences();
609
- const weekStart = useMemo(() => startOfWeek(selectedDate), [selectedDate]);
610
- const weekEnd = useMemo(() => endOfWeek(selectedDate), [selectedDate]);
611
+ const weekStart = useMemo(
612
+ () => startOfWeek(selectedDate, { weekStartsOn }),
613
+ [selectedDate, weekStartsOn],
614
+ );
615
+ const weekEnd = useMemo(
616
+ () => endOfWeek(selectedDate, { weekStartsOn }),
617
+ [selectedDate, weekStartsOn],
618
+ );
611
619
  // Stable day/hour arrays — recomputed only when the week or weekend
612
620
  // visibility actually changes, so memoized children (event buttons) don't
613
621
  // see a new array identity on every drag/focus re-render.
@@ -1408,7 +1416,7 @@ export const WeekView = memo(function WeekView({
1408
1416
 
1409
1417
  {/* Skeleton events when loading */}
1410
1418
  {isLoading &&
1411
- WEEK_SKELETONS[dayIndex]?.map(
1419
+ WEEK_SKELETONS[day.getDay()]?.map(
1412
1420
  ([startHour, startMin, duration, widthPct], i) => {
1413
1421
  const topPx =
1414
1422
  ((startHour - START_HOUR) * 60 + startMin) *
@@ -5,6 +5,7 @@ import { openCommandMenu } from "@agent-native/core/client/navigation";
5
5
  import { OrgSwitcher } from "@agent-native/core/client/org";
6
6
  import { FeedbackButton } from "@agent-native/core/client/ui";
7
7
  import { SidebarFooterActions } from "@agent-native/toolkit/app-shell";
8
+ import { getWeekdayOrder, getWeekStartsOn } from "@shared/calendar-week";
8
9
  import {
9
10
  IconCalendar,
10
11
  IconSettings,
@@ -77,6 +78,7 @@ import {
77
78
  useRemoveOverlayPerson,
78
79
  useUpdateOverlayPersonColor,
79
80
  } from "@/hooks/use-overlay-people";
81
+ import { useSettings } from "@/hooks/use-settings";
80
82
  import { useViewPreferences } from "@/hooks/use-view-preferences";
81
83
  import {
82
84
  CALENDAR_COLORS,
@@ -200,6 +202,8 @@ function MiniCalendar({
200
202
  }) {
201
203
  const [viewMonth, setViewMonth] = useState(() => startOfMonth(selectedDate));
202
204
  const [pickerOpen, setPickerOpen] = useState(false);
205
+ const { data: settings } = useSettings();
206
+ const weekStartsOn = getWeekStartsOn(settings?.weekStart);
203
207
 
204
208
  // Sync viewMonth when selectedDate changes to a different month
205
209
  useEffect(() => {
@@ -211,8 +215,8 @@ function MiniCalendar({
211
215
  const days = useMemo(() => {
212
216
  const monthStart = startOfMonth(viewMonth);
213
217
  const monthEnd = endOfMonth(viewMonth);
214
- const calStart = startOfWeek(monthStart);
215
- const calEnd = endOfWeek(monthEnd);
218
+ const calStart = startOfWeek(monthStart, { weekStartsOn });
219
+ const calEnd = endOfWeek(monthEnd, { weekStartsOn });
216
220
 
217
221
  const result: Date[] = [];
218
222
  let current = calStart;
@@ -221,9 +225,11 @@ function MiniCalendar({
221
225
  current = addDays(current, 1);
222
226
  }
223
227
  return result;
224
- }, [viewMonth]);
228
+ }, [viewMonth, weekStartsOn]);
225
229
 
226
- const weekdays = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
230
+ const weekdays = getWeekdayOrder(weekStartsOn).map(
231
+ (day) => ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"][day],
232
+ );
227
233
 
228
234
  return (
229
235
  <div className="px-3 py-3">
@@ -265,6 +265,9 @@ const enUS = {
265
265
  defaultDurationLabel: "Default event duration (minutes)",
266
266
  defaultDurationHelp:
267
267
  "Default length for new calendar events and booking slots. Booking links can override this per link.",
268
+ weekStartLabel: "Week starts on",
269
+ weekStartSunday: "Sunday - Saturday",
270
+ weekStartMonday: "Monday - Sunday",
268
271
  },
269
272
  eventDialog: {
270
273
  eventUpdated: "Event updated",
@@ -4303,6 +4306,9 @@ const translatedCalendarExtras = {
4303
4306
  defaultDurationLabel: "默认事件时长(分钟)",
4304
4307
  defaultDurationHelp:
4305
4308
  "新日历事件和预约时段的默认长度。预约链接可按链接覆盖此设置。",
4309
+ weekStartLabel: "每周从哪天开始",
4310
+ weekStartSunday: "周日 - 周六",
4311
+ weekStartMonday: "周一 - 周日",
4306
4312
  },
4307
4313
  eventDialog: {
4308
4314
  eventUpdated: "事件已更新",
@@ -4372,6 +4378,9 @@ const translatedCalendarExtras = {
4372
4378
  defaultDurationLabel: "Duración predeterminada del evento (minutos)",
4373
4379
  defaultDurationHelp:
4374
4380
  "Duración predeterminada para eventos nuevos y franjas de reserva. Cada enlace de reserva puede anularla.",
4381
+ weekStartLabel: "La semana empieza el",
4382
+ weekStartSunday: "Domingo - sábado",
4383
+ weekStartMonday: "Lunes - domingo",
4375
4384
  },
4376
4385
  eventDialog: {
4377
4386
  eventUpdated: "Evento actualizado",
@@ -4442,6 +4451,9 @@ const translatedCalendarExtras = {
4442
4451
  defaultDurationLabel: "Durée d'événement par défaut (minutes)",
4443
4452
  defaultDurationHelp:
4444
4453
  "Durée par défaut des nouveaux événements et créneaux de réservation. Chaque lien de réservation peut la remplacer.",
4454
+ weekStartLabel: "La semaine commence le",
4455
+ weekStartSunday: "Dimanche - samedi",
4456
+ weekStartMonday: "Lundi - dimanche",
4445
4457
  },
4446
4458
  eventDialog: {
4447
4459
  eventUpdated: "Événement mis à jour",
@@ -4512,6 +4524,9 @@ const translatedCalendarExtras = {
4512
4524
  defaultDurationLabel: "Standarddauer für Ereignisse (Minuten)",
4513
4525
  defaultDurationHelp:
4514
4526
  "Standardlänge für neue Kalenderereignisse und Buchungsslots. Buchungslinks können sie pro Link überschreiben.",
4527
+ weekStartLabel: "Wochenbeginn",
4528
+ weekStartSunday: "Sonntag - Samstag",
4529
+ weekStartMonday: "Montag - Sonntag",
4515
4530
  },
4516
4531
  eventDialog: {
4517
4532
  eventUpdated: "Ereignis aktualisiert",
@@ -4581,6 +4596,9 @@ const translatedCalendarExtras = {
4581
4596
  defaultDurationLabel: "デフォルトのイベント時間(分)",
4582
4597
  defaultDurationHelp:
4583
4598
  "新しいカレンダーイベントと予約枠のデフォルト時間です。予約リンクごとに上書きできます。",
4599
+ weekStartLabel: "週の開始日",
4600
+ weekStartSunday: "日曜日 - 土曜日",
4601
+ weekStartMonday: "月曜日 - 日曜日",
4584
4602
  },
4585
4603
  eventDialog: {
4586
4604
  eventUpdated: "イベントを更新しました",
@@ -4647,6 +4665,9 @@ const translatedCalendarExtras = {
4647
4665
  defaultDurationLabel: "기본 이벤트 시간(분)",
4648
4666
  defaultDurationHelp:
4649
4667
  "새 캘린더 이벤트와 예약 슬롯의 기본 길이입니다. 예약 링크별로 재정의할 수 있습니다.",
4668
+ weekStartLabel: "주 시작일",
4669
+ weekStartSunday: "일요일 - 토요일",
4670
+ weekStartMonday: "월요일 - 일요일",
4650
4671
  },
4651
4672
  eventDialog: {
4652
4673
  eventUpdated: "이벤트가 업데이트되었습니다",
@@ -4716,6 +4737,9 @@ const translatedCalendarExtras = {
4716
4737
  defaultDurationLabel: "Duração padrão do evento (minutos)",
4717
4738
  defaultDurationHelp:
4718
4739
  "Duração padrão para novos eventos e horários de agendamento. Links de agendamento podem substituir isso por link.",
4740
+ weekStartLabel: "A semana começa no",
4741
+ weekStartSunday: "Domingo - sábado",
4742
+ weekStartMonday: "Segunda-feira - domingo",
4719
4743
  },
4720
4744
  eventDialog: {
4721
4745
  eventUpdated: "Evento atualizado",
@@ -4784,6 +4808,9 @@ const translatedCalendarExtras = {
4784
4808
  defaultDurationLabel: "डिफ़ॉल्ट इवेंट अवधि (मिनट)",
4785
4809
  defaultDurationHelp:
4786
4810
  "नए कैलेंडर इवेंट और बुकिंग स्लॉट की डिफ़ॉल्ट लंबाई। बुकिंग लिंक इसे प्रति लिंक बदल सकते हैं।",
4811
+ weekStartLabel: "सप्ताह की शुरुआत",
4812
+ weekStartSunday: "रविवार - शनिवार",
4813
+ weekStartMonday: "सोमवार - रविवार",
4787
4814
  },
4788
4815
  eventDialog: {
4789
4816
  eventUpdated: "इवेंट अपडेट हो गया",
@@ -4851,6 +4878,9 @@ const translatedCalendarExtras = {
4851
4878
  defaultDurationLabel: "مدة الحدث الافتراضية (بالدقائق)",
4852
4879
  defaultDurationHelp:
4853
4880
  "المدة الافتراضية لأحداث التقويم الجديدة وفتحات الحجز. يمكن لروابط الحجز تجاوزها لكل رابط.",
4881
+ weekStartLabel: "يبدأ الأسبوع يوم",
4882
+ weekStartSunday: "الأحد - السبت",
4883
+ weekStartMonday: "الاثنين - الأحد",
4854
4884
  },
4855
4885
  eventDialog: {
4856
4886
  eventUpdated: "تم تحديث الحدث",
@@ -0,0 +1,53 @@
1
+ import type { DaySchedule, TimeSlot } from "../../shared/api";
2
+
3
+ export const DEFAULT_TIME_SLOT: TimeSlot = {
4
+ start: "09:00",
5
+ end: "17:00",
6
+ };
7
+
8
+ export function getEditableTimeSlots(day: DaySchedule): TimeSlot[] {
9
+ return day.slots.length > 0 ? day.slots : [{ ...DEFAULT_TIME_SLOT }];
10
+ }
11
+
12
+ export function setDayEnabled(day: DaySchedule, enabled: boolean): DaySchedule {
13
+ return {
14
+ ...day,
15
+ enabled,
16
+ slots: enabled ? getEditableTimeSlots(day) : day.slots,
17
+ };
18
+ }
19
+
20
+ export function updateTimeSlot(
21
+ day: DaySchedule,
22
+ slotIndex: number,
23
+ field: "start" | "end",
24
+ value: string,
25
+ ): DaySchedule {
26
+ return {
27
+ ...day,
28
+ slots: getEditableTimeSlots(day).map((slot, index) =>
29
+ index === slotIndex ? { ...slot, [field]: value } : slot,
30
+ ),
31
+ };
32
+ }
33
+
34
+ export function addTimeSlot(day: DaySchedule): DaySchedule {
35
+ return {
36
+ ...day,
37
+ slots: [...getEditableTimeSlots(day), { ...DEFAULT_TIME_SLOT }],
38
+ };
39
+ }
40
+
41
+ export function removeTimeSlot(
42
+ day: DaySchedule,
43
+ slotIndex: number,
44
+ ): DaySchedule {
45
+ const slots = getEditableTimeSlots(day).filter(
46
+ (_, index) => index !== slotIndex,
47
+ );
48
+
49
+ return {
50
+ ...day,
51
+ slots: slots.length > 0 ? slots : [{ ...DEFAULT_TIME_SLOT }],
52
+ };
53
+ }
@@ -230,6 +230,11 @@ export function getLocalTimezone() {
230
230
  return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
231
231
  }
232
232
 
233
+ /** New event drafts follow the viewer's browser zone unless they name one. */
234
+ export function resolveEventTimezone(timezone?: string | null) {
235
+ return timezone?.trim() || getLocalTimezone();
236
+ }
237
+
233
238
  function getTimezoneOffsetMs(date: Date, timezone: string) {
234
239
  const parts = new Intl.DateTimeFormat("en-US", {
235
240
  timeZone: timezone,