@agent-native/core 0.141.3 → 0.141.4

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 (152) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
  3. package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
  4. package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
  5. package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
  7. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  8. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  9. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  10. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
  12. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
  13. package/corpus/templates/analytics/server/lib/gong.ts +132 -33
  14. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  15. package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
  16. package/corpus/templates/analytics/server/plugins/db.ts +11 -3
  17. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  18. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
  19. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  20. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  21. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  22. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  23. package/corpus/templates/clips/actions/invite-member.ts +39 -16
  24. package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
  25. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  28. package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
  29. package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
  31. package/corpus/templates/clips/desktop/src/styles.css +0 -3
  32. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  33. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  34. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  35. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
  36. package/corpus/templates/clips/shared/email-preview.ts +61 -0
  37. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  38. package/corpus/templates/content/server/lib/emails.ts +70 -0
  39. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  40. package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
  41. package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
  42. package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
  43. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  44. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  45. package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
  46. package/corpus/templates/factory/AGENTS.md +18 -17
  47. package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
  48. package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
  49. package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
  50. package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
  51. package/corpus/templates/factory/app/routes/factory.tsx +376 -206
  52. package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
  53. package/corpus/templates/factory/server/db/schema.ts +6 -0
  54. package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
  55. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
  56. package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
  57. package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
  58. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  59. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  60. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  61. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  62. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  63. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  64. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  65. package/corpus/templates/slides/actions/import-from-url.ts +1 -0
  66. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
  67. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
  68. package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
  69. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
  70. package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
  71. package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
  72. package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
  73. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
  74. package/corpus/templates/slides/app/pages/Index.tsx +127 -137
  75. package/corpus/templates/slides/app/root.tsx +8 -0
  76. package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
  77. package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
  79. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  80. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  81. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  82. package/dist/brand-kit/index.d.ts +1 -0
  83. package/dist/brand-kit/index.js +1 -0
  84. package/dist/brand-kit/tokens.d.ts +91 -0
  85. package/dist/brand-kit/tokens.js +279 -0
  86. package/dist/brand-kit/types.d.ts +25 -0
  87. package/dist/client/AssistantChat.js +7 -7
  88. package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
  89. package/dist/client/onboarding/first-run-registry.d.ts +25 -0
  90. package/dist/client/onboarding/first-run-registry.js +18 -0
  91. package/dist/client/onboarding/index.d.ts +2 -0
  92. package/dist/client/onboarding/index.js +1 -0
  93. package/dist/collab/routes.d.ts +1 -1
  94. package/dist/deploy/build.d.ts +8 -0
  95. package/dist/deploy/build.js +92 -3
  96. package/dist/deploy/workspace-deploy.js +86 -3
  97. package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
  98. package/dist/email-catalog/actions/list-email-activity.js +19 -0
  99. package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
  100. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  101. package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
  102. package/dist/email-catalog/actions/list-email-log.js +23 -0
  103. package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
  104. package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
  105. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
  106. package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
  107. package/dist/email-catalog/authorize.d.ts +1 -0
  108. package/dist/email-catalog/authorize.js +9 -0
  109. package/dist/email-catalog/log.d.ts +60 -0
  110. package/dist/email-catalog/log.js +142 -0
  111. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  112. package/dist/email-catalog/provider-metrics.js +182 -0
  113. package/dist/email-catalog/registry.d.ts +78 -0
  114. package/dist/email-catalog/registry.js +70 -0
  115. package/dist/email-catalog/schema.d.ts +231 -0
  116. package/dist/email-catalog/schema.js +53 -0
  117. package/dist/email-catalog/system-emails.d.ts +13 -0
  118. package/dist/email-catalog/system-emails.js +67 -0
  119. package/dist/jobs/index.d.ts +2 -0
  120. package/dist/jobs/index.js +2 -0
  121. package/dist/jobs/run-history.js +48 -1
  122. package/dist/jobs/scheduler-dispatch.d.ts +7 -0
  123. package/dist/jobs/scheduler-dispatch.js +7 -0
  124. package/dist/jobs/scheduler-health.d.ts +22 -0
  125. package/dist/jobs/scheduler-health.js +172 -0
  126. package/dist/jobs/scheduler.js +67 -2
  127. package/dist/localization/default-messages.d.ts +45 -0
  128. package/dist/localization/default-messages.js +45 -0
  129. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  130. package/dist/notifications/routes.d.ts +3 -3
  131. package/dist/observability/routes.d.ts +5 -5
  132. package/dist/org/handlers.js +9 -1
  133. package/dist/progress/routes.d.ts +1 -1
  134. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  135. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  136. package/dist/secrets/routes.d.ts +9 -9
  137. package/dist/server/action-discovery.js +22 -0
  138. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
  139. package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
  140. package/dist/server/agent-chat-plugin.d.ts +2 -1
  141. package/dist/server/agent-chat-plugin.js +39 -1
  142. package/dist/server/better-auth-instance.js +17 -2
  143. package/dist/server/design-token-utils.d.ts +6 -1
  144. package/dist/server/design-token-utils.js +252 -59
  145. package/dist/server/email.d.ts +11 -0
  146. package/dist/server/email.js +54 -3
  147. package/dist/server/index.d.ts +1 -0
  148. package/dist/server/index.js +1 -0
  149. package/dist/server/realtime-token.d.ts +1 -1
  150. package/dist/server/transcribe-voice.d.ts +1 -1
  151. package/dist/vite/client.js +20 -14
  152. package/package.json +2 -1
@@ -1,42 +1,53 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
1
2
  import {
2
- IconArrowRight,
3
+ IconArrowLeft,
4
+ IconBrandFigma,
5
+ IconBrandGoogle,
3
6
  IconCheck,
7
+ IconChevronDown,
8
+ IconFileText,
9
+ IconFileTypePdf,
4
10
  IconPalette,
5
11
  IconPresentation,
6
- IconSearch,
7
- IconStar,
8
- IconUpload,
9
- IconX,
12
+ IconWorld,
10
13
  } from "@tabler/icons-react";
14
+ import { useEffect, useState } from "react";
11
15
  import type { ChangeEvent, ReactNode } from "react";
12
- import { useMemo, useState } from "react";
13
16
 
14
17
  import { Button } from "@/components/ui/button";
15
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
16
18
  import {
17
- Dialog,
18
- DialogContent,
19
- DialogDescription,
20
- DialogHeader,
21
- DialogTitle,
22
- } from "@/components/ui/dialog";
19
+ Command,
20
+ CommandEmpty,
21
+ CommandGroup,
22
+ CommandInput,
23
+ CommandItem,
24
+ CommandList,
25
+ } from "@/components/ui/command";
23
26
  import { Input } from "@/components/ui/input";
24
- import { ScrollArea } from "@/components/ui/scroll-area";
27
+ import {
28
+ Popover,
29
+ PopoverContent,
30
+ PopoverTrigger,
31
+ } from "@/components/ui/popover";
25
32
  import {
26
33
  Select,
27
34
  SelectContent,
28
- SelectGroup,
29
35
  SelectItem,
30
- SelectLabel,
31
36
  SelectTrigger,
32
37
  SelectValue,
33
38
  } from "@/components/ui/select";
34
- import { Separator } from "@/components/ui/separator";
35
39
  import type { Deck } from "@/context/DeckContext";
40
+ import type { RecentReference } from "@/lib/recent-references";
36
41
  import { cn } from "@/lib/utils";
37
42
 
38
- import { GoogleSlidesReferenceImport } from "./GoogleSlidesReferenceImport";
39
-
43
+ export interface NewDeckReferenceSelection {
44
+ designSystemId?: string | null;
45
+ referenceDeckId?: string | null;
46
+ referenceSource?: {
47
+ kind: "google-docs" | "website" | "figma";
48
+ value: string;
49
+ } | null;
50
+ }
40
51
  interface DesignSystemOption {
41
52
  id: string;
42
53
  title: string;
@@ -49,41 +60,23 @@ interface NewDeckReferenceStepProps {
49
60
  decks: Deck[];
50
61
  defaultDesignSystemId: string | null;
51
62
  defaultReferenceDeckId: string | null;
52
- selectedDesignSystemId: string | null;
53
- selectedReferenceDeckId: string | null;
54
- onSelectDesignSystem: (designSystemId: string | null) => void;
55
- onSelectReferenceDeck: (referenceDeckId: string | null) => void;
63
+ recentReferences: RecentReference[];
64
+ onSelect: (selection: NewDeckReferenceSelection) => void;
56
65
  onImport: (files: File[]) => Promise<void>;
57
- onContinue: () => void;
58
66
  onSkip: () => void;
59
67
  onOpenChange: (open: boolean) => void;
60
- onGoogleSlidesImported: (imported: {
61
- id: string;
62
- title: string;
63
- }) => void | Promise<void>;
64
68
  importing?: boolean;
65
69
  title: string;
66
- description: string;
67
70
  designSystemLabel: string;
68
71
  referenceDeckLabel: string;
69
- noneLabel: string;
70
- importFileLabel: string;
72
+ chooseDeckLabel: string;
71
73
  importingLabel: string;
72
- continueLabel: string;
73
74
  skipLabel: string;
74
75
  defaultSuffix: string;
75
76
  starredLabel: string;
76
77
  otherDecksLabel: string;
77
- promptNote?: string;
78
78
  searchDecksLabel: string;
79
- chooseAnotherDeckLabel: string;
80
- noMatchingDecksLabel: string;
81
- googleSlidesTitle: string;
82
- googleSlidesConnectLabel: string;
83
- googleSlidesChooseLabel: string;
84
- googleSlidesPickingLabel: string;
85
- googleSlidesConnectedLabel: string;
86
- googleSlidesUnavailableLabel: string;
79
+ promptSummary?: string;
87
80
  }
88
81
 
89
82
  export function NewDeckReferenceStep({
@@ -92,83 +85,76 @@ export function NewDeckReferenceStep({
92
85
  decks,
93
86
  defaultDesignSystemId,
94
87
  defaultReferenceDeckId,
95
- selectedDesignSystemId,
96
- selectedReferenceDeckId,
97
- onSelectDesignSystem,
98
- onSelectReferenceDeck,
88
+ recentReferences,
89
+ onSelect,
99
90
  onImport,
100
- onContinue,
101
91
  onSkip,
102
92
  onOpenChange,
103
- onGoogleSlidesImported,
104
93
  importing = false,
105
94
  title,
106
- description,
107
95
  designSystemLabel,
108
96
  referenceDeckLabel,
109
- noneLabel,
110
- importFileLabel,
97
+ chooseDeckLabel,
111
98
  importingLabel,
112
- continueLabel,
113
99
  skipLabel,
114
100
  defaultSuffix,
115
101
  starredLabel,
116
102
  otherDecksLabel,
117
- promptNote,
118
103
  searchDecksLabel,
119
- chooseAnotherDeckLabel,
120
- noMatchingDecksLabel,
121
- googleSlidesTitle,
122
- googleSlidesConnectLabel,
123
- googleSlidesChooseLabel,
124
- googleSlidesPickingLabel,
125
- googleSlidesConnectedLabel,
126
- googleSlidesUnavailableLabel,
104
+ promptSummary,
127
105
  }: NewDeckReferenceStepProps) {
128
- const [query, setQuery] = useState("");
129
- const [googleSlidesBusy, setGoogleSlidesBusy] = useState(false);
130
- const designSystemById = useMemo(
131
- () =>
132
- new Map(
133
- designSystems.map((designSystem) => [designSystem.id, designSystem]),
134
- ),
135
- [designSystems],
136
- );
137
- const deckById = useMemo(
138
- () => new Map(decks.map((deck) => [deck.id, deck])),
139
- [decks],
140
- );
106
+ const t = useT();
107
+ const [selectedDesignSystemId, setSelectedDesignSystemId] = useState<
108
+ string | null
109
+ >(defaultDesignSystemId);
110
+ const [selectedReferenceDeckId, setSelectedReferenceDeckId] = useState<
111
+ string | null
112
+ >(defaultReferenceDeckId);
113
+ const [selectedSource, setSelectedSource] =
114
+ useState<NewDeckReferenceSelection["referenceSource"]>(null);
115
+ const [referenceDeckSearchOpen, setReferenceDeckSearchOpen] = useState(false);
141
116
 
142
- const filteredDecks = useMemo(() => {
143
- const normalized = query.trim().toLowerCase();
144
- if (!normalized) {
145
- return {
146
- starred: decks.filter((deck) => deck.starred),
147
- other: decks.filter((deck) => !deck.starred),
148
- flat: [] as Deck[],
149
- };
150
- }
151
- const flat = decks.filter((deck) =>
152
- deck.title.toLowerCase().includes(normalized),
117
+ const designSystemById = new Map(
118
+ designSystems.map((designSystem) => [designSystem.id, designSystem]),
119
+ );
120
+ const deckById = new Map(decks.map((deck) => [deck.id, deck]));
121
+ const recentOptions = recentReferences
122
+ .map((reference) => {
123
+ const item =
124
+ reference.kind === "design-system"
125
+ ? designSystemById.get(reference.id)
126
+ : deckById.get(reference.id);
127
+ return item ? { reference, item } : null;
128
+ })
129
+ .filter(
130
+ (
131
+ value,
132
+ ): value is {
133
+ reference: RecentReference;
134
+ item: DesignSystemOption | Deck;
135
+ } => value !== null,
153
136
  );
154
- return { starred: [], other: [], flat };
155
- }, [decks, query]);
137
+ const visibleRecentOptions = recentOptions.filter(({ reference }) =>
138
+ reference.kind === "design-system"
139
+ ? reference.id !== selectedDesignSystemId
140
+ : reference.id !== selectedReferenceDeckId,
141
+ );
142
+ const starredDecks = decks.filter((deck) => deck.starred);
143
+ const otherDecks = decks.filter((deck) => !deck.starred);
144
+ const selectedDesignSystem = selectedDesignSystemId
145
+ ? designSystemById.get(selectedDesignSystemId)
146
+ : undefined;
147
+ const selectedReferenceDeck = selectedReferenceDeckId
148
+ ? deckById.get(selectedReferenceDeckId)
149
+ : undefined;
156
150
 
157
- const currentDesignSystem = selectedDesignSystemId
158
- ? (designSystemById.get(selectedDesignSystemId) ?? null)
159
- : null;
160
- const selectedDesignSystemLabel =
161
- currentDesignSystem?.title ?? designSystemLabel;
162
- const selectedReferenceDeckLabel =
163
- (selectedReferenceDeckId
164
- ? deckById.get(selectedReferenceDeckId)?.title
165
- : null) ?? referenceDeckLabel;
166
- const selectedDesignSystemIsDefault =
167
- !!selectedDesignSystemId &&
168
- selectedDesignSystemId === defaultDesignSystemId;
169
- const selectedReferenceDeckIsDefault =
170
- !!selectedReferenceDeckId &&
171
- selectedReferenceDeckId === defaultReferenceDeckId;
151
+ useEffect(() => {
152
+ if (!open) return;
153
+ setSelectedDesignSystemId(defaultDesignSystemId);
154
+ setSelectedReferenceDeckId(defaultReferenceDeckId);
155
+ setSelectedSource(null);
156
+ setReferenceDeckSearchOpen(false);
157
+ }, [open, defaultDesignSystemId, defaultReferenceDeckId]);
172
158
 
173
159
  const handleImport = async (event: ChangeEvent<HTMLInputElement>) => {
174
160
  const files = Array.from(event.target.files ?? []);
@@ -177,335 +163,460 @@ export function NewDeckReferenceStep({
177
163
  await onImport(files);
178
164
  };
179
165
 
180
- return (
181
- <Dialog open={open} onOpenChange={onOpenChange}>
182
- <DialogContent className="max-h-[min(88vh,880px)] overflow-hidden sm:max-w-5xl">
183
- <DialogHeader>
184
- <DialogTitle>{title}</DialogTitle>
185
- <DialogDescription>{description}</DialogDescription>
186
- </DialogHeader>
166
+ const handleContinue = () => {
167
+ onSelect({
168
+ designSystemId: selectedDesignSystemId,
169
+ referenceDeckId: selectedReferenceDeckId,
170
+ referenceSource:
171
+ selectedSource && selectedSource.value.trim()
172
+ ? { ...selectedSource, value: selectedSource.value.trim() }
173
+ : null,
174
+ });
175
+ };
187
176
 
188
- <div className="space-y-2">
189
- <p className="text-sm text-muted-foreground">{promptNote ?? ""}</p>
190
- </div>
177
+ const chooseSource = (
178
+ kind: NonNullable<NewDeckReferenceSelection["referenceSource"]>["kind"],
179
+ ) => {
180
+ setSelectedSource({ kind, value: "" });
181
+ if (kind === "website" || kind === "figma") {
182
+ setSelectedDesignSystemId(null);
183
+ } else {
184
+ setSelectedReferenceDeckId(null);
185
+ }
186
+ };
187
+ const selectedSourceLabel =
188
+ selectedSource?.kind === "google-docs"
189
+ ? "Google Docs"
190
+ : selectedSource?.kind === "website"
191
+ ? "Website"
192
+ : "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
+ );
191
201
 
192
- <div className="grid gap-4 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.35fr)]">
193
- <div className="grid gap-4">
194
- <Card className="border-border/70">
195
- <CardHeader className="space-y-1 pb-3">
196
- <CardTitle className="text-sm">{designSystemLabel}</CardTitle>
197
- <p className="text-xs text-muted-foreground">
198
- {defaultDesignSystemId
199
- ? `${designSystemLabel}${defaultSuffix}`
200
- : noneLabel}
201
- </p>
202
- </CardHeader>
203
- <CardContent className="space-y-3">
204
- <Select
205
- value={selectedDesignSystemId ?? "none"}
206
- onValueChange={(value) =>
207
- onSelectDesignSystem(value === "none" ? null : value)
208
- }
209
- >
210
- <SelectTrigger className="w-full">
211
- <SelectValue placeholder={designSystemLabel} />
212
- </SelectTrigger>
213
- <SelectContent>
214
- <SelectGroup>
215
- <SelectLabel>{designSystemLabel}</SelectLabel>
216
- <SelectItem value="none">{noneLabel}</SelectItem>
217
- {designSystems.map((designSystem) => (
218
- <SelectItem
219
- key={designSystem.id}
220
- value={designSystem.id}
221
- >
222
- {designSystem.title}
223
- {designSystem.isDefault ? defaultSuffix : ""}
224
- </SelectItem>
225
- ))}
226
- </SelectGroup>
227
- </SelectContent>
228
- </Select>
202
+ return open ? (
203
+ <div
204
+ className="fixed inset-0 z-[200] flex min-h-screen flex-col bg-background text-foreground"
205
+ role="dialog"
206
+ aria-modal="true"
207
+ aria-label={title}
208
+ >
209
+ <header className="flex h-14 shrink-0 items-center border-b border-border px-5 sm:px-8">
210
+ <button
211
+ type="button"
212
+ onClick={() => onOpenChange(false)}
213
+ className="inline-flex items-center gap-2 rounded-md px-1.5 py-1 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
214
+ >
215
+ <IconArrowLeft className="size-4" />
216
+ <span>{title}</span>
217
+ </button>
218
+ </header>
229
219
 
230
- <SelectionSummary
220
+ <main className="flex min-h-0 flex-1 justify-center overflow-y-auto px-5 py-10 sm:px-8 sm:py-14">
221
+ <div className="w-full max-w-2xl">
222
+ <h1 className="text-2xl font-semibold tracking-[-0.025em] text-foreground sm:text-3xl">
223
+ {t("home.chooseReferences")}
224
+ </h1>
225
+ {promptSummary?.trim() && (
226
+ <p className="mt-2 max-w-xl truncate text-sm text-muted-foreground">
227
+ “{promptSummary.trim()}”
228
+ </p>
229
+ )}
230
+
231
+ {(selectedDesignSystem || selectedSource) && (
232
+ <div className="mt-8 space-y-2">
233
+ {selectedDesignSystem && (
234
+ <ReferenceRow
231
235
  icon={<IconPalette className="size-4" />}
232
- label={
233
- selectedDesignSystemId
234
- ? selectedDesignSystemLabel
235
- : noneLabel
236
- }
237
- meta={
238
- selectedDesignSystemIsDefault
239
- ? `${designSystemLabel}${defaultSuffix}`
240
- : selectedDesignSystemId
241
- ? designSystemLabel
242
- : noneLabel
243
- }
244
- onClear={
245
- selectedDesignSystemId
246
- ? () => onSelectDesignSystem(null)
247
- : undefined
248
- }
236
+ label={selectedDesignSystem.title}
237
+ meta={`${designSystemLabel}${defaultDesignSystemId === selectedDesignSystem.id ? defaultSuffix : ""}`}
238
+ selected
239
+ onClick={() => setSelectedDesignSystemId(null)}
249
240
  />
250
- </CardContent>
251
- </Card>
252
-
253
- <Card className="border-border/70">
254
- <CardHeader className="space-y-1 pb-3">
255
- <CardTitle className="text-sm">{googleSlidesTitle}</CardTitle>
256
- <p className="text-xs text-muted-foreground">
257
- {googleSlidesConnectedLabel}
258
- </p>
259
- </CardHeader>
260
- <CardContent className="space-y-3">
261
- <GoogleSlidesReferenceImport
262
- onImported={onGoogleSlidesImported}
263
- onBusyChange={setGoogleSlidesBusy}
264
- title={googleSlidesTitle}
265
- chooseLabel={googleSlidesChooseLabel}
266
- connectLabel={googleSlidesConnectLabel}
267
- pickingLabel={googleSlidesPickingLabel}
268
- connectedLabel={googleSlidesConnectedLabel}
269
- unavailableLabel={googleSlidesUnavailableLabel}
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)}
270
249
  />
250
+ )}
251
+ </div>
252
+ )}
271
253
 
272
- <label
273
- className={cn(
274
- "inline-flex cursor-pointer items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-accent",
275
- importing && "pointer-events-none opacity-60",
276
- )}
277
- >
278
- <IconUpload className="size-4" />
279
- {importing ? importingLabel : importFileLabel}
280
- <input
281
- type="file"
282
- className="sr-only"
283
- accept=".pptx,.pdf,.docx"
284
- multiple
285
- disabled={importing}
286
- onChange={(event) => void handleImport(event)}
287
- />
288
- </label>
289
- </CardContent>
290
- </Card>
291
- </div>
292
-
293
- <Card className="min-h-0 border-border/70">
294
- <CardHeader className="space-y-1 pb-3">
295
- <CardTitle className="text-sm">
296
- {chooseAnotherDeckLabel}
297
- </CardTitle>
298
- <p className="text-xs text-muted-foreground">
299
- {searchDecksLabel}
300
- </p>
301
- </CardHeader>
302
- <CardContent className="min-h-0 space-y-3">
303
- <div className="relative">
304
- <IconSearch className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
305
- <Input
306
- value={query}
307
- onChange={(event) => setQuery(event.target.value)}
308
- aria-label={searchDecksLabel}
309
- placeholder={searchDecksLabel}
310
- className="ps-9"
311
- />
254
+ <div className="mt-10 space-y-6">
255
+ <div className="grid gap-2">
256
+ <div className="flex items-center justify-between gap-3">
257
+ <span className="text-xs font-medium text-muted-foreground">
258
+ {designSystemLabel}
259
+ </span>
260
+ {designSystems.length === 0 && (
261
+ <a
262
+ href="/design-systems"
263
+ target="_blank"
264
+ rel="noreferrer"
265
+ className="text-xs font-medium text-primary underline-offset-4 transition-colors hover:underline"
266
+ >
267
+ {t("home.addDesignSystem")}
268
+ </a>
269
+ )}
312
270
  </div>
271
+ <Select
272
+ value={selectedDesignSystemId ?? undefined}
273
+ onValueChange={(value) => {
274
+ setSelectedDesignSystemId(value);
275
+ setSelectedSource(null);
276
+ }}
277
+ >
278
+ <SelectTrigger
279
+ className="w-full"
280
+ disabled={designSystems.length === 0}
281
+ >
282
+ <SelectValue placeholder={designSystemLabel} />
283
+ </SelectTrigger>
284
+ <SelectContent>
285
+ {designSystems.map((designSystem) => (
286
+ <SelectItem key={designSystem.id} value={designSystem.id}>
287
+ {designSystem.title}
288
+ </SelectItem>
289
+ ))}
290
+ </SelectContent>
291
+ </Select>
292
+ </div>
313
293
 
314
- <ScrollArea className="max-h-[430px] pr-3">
315
- <div className="space-y-4">
316
- {query.trim() ? (
317
- filteredDecks.flat.length > 0 ? (
318
- <div className="space-y-2">
319
- {filteredDecks.flat.map((deck) => (
320
- <DeckRow
321
- key={deck.id}
322
- deck={deck}
323
- selected={deck.id === selectedReferenceDeckId}
324
- onSelect={() => onSelectReferenceDeck(deck.id)}
325
- starredLabel={starredLabel}
326
- />
327
- ))}
328
- </div>
329
- ) : (
330
- <div className="rounded-lg border border-dashed border-border/70 px-4 py-6 text-sm text-muted-foreground">
331
- {noMatchingDecksLabel}
332
- </div>
333
- )
334
- ) : (
335
- <>
336
- {filteredDecks.starred.length > 0 && (
337
- <div className="space-y-2">
338
- <div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
339
- {starredLabel}
340
- </div>
341
- {filteredDecks.starred.map((deck) => (
342
- <DeckRow
294
+ <div className="grid gap-2">
295
+ <span className="text-xs font-medium text-muted-foreground">
296
+ {referenceDeckLabel}
297
+ </span>
298
+ <Popover
299
+ open={referenceDeckSearchOpen}
300
+ onOpenChange={setReferenceDeckSearchOpen}
301
+ >
302
+ <PopoverTrigger asChild>
303
+ <button
304
+ type="button"
305
+ role="combobox"
306
+ aria-expanded={referenceDeckSearchOpen}
307
+ aria-label={referenceDeckLabel}
308
+ disabled={decks.length === 0}
309
+ 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
+ >
311
+ <span className="truncate">
312
+ {selectedReferenceDeck?.title ?? chooseDeckLabel}
313
+ </span>
314
+ <IconChevronDown className="size-4 shrink-0 opacity-50" />
315
+ </button>
316
+ </PopoverTrigger>
317
+ <PopoverContent
318
+ align="start"
319
+ className="w-[--radix-popover-trigger-width] p-0"
320
+ >
321
+ <Command
322
+ filter={(value, search) =>
323
+ value.toLowerCase().includes(search.toLowerCase().trim())
324
+ ? 1
325
+ : 0
326
+ }
327
+ >
328
+ <CommandInput placeholder={searchDecksLabel} />
329
+ <CommandList className="max-h-72">
330
+ <CommandEmpty>{t("home.noMatchingDecks")}</CommandEmpty>
331
+ {starredDecks.length > 0 && (
332
+ <CommandGroup heading={starredLabel}>
333
+ {starredDecks.map((deck) => (
334
+ <CommandItem
343
335
  key={deck.id}
344
- deck={deck}
345
- selected={deck.id === selectedReferenceDeckId}
346
- onSelect={() => onSelectReferenceDeck(deck.id)}
347
- starredLabel={starredLabel}
348
- />
336
+ value={`${deck.title} ${deck.id}`}
337
+ onSelect={() => {
338
+ setSelectedReferenceDeckId(deck.id);
339
+ setSelectedSource(null);
340
+ setReferenceDeckSearchOpen(false);
341
+ }}
342
+ >
343
+ <IconCheck
344
+ className={cn(
345
+ "me-2 size-4",
346
+ selectedReferenceDeckId === deck.id
347
+ ? "opacity-100"
348
+ : "opacity-0",
349
+ )}
350
+ />
351
+ <span className="truncate">{deck.title}</span>
352
+ </CommandItem>
349
353
  ))}
350
- </div>
354
+ </CommandGroup>
351
355
  )}
352
- {filteredDecks.other.length > 0 && (
353
- <div className="space-y-2">
354
- {filteredDecks.starred.length > 0 && (
355
- <Separator className="my-1" />
356
- )}
357
- <div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
358
- {otherDecksLabel}
359
- </div>
360
- {filteredDecks.other.map((deck) => (
361
- <DeckRow
356
+ {otherDecks.length > 0 && (
357
+ <CommandGroup heading={otherDecksLabel}>
358
+ {otherDecks.map((deck) => (
359
+ <CommandItem
362
360
  key={deck.id}
363
- deck={deck}
364
- selected={deck.id === selectedReferenceDeckId}
365
- onSelect={() => onSelectReferenceDeck(deck.id)}
366
- starredLabel={starredLabel}
367
- />
361
+ value={`${deck.title} ${deck.id}`}
362
+ onSelect={() => {
363
+ setSelectedReferenceDeckId(deck.id);
364
+ setSelectedSource(null);
365
+ setReferenceDeckSearchOpen(false);
366
+ }}
367
+ >
368
+ <IconCheck
369
+ className={cn(
370
+ "me-2 size-4",
371
+ selectedReferenceDeckId === deck.id
372
+ ? "opacity-100"
373
+ : "opacity-0",
374
+ )}
375
+ />
376
+ <span className="truncate">{deck.title}</span>
377
+ </CommandItem>
368
378
  ))}
369
- </div>
379
+ </CommandGroup>
370
380
  )}
371
- </>
372
- )}
373
- </div>
374
- </ScrollArea>
381
+ </CommandList>
382
+ </Command>
383
+ </PopoverContent>
384
+ </Popover>
385
+ </div>
375
386
 
376
- <Separator />
377
- <SelectionSummary
378
- icon={<IconPresentation className="size-4" />}
379
- label={
380
- selectedReferenceDeckId
381
- ? selectedReferenceDeckLabel
382
- : noneLabel
383
- }
384
- meta={
385
- selectedReferenceDeckIsDefault
386
- ? `${referenceDeckLabel}${defaultSuffix}`
387
- : selectedReferenceDeckId
388
- ? referenceDeckLabel
389
- : noneLabel
390
- }
391
- onClear={
392
- selectedReferenceDeckId
393
- ? () => onSelectReferenceDeck(null)
394
- : undefined
395
- }
396
- />
397
- </CardContent>
398
- </Card>
399
- </div>
387
+ {visibleRecentOptions.length > 0 && (
388
+ <div className="border-t border-border pt-5">
389
+ <span className="text-xs font-medium text-muted-foreground">
390
+ Recent
391
+ </span>
392
+ <div className="mt-2 space-y-2">
393
+ {visibleRecentOptions.map(({ reference, item }) => (
394
+ <ReferenceRow
395
+ key={`${reference.kind}:${reference.id}`}
396
+ compact
397
+ icon={
398
+ reference.kind === "design-system" ? (
399
+ <IconPalette className="size-4" />
400
+ ) : (
401
+ <IconPresentation className="size-4" />
402
+ )
403
+ }
404
+ label={item.title}
405
+ meta={
406
+ reference.kind === "design-system"
407
+ ? designSystemLabel
408
+ : referenceDeckLabel
409
+ }
410
+ onClick={() => {
411
+ if (reference.kind === "design-system") {
412
+ setSelectedDesignSystemId(reference.id);
413
+ } else {
414
+ setSelectedReferenceDeckId(reference.id);
415
+ }
416
+ setSelectedSource(null);
417
+ }}
418
+ />
419
+ ))}
420
+ </div>
421
+ </div>
422
+ )}
400
423
 
401
- <div className="flex flex-col-reverse items-stretch justify-between gap-2 border-t border-border pt-3 sm:flex-row sm:items-center">
402
- <Button
403
- type="button"
404
- variant="ghost"
405
- onClick={onSkip}
406
- disabled={importing || googleSlidesBusy}
407
- >
408
- {skipLabel}
409
- </Button>
410
- <div className="flex items-center gap-2">
411
- <Button
412
- type="button"
413
- variant="outline"
414
- onClick={onContinue}
415
- disabled={importing || googleSlidesBusy}
416
- >
417
- {continueLabel}
418
- </Button>
419
- <IconArrowRight className="size-4 text-muted-foreground" />
424
+ <div className="border-t border-border pt-5">
425
+ <span className="text-xs font-medium text-muted-foreground">
426
+ {t("home.importFrom")}
427
+ </span>
428
+ <div className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-5">
429
+ <FileImportOption
430
+ accept=".pptx"
431
+ icon={<IconPresentation className="size-4" />}
432
+ label="PPT"
433
+ importing={importing}
434
+ importingLabel={importingLabel}
435
+ onChange={handleImport}
436
+ />
437
+ <FileImportOption
438
+ accept=".pdf"
439
+ icon={<IconFileTypePdf className="size-4" />}
440
+ label="PDF"
441
+ importing={importing}
442
+ importingLabel={importingLabel}
443
+ onChange={handleImport}
444
+ />
445
+ <FileImportOption
446
+ accept=".docx"
447
+ icon={<IconFileText className="size-4" />}
448
+ label="DOCX"
449
+ importing={importing}
450
+ importingLabel={importingLabel}
451
+ onChange={handleImport}
452
+ />
453
+ <ImportOption
454
+ icon={<IconBrandGoogle className="size-4" />}
455
+ label="GDocs"
456
+ selected={selectedSource?.kind === "google-docs"}
457
+ onClick={() => chooseSource("google-docs")}
458
+ />
459
+ <ImportOption
460
+ icon={<IconWorld className="size-4" />}
461
+ label="Website"
462
+ selected={selectedSource?.kind === "website"}
463
+ onClick={() => chooseSource("website")}
464
+ />
465
+ <ImportOption
466
+ icon={<IconBrandFigma className="size-4" />}
467
+ label="Figma"
468
+ selected={selectedSource?.kind === "figma"}
469
+ onClick={() => chooseSource("figma")}
470
+ />
471
+ </div>
472
+ {selectedSource && (
473
+ <Input
474
+ autoFocus
475
+ className="mt-3"
476
+ value={selectedSource.value}
477
+ placeholder={`Paste a ${selectedSourceLabel} link`}
478
+ aria-label={`${selectedSourceLabel} link`}
479
+ onChange={(event) =>
480
+ setSelectedSource({
481
+ ...selectedSource,
482
+ value: event.target.value,
483
+ })
484
+ }
485
+ />
486
+ )}
487
+ </div>
420
488
  </div>
421
489
  </div>
422
- </DialogContent>
423
- </Dialog>
490
+ </main>
491
+
492
+ <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}>
494
+ {skipLabel}
495
+ </Button>
496
+ <Button
497
+ type="button"
498
+ onClick={handleContinue}
499
+ disabled={
500
+ importing || Boolean(selectedSource && !selectedSource.value.trim())
501
+ }
502
+ >
503
+ Continue
504
+ <IconCheck className="ms-1.5 size-4" />
505
+ </Button>
506
+ </footer>
507
+ </div>
508
+ ) : null;
509
+ }
510
+
511
+ function FileImportOption({
512
+ accept,
513
+ icon,
514
+ label,
515
+ importing,
516
+ importingLabel,
517
+ onChange,
518
+ }: {
519
+ accept: string;
520
+ icon: ReactNode;
521
+ label: string;
522
+ importing: boolean;
523
+ importingLabel: string;
524
+ onChange: (event: ChangeEvent<HTMLInputElement>) => void;
525
+ }) {
526
+ return (
527
+ <label
528
+ className={cn(
529
+ "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
+ importing && "pointer-events-none opacity-60",
531
+ )}
532
+ >
533
+ {icon}
534
+ <span>{importing ? importingLabel : label}</span>
535
+ <input
536
+ type="file"
537
+ className="sr-only"
538
+ accept={accept}
539
+ multiple
540
+ disabled={importing}
541
+ onChange={onChange}
542
+ />
543
+ </label>
424
544
  );
425
545
  }
426
546
 
427
- function SelectionSummary({
547
+ function ImportOption({
428
548
  icon,
429
549
  label,
430
- meta,
431
- onClear,
550
+ selected = false,
551
+ onClick,
432
552
  }: {
433
553
  icon: ReactNode;
434
554
  label: string;
435
- meta: string;
436
- onClear?: () => void;
555
+ selected?: boolean;
556
+ onClick: () => void;
437
557
  }) {
438
558
  return (
439
- <div className="flex min-w-0 items-center gap-3 rounded-lg border border-border/70 bg-background px-3 py-2">
440
- <span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-accent text-muted-foreground">
441
- {icon}
442
- </span>
443
- <span className="min-w-0 flex-1">
444
- <span className="block truncate text-sm font-medium text-foreground">
445
- {label}
446
- </span>
447
- <span className="block truncate text-xs text-muted-foreground">
448
- {meta}
449
- </span>
450
- </span>
451
- {onClear && (
452
- <Button
453
- type="button"
454
- variant="ghost"
455
- size="icon"
456
- className="size-8 shrink-0"
457
- onClick={onClear}
458
- aria-label={`Remove ${label}`}
459
- >
460
- <IconX className="size-4" />
461
- </Button>
559
+ <button
560
+ type="button"
561
+ onClick={onClick}
562
+ className={cn(
563
+ "flex items-center justify-center gap-2 rounded-md border px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
564
+ selected
565
+ ? "border-primary/50 bg-primary/5 text-primary"
566
+ : "border-border hover:bg-accent",
462
567
  )}
463
- </div>
568
+ aria-pressed={selected}
569
+ >
570
+ {icon}
571
+ <span>{label}</span>
572
+ </button>
464
573
  );
465
574
  }
466
575
 
467
- function DeckRow({
468
- deck,
469
- selected,
470
- onSelect,
471
- starredLabel,
576
+ function ReferenceRow({
577
+ icon,
578
+ label,
579
+ meta,
580
+ selected = false,
581
+ compact = false,
582
+ onClick,
472
583
  }: {
473
- deck: Deck;
474
- selected: boolean;
475
- onSelect: () => void;
476
- starredLabel: string;
584
+ icon: ReactNode;
585
+ label: string;
586
+ meta: string;
587
+ selected?: boolean;
588
+ compact?: boolean;
589
+ onClick: () => void;
477
590
  }) {
478
591
  return (
479
592
  <button
480
593
  type="button"
481
- onClick={onSelect}
594
+ onClick={onClick}
482
595
  className={cn(
483
- "flex w-full items-center gap-3 rounded-lg border px-3 py-3 text-start transition-colors",
596
+ "flex w-full min-w-0 items-center gap-3 rounded-lg border px-4 text-start transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
597
+ compact ? "py-2.5" : "py-3.5",
484
598
  selected
485
599
  ? "border-primary/50 bg-primary/5"
486
- : "border-border/70 bg-background hover:border-foreground/20 hover:bg-accent/35",
600
+ : "border-border bg-card/40 hover:border-foreground/30 hover:bg-accent/40",
487
601
  )}
488
602
  >
489
- <span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-accent text-muted-foreground">
490
- {deck.starred ? (
491
- <IconStar className="size-4" />
492
- ) : (
493
- <IconPresentation className="size-4" />
603
+ <span
604
+ className={cn(
605
+ "flex size-8 shrink-0 items-center justify-center rounded-md",
606
+ selected
607
+ ? "bg-primary/10 text-primary"
608
+ : "bg-accent text-muted-foreground",
494
609
  )}
610
+ >
611
+ {icon}
495
612
  </span>
496
613
  <span className="min-w-0 flex-1">
497
- <span className="block truncate text-sm font-medium text-foreground">
498
- {deck.title}
499
- </span>
614
+ <span className="block truncate text-sm font-medium">{label}</span>
500
615
  <span className="block truncate text-xs text-muted-foreground">
501
- {deck.starred ? starredLabel : ""}
616
+ {meta}
502
617
  </span>
503
618
  </span>
504
- {selected ? (
505
- <IconCheck className="size-4 shrink-0 text-primary" />
506
- ) : (
507
- <IconArrowRight className="size-4 shrink-0 text-muted-foreground" />
508
- )}
619
+ {selected && <IconCheck className="size-4 shrink-0 text-primary" />}
509
620
  </button>
510
621
  );
511
622
  }