@agent-native/core 0.176.1 → 0.177.0-nightly-20260828155142

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 (71) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/set-workspace-feature-flag.ts +2 -2
  3. package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +16 -1
  4. package/corpus/templates/analytics/changelog/2026-08-27-show-analytics-s-own-registered-flags-in-fleet-feature-flag-.md +6 -0
  5. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +204 -32
  6. package/corpus/templates/assets/changelog/2026-08-26-templates-across-brand-kits.md +1 -0
  7. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +61 -68
  8. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +9 -3
  9. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +3 -3
  10. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +8 -3
  11. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +12 -3
  12. package/corpus/templates/calendar/changelog/2026-08-28-all-day-events-stay-in-the-compact-top-bar-in-day-and-week-v.md +6 -0
  13. package/corpus/templates/calendar/changelog/2026-08-28-event-creators-can-choose-whether-a-meeting-shows-as-free-or.md +6 -0
  14. package/corpus/templates/clips/app/global.css +0 -27
  15. package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
  16. package/corpus/templates/clips/app/routes/download.tsx +131 -16
  17. package/corpus/templates/clips/app/routes/share.$shareId.tsx +35 -20
  18. package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +77 -22
  19. package/corpus/templates/forms/actions/response-insights.ts +4 -4
  20. package/corpus/templates/forms/actions/retry-response-deliveries.ts +26 -0
  21. package/corpus/templates/forms/server/db/schema.ts +64 -17
  22. package/corpus/templates/forms/server/handlers/submissions.ts +1022 -94
  23. package/corpus/templates/forms/server/lib/integrations.ts +100 -33
  24. package/corpus/templates/forms/server/middleware/00-public-cors.ts +1 -1
  25. package/corpus/templates/forms/server/plugins/db.ts +40 -0
  26. package/corpus/templates/plan/agent-native.json +1 -4
  27. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +6 -1
  28. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +2 -2
  29. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -6
  30. package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +78 -12
  31. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +4 -4
  32. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +111 -27
  33. package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +20 -34
  34. package/corpus/templates/slides/app/components/editor/slide-style.ts +2 -0
  35. package/corpus/templates/slides/app/global.css +35 -0
  36. package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +133 -5
  37. package/corpus/templates/slides/app/lib/slide-image-replacement.ts +675 -0
  38. package/corpus/templates/slides/app/lib/validate-native-slide-html.ts +1 -0
  39. package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -44
  40. package/corpus/templates/slides/app/root.tsx +1 -22
  41. package/corpus/templates/slides/changelog/2026-08-27-image-fit-controls-apply-cover-crops-and-position.md +6 -0
  42. package/corpus/templates/slides/changelog/2026-08-28-shared-slides-with-a-style-block-render-again.md +6 -0
  43. package/dist/action.d.ts +3 -1
  44. package/dist/client/FeedbackButton.d.ts +12 -0
  45. package/dist/client/FeedbackButton.js +62 -28
  46. package/dist/client/app-providers.js +13 -1
  47. package/dist/client/feedback-context.d.ts +1 -0
  48. package/dist/client/feedback-context.js +3 -2
  49. package/dist/client/host-bridge.d.ts +1 -0
  50. package/dist/client/observability/ThumbsFeedback.js +73 -10
  51. package/dist/client/webmcp.d.ts +4 -0
  52. package/dist/client/webmcp.js +51 -4
  53. package/dist/collab/routes.d.ts +1 -1
  54. package/dist/collab/struct-routes.d.ts +1 -1
  55. package/dist/index.browser.d.ts +1 -1
  56. package/dist/index.browser.js +1 -1
  57. package/dist/observability/routes.d.ts +5 -5
  58. package/dist/observability/routes.js +8 -2
  59. package/dist/observability/store.d.ts +1 -1
  60. package/dist/observability/store.js +18 -3
  61. package/dist/observability/types.d.ts +1 -0
  62. package/dist/progress/routes.d.ts +1 -1
  63. package/dist/resources/handlers.d.ts +2 -2
  64. package/dist/secrets/routes.d.ts +9 -9
  65. package/dist/server/action-routes.d.ts +1 -6
  66. package/dist/server/action-routes.js +58 -17
  67. package/dist/server/agent-chat-plugin.js +8 -1
  68. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  69. package/dist/server/transcribe-voice.d.ts +1 -1
  70. package/docs/content/webmcp.mdx +31 -12
  71. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 8675
34
+ - template files: 8681
@@ -30,7 +30,7 @@ const schema = z.discriminatedUnion("operation", [
30
30
  ]);
31
31
  export default defineAction({
32
32
  description:
33
- "Persist one feature-flag change on a trusted organization app. The app target is resolved only through the organization directory.",
33
+ "Persist one feature-flag change on Analytics or a trusted organization app. Analytics uses its local flag action; peer targets are resolved through the organization directory.",
34
34
  schema,
35
35
  agentInputSchema: z.object({
36
36
  appId: z.string(),
@@ -40,6 +40,6 @@ export default defineAction({
40
40
  }),
41
41
  run: async (args, ctx) => {
42
42
  const admin = await requireAnalyticsAdminContext(ctx);
43
- return setWorkspaceFeatureFlag(admin, args);
43
+ return setWorkspaceFeatureFlag(admin, args, ctx);
44
44
  },
45
45
  });
@@ -89,6 +89,10 @@ function asDirectory(value: unknown): FlagDirectory {
89
89
  return value && typeof value === "object" ? (value as FlagDirectory) : {};
90
90
  }
91
91
 
92
+ export function hasFleetApps(value: unknown): boolean {
93
+ return (asDirectory(value).apps?.length ?? 0) > 0;
94
+ }
95
+
92
96
  function isReady(app: FlagApp) {
93
97
  return app.state === "ready" || app.status === "ready";
94
98
  }
@@ -188,7 +192,10 @@ export function FeatureFlagsFleetPanel() {
188
192
  const directory = asDirectory(flags.data);
189
193
  const apps = directory.apps ?? [];
190
194
  if (flags.isLoading) return <PanelLoading />;
191
- if (flags.error || directory.directoryStatus === "unavailable")
195
+ if (
196
+ flags.error ||
197
+ (directory.directoryStatus === "unavailable" && !hasFleetApps(directory))
198
+ )
192
199
  return (
193
200
  <StatusState
194
201
  title={t("agents.flagsUnavailable")}
@@ -226,6 +233,14 @@ export function FeatureFlagsFleetPanel() {
226
233
  {t("agents.reloadFlags")}
227
234
  </Button>
228
235
  </div>
236
+ {directory.directoryStatus === "unavailable" ? (
237
+ <p
238
+ className="rounded-lg border px-4 py-3 text-sm text-muted-foreground"
239
+ role="status"
240
+ >
241
+ {t("agents.flagsUnreachable")}
242
+ </p>
243
+ ) : null}
229
244
  {apps.map((app) => (
230
245
  <section key={appId(app)} className="overflow-hidden rounded-lg border">
231
246
  <div className="flex items-center justify-between gap-3 border-b bg-muted/30 px-4 py-3">
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-27
4
+ ---
5
+
6
+ Show Analytics's own registered flags in fleet feature flag management.
@@ -4,14 +4,18 @@ import { signA2AToken } from "@agent-native/core/a2a";
4
4
  import {
5
5
  ActionContractError,
6
6
  AgentActionStopError,
7
+ type ActionRunContext,
7
8
  } from "@agent-native/core/action";
8
9
  import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
10
+ import listLocalFeatureFlagsAction from "@agent-native/core/feature-flags/actions/list-feature-flags";
11
+ import setLocalFeatureFlagAction from "@agent-native/core/feature-flags/actions/set-feature-flag";
9
12
  import {
10
13
  fetchOrgApps,
11
14
  fetchOrgAppsResult,
12
15
  type OrgApp,
13
16
  } from "@agent-native/core/mcp";
14
17
  import { getOrgDomain } from "@agent-native/core/org";
18
+ import { getAppConfig } from "@agent-native/core/server";
15
19
 
16
20
  import {
17
21
  RESILIENT_FLEET_FLAG_DIRECTORY,
@@ -23,6 +27,7 @@ const TARGET_TIMEOUT_MS = 3_000;
23
27
  const DIRECTORY_ATTEMPTS = 2;
24
28
  const VERIFICATION_ATTEMPTS = 2;
25
29
  const CONCURRENCY = 4;
30
+ const ANALYTICS_APP_ID = "analytics";
26
31
 
27
32
  export type FleetFlagState =
28
33
  | "ready"
@@ -91,6 +96,37 @@ export function workspaceFeatureFlagTargetInput(
91
96
  : rawTargetInput;
92
97
  }
93
98
 
99
+ function mutationValidationExpectation(
100
+ admin: AnalyticsAdminContext,
101
+ input: WorkspaceFeatureFlagMutationInput,
102
+ orgDomain: string,
103
+ ) {
104
+ return {
105
+ key: input.key,
106
+ orgDomain,
107
+ allowExplicitNoOrgTarget: true,
108
+ ...(input.operation === "replace-rules" && input.rules
109
+ ? {
110
+ rules: {
111
+ mode: input.rules.mode,
112
+ emails: input.rules.emails ?? [],
113
+ orgIds: input.rules.orgIds ?? [],
114
+ percentage: input.rules.percentage ?? 0,
115
+ },
116
+ }
117
+ : input.operation === "off"
118
+ ? {
119
+ rules: {
120
+ mode: "off",
121
+ emails: [],
122
+ orgIds: [],
123
+ percentage: 0,
124
+ },
125
+ }
126
+ : { enabledForEmail: admin.userEmail }),
127
+ };
128
+ }
129
+
94
130
  export function validateWorkspaceFeatureFlagMutation(
95
131
  body: unknown,
96
132
  expected: {
@@ -179,6 +215,54 @@ function targetOrigin(app: OrgApp): string {
179
215
  return new URL(app.url).origin;
180
216
  }
181
217
 
218
+ function localAnalyticsApp(): OrgApp {
219
+ const configuredUrl = getAppConfig().app.url;
220
+ const url = configuredUrl ?? "http://analytics.local";
221
+ return {
222
+ id: ANALYTICS_APP_ID,
223
+ name: "Analytics",
224
+ url,
225
+ a2aUrl: url,
226
+ };
227
+ }
228
+
229
+ function localActionContext(
230
+ admin: AnalyticsAdminContext,
231
+ actionName: "list-feature-flags" | "set-feature-flag",
232
+ sourceContext?: ActionRunContext,
233
+ ) {
234
+ return {
235
+ ...sourceContext,
236
+ caller: sourceContext?.caller ?? ("http" as const),
237
+ userEmail: admin.userEmail,
238
+ orgId: admin.orgId,
239
+ appId: ANALYTICS_APP_ID,
240
+ actionName,
241
+ };
242
+ }
243
+
244
+ async function listLocalAnalyticsFeatureFlags(admin: AnalyticsAdminContext) {
245
+ return classifyWorkspaceFeatureFlagList(localAnalyticsApp(), {
246
+ status: 200,
247
+ body: await listLocalFeatureFlagsAction.run(
248
+ {},
249
+ localActionContext(admin, "list-feature-flags"),
250
+ ),
251
+ });
252
+ }
253
+
254
+ function unreachableLocalAnalyticsEntry(): FleetFlagApp {
255
+ const app = localAnalyticsApp();
256
+ return {
257
+ appId: app.id,
258
+ appName: app.name,
259
+ appOrigin: targetOrigin(app),
260
+ state: "unreachable",
261
+ flags: [],
262
+ reason: "target-execution",
263
+ };
264
+ }
265
+
182
266
  async function delegatedToken(
183
267
  admin: AnalyticsAdminContext,
184
268
  origin: string,
@@ -298,6 +382,18 @@ function targetFailure(error: unknown): WorkspaceFeatureFlagFailure {
298
382
  return new WorkspaceFeatureFlagFailure(reason);
299
383
  }
300
384
 
385
+ function localMutationFailure(error: unknown): WorkspaceFeatureFlagFailure {
386
+ const statusCode =
387
+ error && typeof error === "object" && "statusCode" in error
388
+ ? (error as { statusCode?: unknown }).statusCode
389
+ : undefined;
390
+ return new WorkspaceFeatureFlagFailure(
391
+ statusCode === 401 || statusCode === 403
392
+ ? "authorization"
393
+ : "target-action",
394
+ );
395
+ }
396
+
301
397
  async function resolveTargetApp(
302
398
  admin: AnalyticsAdminContext,
303
399
  appId: string,
@@ -471,13 +567,20 @@ async function mapBounded<T, R>(
471
567
  export async function listWorkspaceFeatureFlags(
472
568
  admin: AnalyticsAdminContext,
473
569
  ): Promise<WorkspaceFeatureFlagsResult> {
474
- const apps = await fetchOrgApps({
475
- selfId: "analytics",
476
- includeDirectoryApp: true,
477
- serviceOrgId: admin.orgId,
478
- });
479
- if (apps.length === 0) return { directoryStatus: "unavailable", apps: [] };
480
- const entries = await mapBounded(apps, async (app) => {
570
+ const [localEntry, directoryResult] = await Promise.all([
571
+ listLocalAnalyticsFeatureFlags(admin).catch(() =>
572
+ unreachableLocalAnalyticsEntry(),
573
+ ),
574
+ fetchOrgAppsResult({
575
+ selfId: ANALYTICS_APP_ID,
576
+ includeDirectoryApp: true,
577
+ serviceOrgId: admin.orgId,
578
+ }),
579
+ ]);
580
+ if (directoryResult.status === "unavailable")
581
+ return { directoryStatus: "unavailable", apps: [localEntry] };
582
+ const directoryApps = directoryResult.apps;
583
+ const entries = await mapBounded(directoryApps, async (app) => {
481
584
  try {
482
585
  return classifyWorkspaceFeatureFlagList(
483
586
  app,
@@ -494,13 +597,101 @@ export async function listWorkspaceFeatureFlags(
494
597
  };
495
598
  }
496
599
  });
497
- return { directoryStatus: "available", apps: entries };
600
+ return { directoryStatus: "available", apps: [localEntry, ...entries] };
498
601
  }
499
602
 
500
603
  export async function setWorkspaceFeatureFlag(
501
604
  admin: AnalyticsAdminContext,
502
605
  input: WorkspaceFeatureFlagMutationInput,
606
+ sourceContext?: ActionRunContext,
503
607
  ): Promise<WorkspaceFeatureFlagMutationResult> {
608
+ if (input.appId === ANALYTICS_APP_ID) {
609
+ if (input.operation === "replace-rules" && !input.rules)
610
+ throw new WorkspaceFeatureFlagFailure("target-action");
611
+ const localRules = input.rules as
612
+ | {
613
+ mode: "off" | "on" | "rules";
614
+ emails?: string[];
615
+ orgIds?: string[];
616
+ percentage?: number;
617
+ }
618
+ | undefined;
619
+ const targetInput =
620
+ input.operation === "replace-rules"
621
+ ? {
622
+ operation: input.operation,
623
+ key: input.key,
624
+ rules: localRules!,
625
+ }
626
+ : { operation: input.operation, key: input.key };
627
+ let result;
628
+ try {
629
+ result = await setLocalFeatureFlagAction.run(
630
+ targetInput,
631
+ localActionContext(admin, "set-feature-flag", sourceContext),
632
+ );
633
+ } catch (error) {
634
+ throw localMutationFailure(error);
635
+ }
636
+ const orgDomain = result.scope.orgDomain ?? "";
637
+ let mutation: TargetFeatureFlagMutationResult;
638
+ try {
639
+ mutation = validateWorkspaceFeatureFlagMutation(
640
+ result,
641
+ mutationValidationExpectation(admin, input, orgDomain),
642
+ );
643
+ } catch {
644
+ throw new WorkspaceFeatureFlagFailure("persistence");
645
+ }
646
+ let verifiedApp: FleetFlagApp;
647
+ try {
648
+ verifiedApp = await listLocalAnalyticsFeatureFlags(admin);
649
+ } catch {
650
+ throw new WorkspaceFeatureFlagFailure("verification");
651
+ }
652
+ if (verifiedApp.state === "forbidden")
653
+ throw new WorkspaceFeatureFlagFailure("authorization");
654
+ const verifiedFlag = verifiedApp.flags.find(
655
+ (flag) => flag.key === input.key,
656
+ );
657
+ if (
658
+ verifiedApp.state !== "ready" ||
659
+ !verifiedFlag ||
660
+ typeof verifiedFlag.enabledForCurrentUser !== "boolean" ||
661
+ (input.operation === "enable-for-current-user" &&
662
+ !verifiedFlag.enabledForCurrentUser) ||
663
+ (input.operation === "off" && verifiedFlag.enabledForCurrentUser)
664
+ ) {
665
+ throw new WorkspaceFeatureFlagFailure("verification");
666
+ }
667
+ try {
668
+ validateWorkspaceFeatureFlagMutation(
669
+ {
670
+ contractVersion: 2,
671
+ status: "ready",
672
+ key: input.key,
673
+ rules: verifiedFlag.rules,
674
+ scope: mutation.scope,
675
+ },
676
+ {
677
+ key: input.key,
678
+ orgDomain,
679
+ allowExplicitNoOrgTarget: true,
680
+ rules: mutation.rules,
681
+ },
682
+ );
683
+ } catch {
684
+ throw new WorkspaceFeatureFlagFailure("verification");
685
+ }
686
+ return {
687
+ contractVersion: 3,
688
+ status: "verified",
689
+ key: mutation.key,
690
+ rules: verifiedFlag.rules as Record<string, unknown>,
691
+ scope: mutation.scope,
692
+ enabledForCurrentUser: verifiedFlag.enabledForCurrentUser,
693
+ };
694
+ }
504
695
  const app = await resolveTargetApp(admin, input.appId);
505
696
  const targetInput = workspaceFeatureFlagTargetInput(input);
506
697
  let orgDomain: string | undefined;
@@ -533,30 +724,10 @@ export async function setWorkspaceFeatureFlag(
533
724
  throw new WorkspaceFeatureFlagFailure("target-action");
534
725
  let mutation: TargetFeatureFlagMutationResult;
535
726
  try {
536
- mutation = validateWorkspaceFeatureFlagMutation(result.body, {
537
- key: input.key,
538
- orgDomain,
539
- allowExplicitNoOrgTarget: true,
540
- ...(input.operation === "replace-rules" && input.rules
541
- ? {
542
- rules: {
543
- mode: input.rules.mode,
544
- emails: input.rules.emails ?? [],
545
- orgIds: input.rules.orgIds ?? [],
546
- percentage: input.rules.percentage ?? 0,
547
- },
548
- }
549
- : input.operation === "off"
550
- ? {
551
- rules: {
552
- mode: "off",
553
- emails: [],
554
- orgIds: [],
555
- percentage: 0,
556
- },
557
- }
558
- : { enabledForEmail: admin.userEmail }),
559
- });
727
+ mutation = validateWorkspaceFeatureFlagMutation(
728
+ result.body,
729
+ mutationValidationExpectation(admin, input, orgDomain),
730
+ );
560
731
  } catch {
561
732
  throw new WorkspaceFeatureFlagFailure("persistence");
562
733
  }
@@ -632,6 +803,7 @@ export async function getWorkspaceFlagTarget(
632
803
  admin: AnalyticsAdminContext,
633
804
  appId: string,
634
805
  ): Promise<FleetFlagApp> {
806
+ if (appId === ANALYTICS_APP_ID) return listLocalAnalyticsFeatureFlags(admin);
635
807
  const apps = await fetchOrgApps({
636
808
  selfId: "analytics",
637
809
  includeDirectoryApp: true,
@@ -2,4 +2,5 @@
2
2
  type: added
3
3
  date: 2026-08-26
4
4
  ---
5
+
5
6
  Templates are now their own surface and can be used across brand kits.
@@ -1150,6 +1150,37 @@ export function CreateEventPopover({
1150
1150
  </SelectContent>
1151
1151
  </Select>
1152
1152
  </div>
1153
+ {eventType === "default" && (
1154
+ <div className="flex items-center justify-between gap-3 py-1">
1155
+ <Label
1156
+ htmlFor="event-availability"
1157
+ className="text-sm text-muted-foreground"
1158
+ >
1159
+ {t("eventForm.showAs")}
1160
+ </Label>
1161
+ <Select
1162
+ value={availability}
1163
+ onValueChange={(value) =>
1164
+ setAvailability(value as Availability)
1165
+ }
1166
+ >
1167
+ <SelectTrigger
1168
+ id="event-availability"
1169
+ className="h-8 w-28 text-sm"
1170
+ >
1171
+ <SelectValue />
1172
+ </SelectTrigger>
1173
+ <SelectContent>
1174
+ <SelectItem value="opaque">
1175
+ {t("eventForm.busy")}
1176
+ </SelectItem>
1177
+ <SelectItem value="transparent">
1178
+ {t("eventForm.free")}
1179
+ </SelectItem>
1180
+ </SelectContent>
1181
+ </Select>
1182
+ </div>
1183
+ )}
1153
1184
  {videoProvider === "zoom" && !zoomStatus.data?.connected && (
1154
1185
  <div className="ml-7 rounded-md border border-border/60 bg-muted/20 p-2">
1155
1186
  <div className="flex items-center justify-between gap-2">
@@ -1483,75 +1514,37 @@ export function CreateEventPopover({
1483
1514
  </div>
1484
1515
  )}
1485
1516
 
1486
- <div className="grid grid-cols-2 gap-3">
1487
- <div className="space-y-1.5">
1488
- <Label htmlFor="event-availability" className="text-xs">
1489
- {t("eventForm.showAs")}
1490
- </Label>
1491
- <Select
1492
- value={
1493
- eventType === "workingLocation"
1494
- ? "transparent"
1495
- : eventType === "default"
1496
- ? availability
1497
- : "opaque"
1498
- }
1499
- onValueChange={(value) =>
1500
- setAvailability(value as Availability)
1501
- }
1502
- disabled={eventType !== "default"}
1503
- >
1504
- <SelectTrigger
1505
- id="event-availability"
1506
- className="h-8 text-sm"
1507
- >
1508
- <SelectValue />
1509
- </SelectTrigger>
1510
- <SelectContent>
1511
- <SelectItem value="opaque">
1512
- {t("eventForm.busy")}
1513
- </SelectItem>
1514
- <SelectItem value="transparent">
1515
- {t("eventForm.free")}
1516
- </SelectItem>
1517
- </SelectContent>
1518
- </Select>
1519
- </div>
1520
-
1521
- <div className="space-y-1.5">
1522
- <Label htmlFor="event-visibility" className="text-xs">
1523
- {t("eventForm.visibility")}
1524
- </Label>
1525
- <Select
1526
- value={
1527
- eventType === "workingLocation"
1528
- ? "public"
1529
- : visibility
1530
- }
1531
- onValueChange={(value) =>
1532
- setVisibility(value as Visibility)
1533
- }
1534
- disabled={eventType === "workingLocation"}
1517
+ <div className="space-y-1.5">
1518
+ <Label htmlFor="event-visibility" className="text-xs">
1519
+ {t("eventForm.visibility")}
1520
+ </Label>
1521
+ <Select
1522
+ value={
1523
+ eventType === "workingLocation" ? "public" : visibility
1524
+ }
1525
+ onValueChange={(value) =>
1526
+ setVisibility(value as Visibility)
1527
+ }
1528
+ disabled={eventType === "workingLocation"}
1529
+ >
1530
+ <SelectTrigger
1531
+ id="event-visibility"
1532
+ className="h-8 text-sm"
1535
1533
  >
1536
- <SelectTrigger
1537
- id="event-visibility"
1538
- className="h-8 text-sm"
1539
- >
1540
- <SelectValue />
1541
- </SelectTrigger>
1542
- <SelectContent>
1543
- <SelectItem value="default">
1544
- {t("eventForm.default")}
1545
- </SelectItem>
1546
- <SelectItem value="public">
1547
- {t("eventForm.public")}
1548
- </SelectItem>
1549
- <SelectItem value="private">
1550
- {t("eventForm.private")}
1551
- </SelectItem>
1552
- </SelectContent>
1553
- </Select>
1554
- </div>
1534
+ <SelectValue />
1535
+ </SelectTrigger>
1536
+ <SelectContent>
1537
+ <SelectItem value="default">
1538
+ {t("eventForm.default")}
1539
+ </SelectItem>
1540
+ <SelectItem value="public">
1541
+ {t("eventForm.public")}
1542
+ </SelectItem>
1543
+ <SelectItem value="private">
1544
+ {t("eventForm.private")}
1545
+ </SelectItem>
1546
+ </SelectContent>
1547
+ </Select>
1555
1548
  </div>
1556
1549
 
1557
1550
  <div className="space-y-1.5">
@@ -27,6 +27,7 @@ import {
27
27
  getDateKeyInTimezone,
28
28
  getEventDateKey,
29
29
  getEventSegmentForCalendarDay,
30
+ isAllDayCalendarEvent,
30
31
  } from "@/lib/calendar-timezone";
31
32
  import { getEventDisplayColor, allOtherDeclined } from "@/lib/event-colors";
32
33
  import {
@@ -556,7 +557,9 @@ export const DayView = memo(function DayView({
556
557
  const allDayEvents = useMemo(
557
558
  () =>
558
559
  events.filter(
559
- (event) => event.allDay || fullDayOutOfOfficeCoversDate(event, date),
560
+ (event) =>
561
+ isAllDayCalendarEvent(event) ||
562
+ fullDayOutOfOfficeCoversDate(event, date),
560
563
  ),
561
564
  [date, events],
562
565
  );
@@ -568,14 +571,17 @@ export const DayView = memo(function DayView({
568
571
  () =>
569
572
  events.filter(
570
573
  (event) =>
571
- !event.allDay &&
574
+ !isAllDayCalendarEvent(event) &&
572
575
  isOutOfOfficeEvent(event) &&
573
576
  !isFullDayOutOfOfficeEvent(event),
574
577
  ),
575
578
  [events],
576
579
  );
577
580
  const timedEvents = useMemo(
578
- () => events.filter((event) => !event.allDay && !isOutOfOfficeEvent(event)),
581
+ () =>
582
+ events.filter(
583
+ (event) => !isAllDayCalendarEvent(event) && !isOutOfOfficeEvent(event),
584
+ ),
579
585
  [events],
580
586
  );
581
587
  const layout = useMemo(
@@ -1524,7 +1524,7 @@ export function EventDetailPopover({
1524
1524
  side={isMobile ? "bottom" : (popoverSide ?? "right")}
1525
1525
  sideOffset={isMobile ? 6 : 8}
1526
1526
  collisionPadding={12}
1527
- className="flex max-h-[var(--radix-popover-content-available-height)] w-[min(18rem,calc(100vw-2rem))] flex-col overflow-hidden p-0"
1527
+ className="flex max-h-[var(--radix-popover-content-available-height)] w-[min(420px,calc(100vw-2rem))] flex-col overflow-hidden p-0"
1528
1528
  onClick={(e) => e.stopPropagation()}
1529
1529
  onOpenAutoFocus={(e) => {
1530
1530
  e.preventDefault();
@@ -1553,8 +1553,8 @@ export function EventDetailPopover({
1553
1553
  >
1554
1554
  <TooltipProvider>
1555
1555
  {/* Header */}
1556
- <div className="flex items-center justify-between border-b border-border/60 px-4 py-3">
1557
- <div className="text-base font-medium text-foreground">
1556
+ <div className="flex items-center justify-between border-b border-border px-4 py-2.5">
1557
+ <div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
1558
1558
  <span>
1559
1559
  {isWorkingLocation
1560
1560
  ? t("eventForm.workingLocation")
@@ -42,6 +42,7 @@ import {
42
42
  getDateKeyInTimezone,
43
43
  getEventDateKey,
44
44
  getEventSegmentForCalendarDay,
45
+ isAllDayCalendarEvent,
45
46
  } from "@/lib/calendar-timezone";
46
47
  import { getEventDisplayColor, allOtherDeclined } from "@/lib/event-colors";
47
48
  import {
@@ -633,7 +634,8 @@ export const WeekView = memo(function WeekView({
633
634
  const allDayEvents = useMemo(
634
635
  () =>
635
636
  events.filter(
636
- (event) => event.allDay || isFullDayOutOfOfficeEvent(event),
637
+ (event) =>
638
+ isAllDayCalendarEvent(event) || isFullDayOutOfOfficeEvent(event),
637
639
  ),
638
640
  [events],
639
641
  );
@@ -642,7 +644,7 @@ export const WeekView = memo(function WeekView({
642
644
  () =>
643
645
  events.filter(
644
646
  (event) =>
645
- !event.allDay &&
647
+ !isAllDayCalendarEvent(event) &&
646
648
  isOutOfOfficeEvent(event) &&
647
649
  !isFullDayOutOfOfficeEvent(event),
648
650
  ),
@@ -650,7 +652,10 @@ export const WeekView = memo(function WeekView({
650
652
  );
651
653
 
652
654
  const timedEvents = useMemo(
653
- () => events.filter((event) => !event.allDay && !isOutOfOfficeEvent(event)),
655
+ () =>
656
+ events.filter(
657
+ (event) => !isAllDayCalendarEvent(event) && !isOutOfOfficeEvent(event),
658
+ ),
654
659
  [events],
655
660
  );
656
661
 
@@ -47,6 +47,15 @@ export function normalizeTimezone(timezone?: string): string {
47
47
  return isCalendarTimezone(timezone) ? timezone : getBrowserTimezone();
48
48
  }
49
49
 
50
+ export function isAllDayCalendarEvent(
51
+ event: Pick<CalendarEvent, "allDay" | "start" | "end">,
52
+ ): boolean {
53
+ return (
54
+ event.allDay ||
55
+ (DATE_ONLY_PATTERN.test(event.start) && DATE_ONLY_PATTERN.test(event.end))
56
+ );
57
+ }
58
+
50
59
  /** Date carriers are kept at local noon so browser DST never changes their date. */
51
60
  export function dateKeyToDate(date: string): Date {
52
61
  const [year, month, day] = date.split("-").map(Number);
@@ -205,7 +214,7 @@ function eventDateRange(
205
214
  event: Pick<CalendarEvent, "start" | "end" | "allDay">,
206
215
  timezone: string,
207
216
  ) {
208
- if (event.allDay) {
217
+ if (isAllDayCalendarEvent(event)) {
209
218
  const startDate = dateOnlyPart(event.start);
210
219
  const endDate =
211
220
  dateOnlyPart(event.end) ??
@@ -234,7 +243,7 @@ export function moveEventToCalendarDate(
234
243
  const sourceStartDate = getEventDateKey(event, timezone);
235
244
  if (!sourceStartDate) return null;
236
245
 
237
- if (event.allDay) {
246
+ if (isAllDayCalendarEvent(event)) {
238
247
  const sourceEndDate =
239
248
  dateOnlyPart(event.end) ?? addCalendarDays(sourceStartDate, 1);
240
249
  const spanDays = Math.max(
@@ -285,7 +294,7 @@ export function eventOverlapsCalendarDay(
285
294
  ): boolean {
286
295
  const normalizedTimezone = normalizeTimezone(timezone);
287
296
  const dayBounds = getCalendarDayBounds(day, normalizedTimezone);
288
- if (event.allDay) {
297
+ if (isAllDayCalendarEvent(event)) {
289
298
  const range = eventDateRange(event, normalizedTimezone);
290
299
  const dayDate = dayBounds.date;
291
300
  return Boolean(
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-28
4
+ ---
5
+
6
+ All-day events stay in the compact top bar in day and week views.