@agent-native/core 0.153.0 → 0.153.1

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 (36) hide show
  1. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -2
  2. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +3 -2
  3. package/corpus/templates/calendar/app/lib/event-layout.ts +16 -35
  4. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +27 -44
  5. package/corpus/templates/clips/app/components/player/share-dialog.tsx +72 -33
  6. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +18 -6
  7. package/corpus/templates/clips/desktop/src/app.tsx +1 -1
  8. package/corpus/templates/clips/desktop/src/styles.css +10 -2
  9. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +2 -2
  10. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +1 -1
  11. package/corpus/templates/factory/app/routes/factory.tsx +0 -3
  12. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +1 -1
  13. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +28 -25
  14. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +50 -0
  15. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +12 -60
  16. package/dist/agent/production-agent.d.ts +8 -0
  17. package/dist/agent/production-agent.js +25 -2
  18. package/dist/agent/run-manager.js +13 -3
  19. package/dist/client/AgentPanel.js +6 -4
  20. package/dist/client/org/WorkspaceNotice.js +7 -4
  21. package/dist/client/sharing/ShareButton.d.ts +2 -2
  22. package/dist/client/sharing/ShareButton.js +18 -13
  23. package/dist/client/sharing/ShareDialog.d.ts +1 -1
  24. package/dist/client/sharing/ShareDialog.js +13 -10
  25. package/dist/collab/routes.d.ts +1 -1
  26. package/dist/collab/struct-routes.d.ts +1 -1
  27. package/dist/notifications/routes.d.ts +1 -1
  28. package/dist/observability/routes.d.ts +3 -3
  29. package/dist/progress/routes.d.ts +1 -1
  30. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  31. package/dist/server/auth.js +18 -4
  32. package/dist/server/better-auth-instance.js +8 -1
  33. package/dist/server/magic-link-attribution.d.ts +21 -0
  34. package/dist/server/magic-link-attribution.js +115 -0
  35. package/dist/server/realtime-token.d.ts +1 -1
  36. package/package.json +1 -1
@@ -232,6 +232,7 @@ const DayEventCard = memo(function DayEventCard({
232
232
  const li = layout.get(event.id) ?? {
233
233
  left: 0,
234
234
  width: 100,
235
+ indent: 0,
235
236
  col: 0,
236
237
  totalCols: 1,
237
238
  stackOrder: 0,
@@ -304,8 +305,8 @@ const DayEventCard = memo(function DayEventCard({
304
305
  }
305
306
  style={{
306
307
  ...posStyle,
307
- left: `calc(${li.left}% + ${li.col > 0 ? 2 : 0}px)`,
308
- width: `calc(${li.width}% - ${li.col > 0 ? 4 : 2}px)`,
308
+ left: `calc(${li.left}% + ${li.indent}px)`,
309
+ width: `calc(${li.width}% - ${li.indent * 2 + 2}px)`,
309
310
  zIndex:
310
311
  isBeingDragged && isDragging
311
312
  ? 100
@@ -274,6 +274,7 @@ const WeekEventCard = memo(function WeekEventCard({
274
274
  const li = layout.get(event.id) ?? {
275
275
  left: 0,
276
276
  width: 100,
277
+ indent: 0,
277
278
  col: 0,
278
279
  totalCols: 1,
279
280
  stackOrder: 0,
@@ -372,8 +373,8 @@ const WeekEventCard = memo(function WeekEventCard({
372
373
  }
373
374
  style={{
374
375
  ...style,
375
- left: `calc(${li.left}% + ${li.col > 0 ? 2 : 0}px)`,
376
- width: `calc(${li.width}% - ${li.col > 0 ? 4 : 2}px)`,
376
+ left: `calc(${li.left}% + ${li.indent}px)`,
377
+ width: `calc(${li.width}% - ${li.indent * 2 + 2}px)`,
377
378
  zIndex:
378
379
  isBeingDragged && isDragging
379
380
  ? 100
@@ -4,6 +4,7 @@ import { addDays, parseISO, startOfDay } from "date-fns";
4
4
  export interface TimedEventLayout {
5
5
  left: number;
6
6
  width: number;
7
+ indent: number;
7
8
  col: number;
8
9
  totalCols: number;
9
10
  stackOrder: number;
@@ -20,18 +21,16 @@ interface EventEntry {
20
21
  inputOrder: number;
21
22
  }
22
23
 
23
- const TEXT_REGION_MINUTES = 45;
24
24
  const MIN_VISIBLE_EVENT_MINUTES = 15;
25
+ const OVERLAP_INDENT_PX = 16;
25
26
 
26
27
  function overlaps(a: EventBounds, b: EventBounds): boolean {
27
28
  return a.start < b.end && b.start < a.end;
28
29
  }
29
30
 
30
31
  /**
31
- * Pack timed events into columns while reserving horizontal space only when
32
- * the event labels can collide. An event may span unused columns when another
33
- * event is present there at a different time, which keeps long events readable
34
- * without wasting the column width needed for genuinely simultaneous labels.
32
+ * Pack timed events into shallow overlap layers. Later events stay wide enough
33
+ * to read while their inset exposes the boundary of the event underneath.
35
34
  */
36
35
  export function computeTimedEventLayout(
37
36
  dayEvents: readonly CalendarEvent[],
@@ -66,52 +65,34 @@ export function computeTimedEventLayout(
66
65
  return a.inputOrder - b.inputOrder;
67
66
  });
68
67
 
69
- const textOverlaps = (a: EventBounds, b: EventBounds): boolean => {
70
- const aTextEnd = Math.min(a.start + TEXT_REGION_MINUTES * 60 * 1000, a.end);
71
- const bTextEnd = Math.min(b.start + TEXT_REGION_MINUTES * 60 * 1000, b.end);
72
- return a.start < bTextEnd && b.start < aTextEnd;
73
- };
74
-
75
- // Place labels side by side only when their visible text regions overlap.
76
- const columns: EventEntry[][] = [];
68
+ // Put overlapping events into the first layer that is free at their start.
69
+ // Reusing finished layers keeps chained overlaps from creating empty gaps.
70
+ const overlapLayers: EventEntry[][] = [];
77
71
  const eventColumns = new Map<CalendarEvent, number>();
78
72
 
79
73
  for (const entry of sorted) {
80
- let column = columns.findIndex((columnEntries) =>
81
- columnEntries.every(
82
- (placed) => !textOverlaps(placed.bounds, entry.bounds),
83
- ),
74
+ let column = overlapLayers.findIndex((layerEntries) =>
75
+ layerEntries.every((placed) => !overlaps(placed.bounds, entry.bounds)),
84
76
  );
85
77
 
86
78
  if (column === -1) {
87
- column = columns.length;
88
- columns.push([]);
79
+ column = overlapLayers.length;
80
+ overlapLayers.push([]);
89
81
  }
90
82
 
91
- columns[column].push(entry);
83
+ overlapLayers[column].push(entry);
92
84
  eventColumns.set(entry.event, column);
93
85
  }
94
86
 
95
- const totalCols = columns.length;
87
+ const totalCols = overlapLayers.length;
96
88
 
97
89
  for (const [stackOrder, entry] of sorted.entries()) {
98
90
  const col = eventColumns.get(entry.event)!;
99
- let span = 1;
100
-
101
- // A column to the right is available when it has no event that overlaps
102
- // this card in time. Label-only collisions do not need to block expansion.
103
- for (let candidate = col + 1; candidate < totalCols; candidate++) {
104
- if (
105
- columns[candidate].some((other) => overlaps(other.bounds, entry.bounds))
106
- ) {
107
- break;
108
- }
109
- span++;
110
- }
111
91
 
112
92
  result.set(entry.event.id, {
113
- left: (col / totalCols) * 100,
114
- width: (span / totalCols) * 100,
93
+ left: 0,
94
+ width: 100,
95
+ indent: col * OVERLAP_INDENT_PX,
115
96
  col,
116
97
  totalCols,
117
98
  stackOrder,
@@ -4,7 +4,7 @@ import {
4
4
  useActionQuery,
5
5
  } from "@agent-native/core/client/hooks";
6
6
  import { useT } from "@agent-native/core/client/i18n";
7
- import { IconCheck, IconLink, IconMail } from "@tabler/icons-react";
7
+ import { IconLink, IconMail } from "@tabler/icons-react";
8
8
  import { useEffect, useMemo, useState, type ReactNode } from "react";
9
9
  import { toast } from "sonner";
10
10
 
@@ -192,57 +192,40 @@ function LinkTab({
192
192
  };
193
193
 
194
194
  return (
195
- <div className="space-y-4">
195
+ <div className="space-y-3">
196
196
  <GeneralAccessSelect
197
197
  visibility={visibility}
198
198
  canManage={canManage}
199
199
  isPending={isPending}
200
200
  onChange={(next) => setResourceVisibility(next)}
201
+ showDescription={false}
201
202
  />
202
203
 
203
- <div>
204
- <div className="mb-2 text-xs font-semibold">
205
- {t("shareMeeting.sharedContent")}
206
- </div>
207
- <div className="rounded-md border border-border">
208
- <div className="flex items-center gap-3 border-b border-border px-3 py-2.5">
209
- <span
210
- aria-hidden
211
- className="inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground"
212
- >
213
- <IconCheck size={12} strokeWidth={2.5} />
214
- </span>
215
- <span className="text-sm">{t("shareMeeting.summaryIncluded")}</span>
216
- </div>
217
- <div className="flex items-start justify-between gap-4 px-3 py-2.5">
218
- <div className="min-w-0">
219
- <label
220
- htmlFor={`meeting-share-transcript-${meetingId}`}
221
- className="text-sm font-medium"
222
- >
223
- {t("shareMeeting.includeTranscript")}
224
- </label>
225
- <p
226
- id={`meeting-share-transcript-description-${meetingId}`}
227
- className="mt-0.5 text-xs text-muted-foreground"
228
- >
229
- {transcriptReady
230
- ? t("shareMeeting.includeTranscriptDescription")
231
- : t("shareMeeting.transcriptUnavailable")}
232
- </p>
233
- </div>
234
- <Switch
235
- id={`meeting-share-transcript-${meetingId}`}
236
- checked={includeTranscript}
237
- onCheckedChange={handleTranscriptSharingChange}
238
- disabled={
239
- !canManage || !transcriptReady || updateMeeting.isPending
240
- }
241
- aria-describedby={`meeting-share-transcript-description-${meetingId}`}
242
- className="mt-0.5 shrink-0"
243
- />
244
- </div>
204
+ <div className="flex items-center justify-between gap-4 rounded-md border border-border px-3 py-2.5">
205
+ <div className="min-w-0">
206
+ <label
207
+ htmlFor={`meeting-share-transcript-${meetingId}`}
208
+ className="text-sm font-medium"
209
+ >
210
+ {t("shareMeeting.includeTranscript")}
211
+ </label>
212
+ <p
213
+ id={`meeting-share-transcript-description-${meetingId}`}
214
+ className="sr-only"
215
+ >
216
+ {transcriptReady
217
+ ? t("shareMeeting.includeTranscriptDescription")
218
+ : t("shareMeeting.transcriptUnavailable")}
219
+ </p>
245
220
  </div>
221
+ <Switch
222
+ id={`meeting-share-transcript-${meetingId}`}
223
+ checked={includeTranscript}
224
+ onCheckedChange={handleTranscriptSharingChange}
225
+ disabled={!canManage || !transcriptReady || updateMeeting.isPending}
226
+ aria-describedby={`meeting-share-transcript-description-${meetingId}`}
227
+ className="shrink-0"
228
+ />
246
229
  </div>
247
230
 
248
231
  <CopyField label={t("clipsFinalRaw.shareLink")} value={shareUrl} />
@@ -7,6 +7,7 @@ import {
7
7
  } from "@agent-native/core/client/hooks";
8
8
  import { useT } from "@agent-native/core/client/i18n";
9
9
  import {
10
+ IconChevronDown,
10
11
  IconCode,
11
12
  IconExternalLink,
12
13
  IconLink,
@@ -35,6 +36,11 @@ import {
35
36
  type Visibility,
36
37
  } from "@/components/sharing/share-ui";
37
38
  import { Button } from "@/components/ui/button";
39
+ import {
40
+ Collapsible,
41
+ CollapsibleContent,
42
+ CollapsibleTrigger,
43
+ } from "@/components/ui/collapsible";
38
44
  import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
39
45
  import { Input } from "@/components/ui/input";
40
46
  import { Label } from "@/components/ui/label";
@@ -434,6 +440,11 @@ function LinkTab({
434
440
  const agentPrompt = agentLink
435
441
  ? t("shareDialog.agentPrompt", { agentContextUrl: agentLink })
436
442
  : "";
443
+ const [agentDetailsOpen, setAgentDetailsOpen] = useState(false);
444
+
445
+ useEffect(() => {
446
+ if (isPublic) setAgentDetailsOpen(false);
447
+ }, [isPublic]);
437
448
 
438
449
  return (
439
450
  <div className="space-y-4">
@@ -444,6 +455,7 @@ function LinkTab({
444
455
  isPending={visibilityPending}
445
456
  onChange={(next) => setResourceVisibility(next)}
446
457
  publicDescription={t("shareDialog.publicDescription")}
458
+ showDescription={false}
447
459
  />
448
460
  ) : (
449
461
  <div className="space-y-2" aria-hidden>
@@ -463,42 +475,69 @@ function LinkTab({
463
475
  />
464
476
 
465
477
  {!isPublic ? (
466
- <div className="space-y-2">
467
- <CopyField
468
- label={t("shareDialog.shareWithAgents")}
469
- value={agentLink}
470
- disabled={agentShareDisabled}
471
- />
472
- {sharesLoaded ? (
473
- <>
474
- <p className="text-xs text-muted-foreground">
475
- {t("shareDialog.agentTokenDescription")}
476
- </p>
477
- {agentLinkError ? (
478
- <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
479
- <p className="text-xs text-muted-foreground">
480
- {t("shareDialog.agentLinkUnavailable")}
481
- </p>
482
- <Button
483
- type="button"
484
- variant="ghost"
485
- size="sm"
486
- className="h-7"
487
- onClick={() => void loadAgentContextUrl()}
488
- disabled={createAgentLink.isPending}
489
- >
490
- {t("shareDialog.retryAgentLink")}
491
- </Button>
492
- </div>
493
- ) : null}
478
+ <Collapsible
479
+ open={agentDetailsOpen}
480
+ onOpenChange={setAgentDetailsOpen}
481
+ className="overflow-hidden rounded-md border border-border"
482
+ >
483
+ <CollapsibleTrigger asChild>
484
+ <button
485
+ type="button"
486
+ className="flex min-h-10 w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm font-medium transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
487
+ >
488
+ <span className="flex min-w-0 items-center gap-2 truncate">
489
+ <IconLink
490
+ aria-hidden="true"
491
+ className="size-3.5 shrink-0 text-muted-foreground"
492
+ strokeWidth={1.8}
493
+ />
494
+ {t("shareDialog.shareWithAgents")}
495
+ </span>
496
+ <IconChevronDown
497
+ aria-hidden="true"
498
+ className={`size-4 shrink-0 text-muted-foreground transition-transform ${agentDetailsOpen ? "rotate-180" : ""}`}
499
+ />
500
+ </button>
501
+ </CollapsibleTrigger>
502
+ <CollapsibleContent className="clips-collapsible-content border-t border-border px-3 py-3">
503
+ <div className="space-y-2">
494
504
  <CopyField
495
- label={t("shareDialog.copyAgentPrompt")}
496
- value={agentPrompt}
505
+ label={t("shareDialog.shareWithAgents")}
506
+ value={agentLink}
497
507
  disabled={agentShareDisabled}
498
508
  />
499
- </>
500
- ) : null}
501
- </div>
509
+ {sharesLoaded ? (
510
+ <>
511
+ <p className="text-xs text-muted-foreground">
512
+ {t("shareDialog.agentTokenDescription")}
513
+ </p>
514
+ {agentLinkError ? (
515
+ <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
516
+ <p className="text-xs text-muted-foreground">
517
+ {t("shareDialog.agentLinkUnavailable")}
518
+ </p>
519
+ <Button
520
+ type="button"
521
+ variant="ghost"
522
+ size="sm"
523
+ className="h-7"
524
+ onClick={() => void loadAgentContextUrl()}
525
+ disabled={createAgentLink.isPending}
526
+ >
527
+ {t("shareDialog.retryAgentLink")}
528
+ </Button>
529
+ </div>
530
+ ) : null}
531
+ <CopyField
532
+ label={t("shareDialog.copyAgentPrompt")}
533
+ value={agentPrompt}
534
+ disabled={agentShareDisabled}
535
+ />
536
+ </>
537
+ ) : null}
538
+ </div>
539
+ </CollapsibleContent>
540
+ </Collapsible>
502
541
  ) : null}
503
542
 
504
543
  {sharesLoaded && !isPublic && canManage ? (
@@ -178,6 +178,7 @@ export function GeneralAccessSelect({
178
178
  isPending,
179
179
  onChange,
180
180
  publicDescription,
181
+ showDescription = true,
181
182
  }: {
182
183
  visibility: Visibility;
183
184
  canManage: boolean;
@@ -185,6 +186,7 @@ export function GeneralAccessSelect({
185
186
  onChange: (next: Visibility) => void;
186
187
  /** Override for the "public" visibility description (e.g. Clips comment hint). */
187
188
  publicDescription?: string;
189
+ showDescription?: boolean;
188
190
  }) {
189
191
  const t = useT();
190
192
  const meta = VIS_META[visibility];
@@ -222,7 +224,12 @@ export function GeneralAccessSelect({
222
224
  ))}
223
225
  </SelectContent>
224
226
  </Select>
225
- <div className="mt-0.5 text-xs text-muted-foreground">
227
+ <div
228
+ className={cn(
229
+ "mt-0.5 text-xs text-muted-foreground",
230
+ !showDescription && "sr-only",
231
+ )}
232
+ >
226
233
  {description}
227
234
  </div>
228
235
  </div>
@@ -244,14 +251,19 @@ export function MakePublicCard({
244
251
  }) {
245
252
  const t = useT();
246
253
  return (
247
- <div className="rounded-md border border-border bg-muted/40 px-3 py-2.5">
248
- <p className="text-xs text-muted-foreground">
249
- {t("shareUi.restrictedLinkDescription")}
250
- </p>
254
+ <div className="flex items-center justify-between gap-2 rounded-md bg-muted/40 px-3 py-2">
255
+ <p className="sr-only">{t("shareUi.restrictedLinkDescription")}</p>
256
+ <IconLock
257
+ aria-hidden
258
+ size={14}
259
+ strokeWidth={1.8}
260
+ className="text-muted-foreground"
261
+ />
251
262
  <Button
252
263
  type="button"
264
+ variant="outline"
253
265
  size="sm"
254
- className="mt-2 h-7"
266
+ className="h-7"
255
267
  onClick={onMakePublic}
256
268
  disabled={isPending}
257
269
  >
@@ -2629,7 +2629,7 @@ export function App() {
2629
2629
  isRecording ||
2630
2630
  recordingFlowActive,
2631
2631
  width:
2632
- popoverView === "settings" ? 920 : popoverView === "memory" ? 440 : 360,
2632
+ popoverView === "settings" ? 920 : popoverView === "memory" ? 440 : 320,
2633
2633
  });
2634
2634
 
2635
2635
  const loadPendingUploads = useCallback(async () => {
@@ -13,6 +13,7 @@
13
13
  --surface: #f7f7f7;
14
14
  --surface-hover: #f0f0f0;
15
15
  --surface-strong: #ebebeb;
16
+ --settings-nav-active: #ffffff;
16
17
  --fg: #171717;
17
18
  --fg-muted: #737373;
18
19
  --fg-subtle: #a3a3a3;
@@ -48,6 +49,7 @@
48
49
  --surface: #262626;
49
50
  --surface-hover: #2e2e2e;
50
51
  --surface-strong: #3d3d3d;
52
+ --settings-nav-active: var(--surface-hover);
51
53
  --fg: #f5f5f5;
52
54
  --fg-muted: #a3a3a3;
53
55
  --fg-subtle: #737373;
@@ -237,9 +239,9 @@ body[data-clips-route="recording-pill"] #root {
237
239
  }
238
240
 
239
241
  .settings-nav-button.is-active {
240
- background: var(--bg);
242
+ background: var(--settings-nav-active);
241
243
  color: var(--fg);
242
- box-shadow: var(--shadow-sm);
244
+ box-shadow: none;
243
245
  }
244
246
 
245
247
  .settings-nav-button:focus-visible {
@@ -2577,6 +2579,12 @@ body[data-clips-route="recording-pill"] #root {
2577
2579
  overscroll-behavior: contain;
2578
2580
  }
2579
2581
 
2582
+ .setup.settings-page {
2583
+ padding: 0;
2584
+ gap: 0;
2585
+ overflow: hidden;
2586
+ }
2587
+
2580
2588
  /* Meetings / dictation drill-ins: a touch more room under the last row so
2581
2589
  content isn't flush with the panel edge. Must follow `.setup` so it wins
2582
2590
  over the shorthand padding above. */
@@ -70,7 +70,7 @@ const ONBOARDING_HEIGHT_LOGICAL: f64 = 640.0;
70
70
  const BUBBLE_SIZE_SMALL: u32 = 360;
71
71
  const BUBBLE_SIZE_MEDIUM: u32 = 504;
72
72
  const POPOVER_SHADOW_GUTTER_LOGICAL: f64 = 24.0;
73
- const POPOVER_DEFAULT_WIDTH_LOGICAL: f64 = 360.0;
73
+ const POPOVER_DEFAULT_WIDTH_LOGICAL: f64 = 320.0;
74
74
  const POPOVER_DEFAULT_HEIGHT_LOGICAL: f64 = 520.0;
75
75
  const OVERLAY_SHADOW_GUTTER_LOGICAL: f64 = 18.0;
76
76
 
@@ -1342,7 +1342,7 @@ pub async fn resize_popover(app: AppHandle, height: f64, width: Option<f64>) ->
1342
1342
  })
1343
1343
  .unwrap_or(820.0);
1344
1344
  let clamped = height.clamp(200.0, max_logical_height);
1345
- let width = width.unwrap_or(360.0).clamp(320.0, 960.0);
1345
+ let width = width.unwrap_or(320.0).clamp(320.0, 960.0);
1346
1346
  let (window_width, window_height) = popover_window_size_logical(width, clamped);
1347
1347
  let _ = w.set_size(tauri::Size::Logical(tauri::LogicalSize::new(
1348
1348
  window_width,
@@ -7,7 +7,7 @@ use crate::state::{
7
7
  };
8
8
 
9
9
  const POPOVER_SHADOW_GUTTER_LOGICAL: f64 = 24.0;
10
- const POPOVER_DEFAULT_WIDTH_LOGICAL: f64 = 360.0;
10
+ const POPOVER_DEFAULT_WIDTH_LOGICAL: f64 = 320.0;
11
11
  const POPOVER_DEFAULT_HEIGHT_LOGICAL: f64 = 520.0;
12
12
 
13
13
  // ---------------------------------------------------------------------------
@@ -1516,9 +1516,6 @@ function RulesView({ t }: { t: ReturnType<typeof useT> }) {
1516
1516
  <CardTitle className="text-base">
1517
1517
  {t("factoryRoute.rulesTitle")}
1518
1518
  </CardTitle>
1519
- <p className="text-sm text-muted-foreground">
1520
- {t("factoryRoute.rulesDescription")}
1521
- </p>
1522
1519
  </CardHeader>
1523
1520
  <CardContent className="space-y-2">
1524
1521
  <Button
@@ -636,7 +636,7 @@ export default function EditorToolbar({
636
636
  />
637
637
 
638
638
  {/* Consolidated editor menu */}
639
- <div className="flex-shrink-0">
639
+ <div className="ml-auto flex-shrink-0">
640
640
  <DropdownMenu>
641
641
  <Tooltip>
642
642
  <TooltipTrigger asChild>
@@ -6,6 +6,7 @@ import {
6
6
  IconCheck,
7
7
  IconLoader2,
8
8
  IconExternalLink,
9
+ IconChevronDown,
9
10
  } from "@tabler/icons-react";
10
11
  import { useState, type ReactNode } from "react";
11
12
 
@@ -86,7 +87,7 @@ export default function ShareDialog({ deck, children }: ShareDialogProps) {
86
87
  <PopoverTrigger asChild>{children}</PopoverTrigger>
87
88
  <PopoverContent
88
89
  align="end"
89
- className="w-[420px] bg-card border-border p-4"
90
+ className="w-[min(420px,calc(100vw-2rem))] bg-card border-border p-4"
90
91
  >
91
92
  {showCloudUpgrade || isLocal ? (
92
93
  <CloudUpgrade
@@ -104,35 +105,37 @@ export default function ShareDialog({ deck, children }: ShareDialogProps) {
104
105
  <IconShare2 className="w-4 h-4 text-[#609FF8]" />
105
106
  {t("share.title")}
106
107
  </div>
107
- <div className="text-muted-foreground text-xs mt-0.5">
108
- {t("share.description", { title: deck.title })}
109
- </div>
110
108
  </div>
111
109
 
112
110
  <div className="space-y-4">
113
111
  {!shareUrl ? (
114
112
  <>
115
- <div className="bg-muted/50 rounded-lg p-3 border border-border">
116
- <h4 className="text-sm font-medium text-foreground/90 mb-2">
113
+ <details className="group rounded-md border border-border bg-muted/30">
114
+ <summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-3 py-2.5 text-sm font-medium [&::-webkit-details-marker]:hidden">
117
115
  {t("share.whatGetsShared")}
118
- </h4>
119
- <ul className="text-xs text-muted-foreground space-y-1">
120
- <li>
121
- {t("share.slideContent", {
122
- count: deck.slides.length,
123
- })}
124
- </li>
125
- <li>{t("share.presentationView")}</li>
126
- </ul>
127
- <h4 className="text-sm font-medium text-foreground/90 mt-3 mb-2">
128
- {t("share.whatStaysPrivate")}
129
- </h4>
130
- <ul className="text-xs text-muted-foreground space-y-1">
131
- <li>{t("share.speakerNotes")}</li>
132
- <li>{t("share.otherPresentations")}</li>
133
- <li>{t("share.editingAccess")}</li>
134
- </ul>
135
- </div>
116
+ <IconChevronDown className="size-4 shrink-0 text-muted-foreground transition-transform group-open:rotate-180" />
117
+ </summary>
118
+ <div className="space-y-3 border-t border-border px-3 py-3 text-xs text-muted-foreground">
119
+ <ul className="space-y-1">
120
+ <li>
121
+ {t("share.slideContent", {
122
+ count: deck.slides.length,
123
+ })}
124
+ </li>
125
+ <li>{t("share.presentationView")}</li>
126
+ </ul>
127
+ <div>
128
+ <h4 className="mb-1 font-medium text-foreground/90">
129
+ {t("share.whatStaysPrivate")}
130
+ </h4>
131
+ <ul className="space-y-1">
132
+ <li>{t("share.speakerNotes")}</li>
133
+ <li>{t("share.otherPresentations")}</li>
134
+ <li>{t("share.editingAccess")}</li>
135
+ </ul>
136
+ </div>
137
+ </div>
138
+ </details>
136
139
 
137
140
  {error && (
138
141
  <p className="text-xs text-red-400 bg-red-400/10 px-3 py-2 rounded-lg">
@@ -194,7 +197,7 @@ export default function ShareDialog({ deck, children }: ShareDialogProps) {
194
197
  {t("share.openSharedLink")}
195
198
  </a>
196
199
 
197
- <p className="text-[11px] text-muted-foreground/70 text-center">
200
+ <p className="sr-only text-[11px] text-muted-foreground/70 text-center">
198
201
  {t("share.anyoneWithLink")}
199
202
  </p>
200
203
  </>