@agent-native/core 0.148.1 → 0.149.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 (165) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/changelog/2026-08-10-wide-tables-in-analytics-chat-now-scroll-horizontally-within.md +6 -0
  3. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +13 -53
  4. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -70
  5. package/corpus/templates/calendar/app/lib/event-layout.ts +122 -0
  6. package/corpus/templates/calendar/app/routes/_app.availability.tsx +1 -1
  7. package/corpus/templates/calendar/changelog/2026-08-10-opening-availability-settings-from-the-agent-lands-on-the-av.md +6 -0
  8. package/corpus/templates/calendar/vitest.config.ts +6 -1
  9. package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +3 -1
  10. package/corpus/templates/clips/app/components/player/video-player.tsx +44 -4
  11. package/corpus/templates/clips/changelog/2026-08-10-fixed-transcription-failures-for-recordings-without-captured.md +6 -0
  12. package/corpus/templates/clips/changelog/2026-08-10-mobile-clips-can-now-enter-fullscreen-on-iphone-safari.md +6 -0
  13. package/corpus/templates/content/actions/_property-utils.ts +11 -1
  14. package/corpus/templates/content/actions/view-screen.ts +62 -12
  15. package/corpus/templates/content/server/plugins/agent-chat.ts +2 -0
  16. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +22 -0
  17. package/corpus/templates/design/AGENTS.md +3 -0
  18. package/corpus/templates/design/actions/create-design-from-template.ts +19 -0
  19. package/corpus/templates/design/actions/get-design-snapshot.ts +18 -0
  20. package/corpus/templates/design/actions/get-design-template.ts +178 -0
  21. package/corpus/templates/design/actions/list-designs.ts +3 -1
  22. package/corpus/templates/design/actions/view-screen.ts +31 -0
  23. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +225 -214
  24. package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
  25. package/corpus/templates/design/app/i18n-data.ts +78 -0
  26. package/corpus/templates/design/app/lib/design-authors.ts +64 -0
  27. package/corpus/templates/design/app/pages/Index.tsx +114 -4
  28. package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +1 -0
  29. package/corpus/templates/design/changelog/2026-08-07-alt-dragging-an-element-to-copy-it-now-keeps-the-copy-visibl.md +6 -0
  30. package/corpus/templates/design/changelog/2026-08-07-design-cards-now-show-who-created-each-design-with-a-filter-.md +6 -0
  31. package/corpus/templates/design/changelog/2026-08-07-drawing-a-shape-on-the-overview-canvas-now-keeps-the-shape-s.md +6 -0
  32. package/corpus/templates/design/changelog/2026-08-07-hold-option-alt-while-dragging-a-frame-on-the-overview-canva.md +6 -0
  33. package/corpus/templates/design/server/lib/design-template-data.ts +143 -0
  34. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -0
  35. package/corpus/templates/forms/AGENTS.md +2 -2
  36. package/corpus/templates/forms/actions/create-form.ts +1 -1
  37. package/corpus/templates/forms/actions/update-form.ts +5 -5
  38. package/corpus/templates/forms/changelog/2026-08-10-forms-now-keep-agent-applied-field-changes-visible-after-an-.md +6 -0
  39. package/corpus/templates/forms/changelog/2026-08-10-published-form-links-always-use-the-working-public-form-url.md +6 -0
  40. package/corpus/templates/forms/server/plugins/agent-chat.ts +2 -2
  41. package/corpus/templates/slides/AGENTS.md +4 -3
  42. package/corpus/templates/slides/actions/import-google-slides-reference.ts +56 -10
  43. package/corpus/templates/slides/actions/patch-deck.ts +18 -2
  44. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
  45. package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
  46. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +22 -75
  47. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +5 -3
  48. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +5 -1
  49. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +15 -4
  50. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +116 -55
  51. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +27 -4
  52. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
  53. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +143 -18
  54. package/corpus/templates/slides/app/i18n/en-US.ts +6 -0
  55. package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
  56. package/corpus/templates/slides/app/lib/generation-state.ts +7 -1
  57. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
  58. package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -14
  59. package/corpus/templates/slides/app/pages/Index.tsx +124 -135
  60. package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
  61. package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
  63. package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
  64. package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
  65. package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
  66. package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
  67. package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
  68. package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
  69. package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
  70. package/corpus/templates/slides/changelog/2026-08-10-imported-reference-decks-now-show-clear-success-and-stay-sel.md +6 -0
  71. package/corpus/templates/slides/changelog/2026-08-10-pasted-google-slides-links-import-after-reconnecting-google-.md +6 -0
  72. package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -0
  73. package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
  74. package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
  75. package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
  76. package/corpus/templates/slides/server/lib/google-docs-oauth.ts +13 -0
  77. package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
  78. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  79. package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
  80. package/dist/agent/engine/registry.d.ts +8 -2
  81. package/dist/agent/engine/registry.js +63 -29
  82. package/dist/agent/production-agent.js +7 -0
  83. package/dist/agent/run-manager.d.ts +5 -4
  84. package/dist/agent/run-manager.js +6 -5
  85. package/dist/cli/claude-code-participant.d.ts +2 -1
  86. package/dist/cli/claude-code-participant.js +19 -0
  87. package/dist/cli/code-agent-executor.js +223 -0
  88. package/dist/cli/create-workspace.js +3 -3
  89. package/dist/cli/create.d.ts +3 -2
  90. package/dist/cli/create.js +20 -24
  91. package/dist/client/AgentPanel.js +8 -4
  92. package/dist/client/AssistantChat.js +34 -1
  93. package/dist/client/agent-chat-adapter.d.ts +1 -0
  94. package/dist/client/agent-chat-adapter.js +10 -2
  95. package/dist/client/chat/markdown-renderer.d.ts +1 -0
  96. package/dist/client/chat/markdown-renderer.js +4 -0
  97. package/dist/client/chat/message-components.d.ts +1 -0
  98. package/dist/client/chat/message-components.js +9 -1
  99. package/dist/client/chat/tool-call-display.d.ts +8 -0
  100. package/dist/client/chat/tool-call-display.js +30 -21
  101. package/dist/client/chat-first/app-pane.js +1 -1
  102. package/dist/client/chat-first/apps-rail.d.ts +1 -1
  103. package/dist/client/chat-first/apps-rail.js +11 -11
  104. package/dist/client/chat-first/chat-history.d.ts +1 -1
  105. package/dist/client/chat-first/chat-history.js +2 -0
  106. package/dist/client/chat-first/primary-nav.js +1 -1
  107. package/dist/client/progress/RunsTray.js +14 -1
  108. package/dist/client/resources/McpConnectionSuggestion.js +2 -2
  109. package/dist/client/settings/SettingsTabsPage.d.ts +3 -1
  110. package/dist/client/settings/SettingsTabsPage.js +2 -2
  111. package/dist/client/sse-event-processor.d.ts +8 -0
  112. package/dist/client/sse-event-processor.js +13 -6
  113. package/dist/collab/awareness.d.ts +2 -2
  114. package/dist/collab/routes.d.ts +1 -1
  115. package/dist/deploy/build.d.ts +5 -7
  116. package/dist/deploy/build.js +19 -12
  117. package/dist/localization/default-messages.js +1 -1
  118. package/dist/notifications/routes.d.ts +3 -3
  119. package/dist/observability/routes.d.ts +6 -6
  120. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  121. package/dist/scripts/call-agent.js +3 -3
  122. package/dist/scripts/describe-workspace-apps.js +1 -1
  123. package/dist/server/agent-capabilities.js +0 -0
  124. package/dist/server/agent-chat/context-tools.js +1 -1
  125. package/dist/server/agent-chat/prompt-resources.js +1 -1
  126. package/dist/server/agent-chat-plugin.js +1 -1
  127. package/dist/server/auth.js +190 -42
  128. package/dist/server/better-auth-migrations.js +53 -0
  129. package/dist/server/http-response-telemetry.js +7 -1
  130. package/dist/server/onboarding-html.js +119 -33
  131. package/dist/server/transcribe-voice.d.ts +1 -1
  132. package/dist/shared/password-policy.d.ts +2 -1
  133. package/dist/shared/password-policy.js +2 -1
  134. package/dist/styles/agent-conversation.css +20 -3
  135. package/dist/styles/agent-native.css +14 -2
  136. package/dist/templates/default/app/i18n/ar-SA.ts +1 -1
  137. package/dist/templates/default/app/i18n/de-DE.ts +1 -1
  138. package/dist/templates/default/app/i18n/en-US.ts +1 -1
  139. package/dist/templates/default/app/i18n/es-ES.ts +1 -1
  140. package/dist/templates/default/app/i18n/fr-FR.ts +1 -1
  141. package/dist/templates/default/app/i18n/hi-IN.ts +1 -1
  142. package/dist/templates/default/app/i18n/ja-JP.ts +1 -1
  143. package/dist/templates/default/app/i18n/ko-KR.ts +1 -1
  144. package/dist/templates/default/app/i18n/pt-BR.ts +1 -1
  145. package/dist/templates/default/app/i18n/zh-CN.ts +1 -1
  146. package/dist/templates/default/app/i18n/zh-TW.ts +1 -1
  147. package/dist/templates/default/pnpm-workspace.yaml +1 -0
  148. package/dist/templates/workspace-root/pnpm-workspace.yaml +2 -0
  149. package/docs/content/cloneable-saas.mdx +6 -6
  150. package/docs/content/deployment.mdx +10 -8
  151. package/docs/content/environment-variables.mdx +4 -3
  152. package/package.json +2 -2
  153. package/src/templates/default/app/i18n/ar-SA.ts +1 -1
  154. package/src/templates/default/app/i18n/de-DE.ts +1 -1
  155. package/src/templates/default/app/i18n/en-US.ts +1 -1
  156. package/src/templates/default/app/i18n/es-ES.ts +1 -1
  157. package/src/templates/default/app/i18n/fr-FR.ts +1 -1
  158. package/src/templates/default/app/i18n/hi-IN.ts +1 -1
  159. package/src/templates/default/app/i18n/ja-JP.ts +1 -1
  160. package/src/templates/default/app/i18n/ko-KR.ts +1 -1
  161. package/src/templates/default/app/i18n/pt-BR.ts +1 -1
  162. package/src/templates/default/app/i18n/zh-CN.ts +1 -1
  163. package/src/templates/default/app/i18n/zh-TW.ts +1 -1
  164. package/src/templates/default/pnpm-workspace.yaml +1 -0
  165. package/src/templates/workspace-root/pnpm-workspace.yaml +2 -0
@@ -17,12 +17,6 @@ import {
17
17
  import { CSS } from "@dnd-kit/utilities";
18
18
  import { appStateKeyForBrowserTab } from "@shared/app-state-tabs";
19
19
  import { hashSlideContent, type DeckFitState } from "@shared/slide-fit";
20
- import {
21
- IconGripVertical,
22
- IconCopy,
23
- IconTrash,
24
- IconLoader2,
25
- } from "@tabler/icons-react";
26
20
  import { useRef, useEffect } from "react";
27
21
  import { useCallback } from "react";
28
22
 
@@ -35,7 +29,7 @@ import {
35
29
  TooltipTrigger,
36
30
  } from "@/components/ui/tooltip";
37
31
  import type { Slide } from "@/context/DeckContext";
38
- import type { AspectRatio } from "@/lib/aspect-ratios";
32
+ import { getAspectRatioDims, type AspectRatio } from "@/lib/aspect-ratios";
39
33
  import { TAB_ID } from "@/lib/tab-id";
40
34
 
41
35
  import type { DesignSystemData } from "../../../shared/api";
@@ -45,8 +39,6 @@ interface EditorSidebarProps {
45
39
  activeSlideId: string;
46
40
  deckId: string;
47
41
  onSelectSlide: (id: string) => void;
48
- onDuplicateSlide: (id: string) => void;
49
- onDeleteSlide: (id: string) => void;
50
42
  /** Viewer-role decks get thumbnails only: no add, duplicate, or delete. */
51
43
  readOnly?: boolean;
52
44
  /** Presence map: slideId → list of users currently viewing that slide */
@@ -140,21 +132,11 @@ function PresenceAvatarTip({
140
132
  );
141
133
  }
142
134
 
143
- // Thumbnail overlays sit on top of rendered slide artwork, which is arbitrary
144
- // user content rather than a themed app surface, so they use a fixed scrim.
145
- const THUMB_OVERLAY_CLASS =
146
- // guard:allow-raw-color — matches the duplicate/delete overlays below.
147
- "absolute top-2 left-7 rounded bg-black/60 p-1 backdrop-blur-sm border border-white/10 cursor-grab active:cursor-grabbing sm:opacity-0 sm:group-hover:opacity-100";
148
- // guard:allow-raw-color — same fixed scrim as the class above.
149
- const THUMB_OVERLAY_ICON_CLASS = "w-3 h-3 text-white/60";
150
-
151
135
  function SortableSlideThumb({
152
136
  slide,
153
137
  index,
154
138
  isActive,
155
139
  onSelect,
156
- onDuplicate,
157
- onDelete,
158
140
  registerButtonRef,
159
141
  presenceUsers = [],
160
142
  aspectRatio,
@@ -166,8 +148,6 @@ function SortableSlideThumb({
166
148
  index: number;
167
149
  isActive: boolean;
168
150
  onSelect: () => void;
169
- onDuplicate: () => void;
170
- onDelete: () => void;
171
151
  readOnly?: boolean;
172
152
  registerButtonRef: (slideId: string, node: HTMLButtonElement | null) => void;
173
153
  presenceUsers?: CollabUser[];
@@ -194,18 +174,25 @@ function SortableSlideThumb({
194
174
  opacity: isDragging ? 0.5 : 1,
195
175
  };
196
176
 
177
+ const thumbDims = getAspectRatioDims(aspectRatio);
178
+
197
179
  return (
198
- <div ref={setNodeRef} style={style} className="group relative">
180
+ <div ref={setNodeRef} style={style}>
199
181
  <button
200
182
  ref={(node) => registerButtonRef(slide.id, node)}
183
+ type="button"
184
+ {...(readOnly ? {} : attributes)}
185
+ {...(readOnly ? {} : listeners)}
201
186
  onClick={onSelect}
202
187
  onFocus={onSelect}
203
188
  aria-label={t("editorSidebar.selectSlide", { number: index + 1 })}
204
189
  aria-current={isActive ? "true" : undefined}
205
190
  data-slide-thumbnail-id={slide.id}
206
191
  className={`w-full text-left flex items-start gap-1.5 p-1.5 rounded-lg transition-[background-color,box-shadow] duration-150 ${
207
- isActive ? "bg-accent ring-1 ring-[#609FF8]/50" : "hover:bg-accent"
208
- } focus:outline-none`}
192
+ isActive ? "bg-accent ring-1 ring-primary/50" : ""
193
+ } ${
194
+ readOnly ? "" : "cursor-grab active:cursor-grabbing"
195
+ } focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1`}
209
196
  >
210
197
  {/* Index and slide presence share the fixed rail so presence does not resize the row. */}
211
198
  <div className="relative flex-shrink-0 w-4 self-stretch">
@@ -228,6 +215,11 @@ function SortableSlideThumb({
228
215
 
229
216
  {/* Thumbnail */}
230
217
  <div className="flex-1 min-w-0">
218
+ {/* Each thumbnail paints a full-resolution slide canvas scaled down by
219
+ * transform, so a long rail keeps dozens of large layers live and the
220
+ * browser drops frames or paints stale tiles. `content-visibility`
221
+ * lets it skip everything below the fold; `aspect-ratio` keeps the row
222
+ * the right height while its contents are skipped. */}
231
223
  <div
232
224
  className="w-full overflow-hidden rounded border"
233
225
  style={{
@@ -235,6 +227,8 @@ function SortableSlideThumb({
235
227
  presenceUsers.length > 0
236
228
  ? presenceUsers[0].color + "66"
237
229
  : "rgba(255,255,255,0.06)",
230
+ aspectRatio: `${thumbDims.width} / ${thumbDims.height}`,
231
+ contentVisibility: "auto",
238
232
  }}
239
233
  >
240
234
  <SlideRenderer
@@ -246,50 +240,6 @@ function SortableSlideThumb({
246
240
  </div>
247
241
  </div>
248
242
  </button>
249
-
250
- {/* Drag handle — overlaid on the thumbnail instead of holding its own
251
- * column, so the rail stays narrow. Mirrors the action buttons opposite. */}
252
- {!readOnly && (
253
- <div {...attributes} {...listeners} className={THUMB_OVERLAY_CLASS}>
254
- <IconGripVertical className={THUMB_OVERLAY_ICON_CLASS} />
255
- </div>
256
- )}
257
-
258
- {/* Actions - always visible on touch devices */}
259
- {!readOnly && (
260
- <div className="absolute top-2 right-2 flex gap-0.5 sm:opacity-0 sm:group-hover:opacity-100">
261
- <Tooltip>
262
- <TooltipTrigger asChild>
263
- <button
264
- onClick={(e) => {
265
- e.stopPropagation();
266
- onDuplicate();
267
- }}
268
- className="p-1.5 rounded bg-black/60 backdrop-blur-sm border border-white/10 hover:bg-black/80"
269
- aria-label={t("editorSidebar.duplicateSlide")}
270
- >
271
- <IconCopy className="w-3 h-3 text-white/60" />
272
- </button>
273
- </TooltipTrigger>
274
- <TooltipContent>{t("editorSidebar.duplicate")}</TooltipContent>
275
- </Tooltip>
276
- <Tooltip>
277
- <TooltipTrigger asChild>
278
- <button
279
- onClick={(e) => {
280
- e.stopPropagation();
281
- onDelete();
282
- }}
283
- className="p-1.5 rounded bg-black/60 backdrop-blur-sm border border-white/10 hover:bg-red-900/80"
284
- aria-label={t("editorSidebar.deleteSlide")}
285
- >
286
- <IconTrash className="w-3 h-3 text-white/60" />
287
- </button>
288
- </TooltipTrigger>
289
- <TooltipContent>{t("editorSidebar.delete")}</TooltipContent>
290
- </Tooltip>
291
- </div>
292
- )}
293
243
  </div>
294
244
  );
295
245
  }
@@ -313,14 +263,14 @@ function GeneratingSlideSkeleton({
313
263
  return (
314
264
  <button
315
265
  type="button"
316
- className={`group relative block w-full rounded-lg text-left transition-colors ${
317
- selected ? "bg-accent ring-1 ring-ring" : "hover:bg-accent/50"
266
+ className={`relative block w-full rounded-lg text-left transition-[box-shadow] ${
267
+ selected ? "ring-1 ring-primary" : ""
318
268
  }`}
319
269
  aria-label={t("editorSidebar.generatingSlide")}
320
270
  aria-current={selected ? "true" : undefined}
321
271
  onClick={onSelect}
322
272
  >
323
- <div className="w-full flex items-start gap-1.5 p-1.5 rounded-lg bg-accent/30">
273
+ <div className="flex w-full items-start gap-1.5 rounded-lg p-1.5">
324
274
  <span className="flex-shrink-0 w-4 text-center text-[10px] font-medium leading-5 text-muted-foreground/70">
325
275
  {index + 1}
326
276
  </span>
@@ -342,8 +292,6 @@ export default function EditorSidebar({
342
292
  activeSlideId,
343
293
  deckId,
344
294
  onSelectSlide,
345
- onDuplicateSlide,
346
- onDeleteSlide,
347
295
  readOnly = false,
348
296
  slidePresence,
349
297
  recentEdits,
@@ -513,8 +461,6 @@ export default function EditorSidebar({
513
461
  index={index}
514
462
  isActive={slide.id === activeSlideId}
515
463
  onSelect={() => onSelectSlide(slide.id)}
516
- onDuplicate={() => onDuplicateSlide(slide.id)}
517
- onDelete={() => onDeleteSlide(slide.id)}
518
464
  readOnly={readOnly}
519
465
  registerButtonRef={registerSlideButton}
520
466
  presenceUsers={slidePresence?.get(slide.id) ?? []}
@@ -543,6 +489,7 @@ export default function EditorSidebar({
543
489
  edits={recentEdits}
544
490
  resolveRect={resolveThumbRect}
545
491
  containerRef={thumbScrollRef}
492
+ outlineOnly
546
493
  />
547
494
  )}
548
495
  </div>
@@ -123,11 +123,13 @@ export function ExportMenu({
123
123
  });
124
124
  return;
125
125
  }
126
- console.warn("Google Slides upload unavailable:", result.reason);
127
126
  if (target) target.location.href = GOOGLE_SLIDES_IMPORT_URL;
128
127
  setGoogleSlidesImportOpen(true);
129
- toast.success(t("editorExport.googleSlidesDownloaded"), {
130
- description: t("editorExport.googleSlidesImportHint"),
128
+ // The deck did not reach Drive. Saying "success" here is why users read
129
+ // the .pptx download as the intended result and never learn that their
130
+ // Google account is unconnected or that Drive rejected the upload.
131
+ toast.warning(t("editorExport.googleSlidesDownloaded"), {
132
+ description: `${result.reason} ${t("editorExport.googleSlidesImportHint")}`,
131
133
  });
132
134
  } catch (err) {
133
135
  googleSlidesImportTarget.current = null;
@@ -47,7 +47,7 @@ export default function GeneratingSlidePreview({
47
47
  return (
48
48
  <div
49
49
  className={cn(
50
- "relative w-full overflow-hidden rounded-lg border border-border/60 bg-muted/20",
50
+ "relative w-full overflow-hidden rounded-lg border border-primary/70",
51
51
  className,
52
52
  )}
53
53
  style={{ aspectRatio: cssRatio }}
@@ -65,6 +65,10 @@ export default function GeneratingSlidePreview({
65
65
  ) : (
66
66
  <SlideLoadingArtwork />
67
67
  )}
68
+ <div className="pointer-events-none absolute left-2 top-2 inline-flex items-center gap-1.5 rounded-full border border-primary/70 px-2 py-1 text-[11px] font-medium text-primary">
69
+ <span className="size-1.5 rounded-full bg-primary" aria-hidden="true" />
70
+ {t("raw.aiEditing")}
71
+ </div>
68
72
  <div className="absolute right-2 top-2 rounded-full bg-background/80 p-1.5 text-muted-foreground shadow-sm backdrop-blur">
69
73
  <IconLoader2 className="size-3.5 animate-spin" aria-hidden="true" />
70
74
  </div>
@@ -13,6 +13,7 @@ import { Button } from "../ui/button";
13
13
  interface GoogleDocsStatus {
14
14
  configured: boolean;
15
15
  connected: boolean;
16
+ googleSlidesUrlImportReady?: boolean;
16
17
  error?: string;
17
18
  message?: string;
18
19
  }
@@ -97,6 +98,12 @@ export function GoogleDriveConnectionCta({
97
98
  void refreshStatus();
98
99
  }, [refreshStatus]);
99
100
 
101
+ const needsReconnect =
102
+ status?.connected === true && status.googleSlidesUrlImportReady === false;
103
+ const requiresUrlImportAccess =
104
+ status?.connected === true ||
105
+ status?.googleSlidesUrlImportReady !== undefined;
106
+
100
107
  const connect = useCallback(async () => {
101
108
  if (connecting) return;
102
109
  setConnecting(true);
@@ -138,23 +145,27 @@ export function GoogleDriveConnectionCta({
138
145
  while (Date.now() < deadline && !popup.closed) {
139
146
  await new Promise((resolve) => window.setTimeout(resolve, 1_200));
140
147
  const next = await refreshStatus();
141
- if (next?.connected) {
148
+ const nextHasUrlImportAccess =
149
+ next?.googleSlidesUrlImportReady ?? !requiresUrlImportAccess;
150
+ if (next?.connected && nextHasUrlImportAccess) {
142
151
  popup.close();
143
152
  onConnected?.();
144
153
  return;
145
154
  }
146
155
  }
147
156
  const next = await refreshStatus();
148
- if (next?.connected) onConnected?.();
157
+ const nextHasUrlImportAccess =
158
+ next?.googleSlidesUrlImportReady ?? !requiresUrlImportAccess;
159
+ if (next?.connected && nextHasUrlImportAccess) onConnected?.();
149
160
  } catch (caught) {
150
161
  popup?.close();
151
162
  setError(caught instanceof Error ? caught.message : String(caught));
152
163
  } finally {
153
164
  setConnecting(false);
154
165
  }
155
- }, [connecting, onConnected, refreshStatus]);
166
+ }, [connecting, onConnected, refreshStatus, requiresUrlImportAccess]);
156
167
 
157
- if (loading || status?.connected) return null;
168
+ if (loading || (status?.connected && !needsReconnect)) return null;
158
169
 
159
170
  const displayStatus = status ?? { configured: false, connected: false };
160
171
 
@@ -48,6 +48,17 @@ export interface NewDeckReferenceSelection {
48
48
  value: string;
49
49
  } | null;
50
50
  }
51
+
52
+ export type NewDeckReferenceSource = NonNullable<
53
+ NewDeckReferenceSelection["referenceSource"]
54
+ >;
55
+
56
+ export interface ImportedReference {
57
+ id: string;
58
+ title: string;
59
+ source: "pptx" | "pdf" | "google-slides";
60
+ }
61
+
51
62
  interface DesignSystemOption {
52
63
  id: string;
53
64
  title: string;
@@ -61,9 +72,12 @@ interface NewDeckReferenceStepProps {
61
72
  defaultDesignSystemId: string | null;
62
73
  defaultReferenceDeckId: string | null;
63
74
  recentReferences: RecentReference[];
64
- onSelect: (selection: NewDeckReferenceSelection) => void;
65
- onImport: (files: File[]) => Promise<void>;
66
- onSkip: () => void;
75
+ onSelect: (selection: NewDeckReferenceSelection) => void | Promise<void>;
76
+ onImport: (files: File[]) => Promise<ImportedReference | null>;
77
+ onImportSource: (
78
+ source: NewDeckReferenceSource,
79
+ ) => Promise<ImportedReference | null>;
80
+ onSkip: () => void | Promise<void>;
67
81
  onOpenChange: (open: boolean) => void;
68
82
  importing?: boolean;
69
83
  title: string;
@@ -72,7 +86,6 @@ interface NewDeckReferenceStepProps {
72
86
  chooseDeckLabel: string;
73
87
  importingLabel: string;
74
88
  skipLabel: string;
75
- defaultSuffix: string;
76
89
  starredLabel: string;
77
90
  otherDecksLabel: string;
78
91
  searchDecksLabel: string;
@@ -88,6 +101,7 @@ export function NewDeckReferenceStep({
88
101
  recentReferences,
89
102
  onSelect,
90
103
  onImport,
104
+ onImportSource,
91
105
  onSkip,
92
106
  onOpenChange,
93
107
  importing = false,
@@ -97,7 +111,6 @@ export function NewDeckReferenceStep({
97
111
  chooseDeckLabel,
98
112
  importingLabel,
99
113
  skipLabel,
100
- defaultSuffix,
101
114
  starredLabel,
102
115
  otherDecksLabel,
103
116
  searchDecksLabel,
@@ -110,6 +123,8 @@ export function NewDeckReferenceStep({
110
123
  const [selectedReferenceDeckId, setSelectedReferenceDeckId] = useState<
111
124
  string | null
112
125
  >(defaultReferenceDeckId);
126
+ const [importedReference, setImportedReference] =
127
+ useState<ImportedReference | null>(null);
113
128
  const [selectedSource, setSelectedSource] =
114
129
  useState<NewDeckReferenceSelection["referenceSource"]>(null);
115
130
  const [referenceDeckSearchOpen, setReferenceDeckSearchOpen] = useState(false);
@@ -141,9 +156,6 @@ export function NewDeckReferenceStep({
141
156
  );
142
157
  const starredDecks = decks.filter((deck) => deck.starred);
143
158
  const otherDecks = decks.filter((deck) => !deck.starred);
144
- const selectedDesignSystem = selectedDesignSystemId
145
- ? designSystemById.get(selectedDesignSystemId)
146
- : undefined;
147
159
  const selectedReferenceDeck = selectedReferenceDeckId
148
160
  ? deckById.get(selectedReferenceDeckId)
149
161
  : undefined;
@@ -152,6 +164,7 @@ export function NewDeckReferenceStep({
152
164
  if (!open) return;
153
165
  setSelectedDesignSystemId(defaultDesignSystemId);
154
166
  setSelectedReferenceDeckId(defaultReferenceDeckId);
167
+ setImportedReference(null);
155
168
  setSelectedSource(null);
156
169
  setReferenceDeckSearchOpen(false);
157
170
  }, [open, defaultDesignSystemId, defaultReferenceDeckId]);
@@ -160,17 +173,33 @@ export function NewDeckReferenceStep({
160
173
  const files = Array.from(event.target.files ?? []);
161
174
  event.target.value = "";
162
175
  if (files.length === 0) return;
163
- await onImport(files);
176
+ const imported = await onImport(files);
177
+ if (imported) applyImportedReference(imported);
164
178
  };
165
179
 
166
- const handleContinue = () => {
180
+ const applyImportedReference = (imported: ImportedReference) => {
181
+ setSelectedDesignSystemId(null);
182
+ setSelectedReferenceDeckId(imported.id);
183
+ setSelectedSource(null);
184
+ setImportedReference(imported);
185
+ };
186
+
187
+ const handleContinue = async () => {
188
+ const trimmedSource =
189
+ selectedSource && selectedSource.value.trim()
190
+ ? { ...selectedSource, value: selectedSource.value.trim() }
191
+ : null;
192
+
193
+ if (trimmedSource?.kind === "google-docs") {
194
+ const imported = await onImportSource(trimmedSource);
195
+ if (imported) applyImportedReference(imported);
196
+ return;
197
+ }
198
+
167
199
  onSelect({
168
200
  designSystemId: selectedDesignSystemId,
169
201
  referenceDeckId: selectedReferenceDeckId,
170
- referenceSource:
171
- selectedSource && selectedSource.value.trim()
172
- ? { ...selectedSource, value: selectedSource.value.trim() }
173
- : null,
202
+ referenceSource: trimmedSource,
174
203
  });
175
204
  };
176
205
 
@@ -182,23 +211,20 @@ export function NewDeckReferenceStep({
182
211
  setSelectedDesignSystemId(null);
183
212
  } else {
184
213
  setSelectedReferenceDeckId(null);
214
+ setImportedReference(null);
185
215
  }
186
216
  };
187
217
  const selectedSourceLabel =
188
218
  selectedSource?.kind === "google-docs"
189
- ? "Google Docs"
219
+ ? t("home.googleSlidesReferenceTitle")
190
220
  : selectedSource?.kind === "website"
191
221
  ? "Website"
192
222
  : "Figma";
193
- const selectedSourceIcon =
194
- selectedSource?.kind === "google-docs" ? (
195
- <IconBrandGoogle className="size-4" />
196
- ) : selectedSource?.kind === "website" ? (
197
- <IconWorld className="size-4" />
198
- ) : (
199
- <IconBrandFigma className="size-4" />
200
- );
201
-
223
+ const selectedReferenceDeckTitle =
224
+ selectedReferenceDeck?.title ??
225
+ (selectedReferenceDeckId === importedReference?.id
226
+ ? importedReference.title
227
+ : undefined);
202
228
  return open ? (
203
229
  <div
204
230
  className="fixed inset-0 z-[200] flex min-h-screen flex-col bg-background text-foreground"
@@ -228,29 +254,6 @@ export function NewDeckReferenceStep({
228
254
  </p>
229
255
  )}
230
256
 
231
- {(selectedDesignSystem || selectedSource) && (
232
- <div className="mt-8 space-y-2">
233
- {selectedDesignSystem && (
234
- <ReferenceRow
235
- icon={<IconPalette className="size-4" />}
236
- label={selectedDesignSystem.title}
237
- meta={`${designSystemLabel}${defaultDesignSystemId === selectedDesignSystem.id ? defaultSuffix : ""}`}
238
- selected
239
- onClick={() => setSelectedDesignSystemId(null)}
240
- />
241
- )}
242
- {selectedSource && (
243
- <ReferenceRow
244
- icon={selectedSourceIcon}
245
- label={selectedSourceLabel}
246
- meta={selectedSource.value || "Add a link below"}
247
- selected
248
- onClick={() => setSelectedSource(null)}
249
- />
250
- )}
251
- </div>
252
- )}
253
-
254
257
  <div className="mt-10 space-y-6">
255
258
  <div className="grid gap-2">
256
259
  <div className="flex items-center justify-between gap-3">
@@ -309,7 +312,7 @@ export function NewDeckReferenceStep({
309
312
  className="flex h-10 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
310
313
  >
311
314
  <span className="truncate">
312
- {selectedReferenceDeck?.title ?? chooseDeckLabel}
315
+ {selectedReferenceDeckTitle ?? chooseDeckLabel}
313
316
  </span>
314
317
  <IconChevronDown className="size-4 shrink-0 opacity-50" />
315
318
  </button>
@@ -336,6 +339,7 @@ export function NewDeckReferenceStep({
336
339
  value={`${deck.title} ${deck.id}`}
337
340
  onSelect={() => {
338
341
  setSelectedReferenceDeckId(deck.id);
342
+ setImportedReference(null);
339
343
  setSelectedSource(null);
340
344
  setReferenceDeckSearchOpen(false);
341
345
  }}
@@ -361,6 +365,7 @@ export function NewDeckReferenceStep({
361
365
  value={`${deck.title} ${deck.id}`}
362
366
  onSelect={() => {
363
367
  setSelectedReferenceDeckId(deck.id);
368
+ setImportedReference(null);
364
369
  setSelectedSource(null);
365
370
  setReferenceDeckSearchOpen(false);
366
371
  }}
@@ -430,6 +435,8 @@ export function NewDeckReferenceStep({
430
435
  accept=".pptx"
431
436
  icon={<IconPresentation className="size-4" />}
432
437
  label="PPT"
438
+ imported={importedReference?.source === "pptx"}
439
+ importedLabel={t("home.imported")}
433
440
  importing={importing}
434
441
  importingLabel={importingLabel}
435
442
  onChange={handleImport}
@@ -438,6 +445,8 @@ export function NewDeckReferenceStep({
438
445
  accept=".pdf"
439
446
  icon={<IconFileTypePdf className="size-4" />}
440
447
  label="PDF"
448
+ imported={importedReference?.source === "pdf"}
449
+ importedLabel={t("home.imported")}
441
450
  importing={importing}
442
451
  importingLabel={importingLabel}
443
452
  onChange={handleImport}
@@ -446,25 +455,34 @@ export function NewDeckReferenceStep({
446
455
  accept=".docx"
447
456
  icon={<IconFileText className="size-4" />}
448
457
  label="DOCX"
458
+ imported={false}
459
+ importedLabel={t("home.imported")}
449
460
  importing={importing}
450
461
  importingLabel={importingLabel}
451
462
  onChange={handleImport}
452
463
  />
453
464
  <ImportOption
454
465
  icon={<IconBrandGoogle className="size-4" />}
455
- label="GDocs"
456
- selected={selectedSource?.kind === "google-docs"}
466
+ label={t("home.googleSlidesReferenceTitle")}
467
+ confirmed={importedReference?.source === "google-slides"}
468
+ confirmedLabel={t("home.imported")}
469
+ selected={
470
+ selectedSource?.kind === "google-docs" ||
471
+ importedReference?.source === "google-slides"
472
+ }
457
473
  onClick={() => chooseSource("google-docs")}
458
474
  />
459
475
  <ImportOption
460
476
  icon={<IconWorld className="size-4" />}
461
477
  label="Website"
478
+ confirmedLabel={t("home.imported")}
462
479
  selected={selectedSource?.kind === "website"}
463
480
  onClick={() => chooseSource("website")}
464
481
  />
465
482
  <ImportOption
466
483
  icon={<IconBrandFigma className="size-4" />}
467
484
  label="Figma"
485
+ confirmedLabel={t("home.imported")}
468
486
  selected={selectedSource?.kind === "figma"}
469
487
  onClick={() => chooseSource("figma")}
470
488
  />
@@ -484,23 +502,54 @@ export function NewDeckReferenceStep({
484
502
  }
485
503
  />
486
504
  )}
505
+ {importedReference && (
506
+ <div
507
+ className="mt-3 flex items-start gap-3 rounded-lg border border-primary/30 bg-primary/5 px-3 py-2.5 text-sm"
508
+ role="status"
509
+ aria-live="polite"
510
+ >
511
+ <span className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
512
+ <IconCheck className="size-3.5" />
513
+ </span>
514
+ <div className="min-w-0">
515
+ <p className="font-medium text-foreground">
516
+ {t("home.referenceImportSuccess")}
517
+ </p>
518
+ <p className="truncate text-xs text-muted-foreground">
519
+ {t("home.referenceImportSelected", {
520
+ title: importedReference.title,
521
+ })}
522
+ </p>
523
+ </div>
524
+ </div>
525
+ )}
487
526
  </div>
488
527
  </div>
489
528
  </div>
490
529
  </main>
491
530
 
492
531
  <footer className="flex shrink-0 items-center justify-between border-t border-border px-5 py-4 sm:px-8">
493
- <Button type="button" variant="ghost" onClick={onSkip}>
532
+ <Button
533
+ type="button"
534
+ variant="ghost"
535
+ onClick={onSkip}
536
+ disabled={importing}
537
+ >
494
538
  {skipLabel}
495
539
  </Button>
496
540
  <Button
497
541
  type="button"
498
- onClick={handleContinue}
542
+ onClick={() => void handleContinue()}
543
+ aria-busy={importing}
499
544
  disabled={
500
545
  importing || Boolean(selectedSource && !selectedSource.value.trim())
501
546
  }
502
547
  >
503
- Continue
548
+ {importing
549
+ ? importingLabel
550
+ : importedReference
551
+ ? t("home.continueToGenerate")
552
+ : t("home.continue")}
504
553
  <IconCheck className="ms-1.5 size-4" />
505
554
  </Button>
506
555
  </footer>
@@ -512,6 +561,8 @@ function FileImportOption({
512
561
  accept,
513
562
  icon,
514
563
  label,
564
+ imported = false,
565
+ importedLabel,
515
566
  importing,
516
567
  importingLabel,
517
568
  onChange,
@@ -519,6 +570,8 @@ function FileImportOption({
519
570
  accept: string;
520
571
  icon: ReactNode;
521
572
  label: string;
573
+ imported?: boolean;
574
+ importedLabel: string;
522
575
  importing: boolean;
523
576
  importingLabel: string;
524
577
  onChange: (event: ChangeEvent<HTMLInputElement>) => void;
@@ -529,8 +582,9 @@ function FileImportOption({
529
582
  "flex cursor-pointer items-center justify-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-accent",
530
583
  importing && "pointer-events-none opacity-60",
531
584
  )}
585
+ aria-label={imported ? `${label} - ${importedLabel}` : label}
532
586
  >
533
- {icon}
587
+ {imported ? <IconCheck className="size-4 text-primary" /> : icon}
534
588
  <span>{importing ? importingLabel : label}</span>
535
589
  <input
536
590
  type="file"
@@ -547,11 +601,15 @@ function FileImportOption({
547
601
  function ImportOption({
548
602
  icon,
549
603
  label,
604
+ confirmed = false,
605
+ confirmedLabel,
550
606
  selected = false,
551
607
  onClick,
552
608
  }: {
553
609
  icon: ReactNode;
554
610
  label: string;
611
+ confirmed?: boolean;
612
+ confirmedLabel?: string;
555
613
  selected?: boolean;
556
614
  onClick: () => void;
557
615
  }) {
@@ -565,9 +623,12 @@ function ImportOption({
565
623
  ? "border-primary/50 bg-primary/5 text-primary"
566
624
  : "border-border hover:bg-accent",
567
625
  )}
626
+ aria-label={
627
+ confirmed && confirmedLabel ? `${label} - ${confirmedLabel}` : label
628
+ }
568
629
  aria-pressed={selected}
569
630
  >
570
- {icon}
631
+ {confirmed ? <IconCheck className="size-4 text-primary" /> : icon}
571
632
  <span>{label}</span>
572
633
  </button>
573
634
  );