@realtimex/email-automator 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/api/server.ts +4 -8
  2. package/api/src/config/index.ts +6 -3
  3. package/bin/email-automator-setup.js +2 -3
  4. package/bin/email-automator.js +7 -11
  5. package/dist/api/server.js +109 -0
  6. package/dist/api/src/config/index.js +88 -0
  7. package/dist/api/src/middleware/auth.js +119 -0
  8. package/dist/api/src/middleware/errorHandler.js +78 -0
  9. package/dist/api/src/middleware/index.js +4 -0
  10. package/dist/api/src/middleware/rateLimit.js +57 -0
  11. package/dist/api/src/middleware/validation.js +111 -0
  12. package/dist/api/src/routes/actions.js +173 -0
  13. package/dist/api/src/routes/auth.js +106 -0
  14. package/dist/api/src/routes/emails.js +100 -0
  15. package/dist/api/src/routes/health.js +33 -0
  16. package/dist/api/src/routes/index.js +19 -0
  17. package/dist/api/src/routes/migrate.js +61 -0
  18. package/dist/api/src/routes/rules.js +104 -0
  19. package/dist/api/src/routes/settings.js +178 -0
  20. package/dist/api/src/routes/sync.js +118 -0
  21. package/dist/api/src/services/eventLogger.js +41 -0
  22. package/dist/api/src/services/gmail.js +350 -0
  23. package/dist/api/src/services/intelligence.js +243 -0
  24. package/dist/api/src/services/microsoft.js +256 -0
  25. package/dist/api/src/services/processor.js +503 -0
  26. package/dist/api/src/services/scheduler.js +210 -0
  27. package/dist/api/src/services/supabase.js +59 -0
  28. package/dist/api/src/utils/contentCleaner.js +94 -0
  29. package/dist/api/src/utils/crypto.js +68 -0
  30. package/dist/api/src/utils/logger.js +119 -0
  31. package/package.json +5 -5
  32. package/src/App.tsx +0 -622
  33. package/src/components/AccountSettings.tsx +0 -310
  34. package/src/components/AccountSettingsPage.tsx +0 -390
  35. package/src/components/Configuration.tsx +0 -1345
  36. package/src/components/Dashboard.tsx +0 -940
  37. package/src/components/ErrorBoundary.tsx +0 -71
  38. package/src/components/LiveTerminal.tsx +0 -308
  39. package/src/components/LoadingSpinner.tsx +0 -39
  40. package/src/components/Login.tsx +0 -371
  41. package/src/components/Logo.tsx +0 -57
  42. package/src/components/SetupWizard.tsx +0 -388
  43. package/src/components/Toast.tsx +0 -109
  44. package/src/components/migration/MigrationBanner.tsx +0 -97
  45. package/src/components/migration/MigrationModal.tsx +0 -458
  46. package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
  47. package/src/components/mode-toggle.tsx +0 -24
  48. package/src/components/theme-provider.tsx +0 -72
  49. package/src/components/ui/alert.tsx +0 -66
  50. package/src/components/ui/button.tsx +0 -57
  51. package/src/components/ui/card.tsx +0 -75
  52. package/src/components/ui/dialog.tsx +0 -133
  53. package/src/components/ui/input.tsx +0 -22
  54. package/src/components/ui/label.tsx +0 -24
  55. package/src/components/ui/otp-input.tsx +0 -184
  56. package/src/context/AppContext.tsx +0 -422
  57. package/src/context/MigrationContext.tsx +0 -53
  58. package/src/context/TerminalContext.tsx +0 -31
  59. package/src/core/actions.ts +0 -76
  60. package/src/core/auth.ts +0 -108
  61. package/src/core/intelligence.ts +0 -76
  62. package/src/core/processor.ts +0 -112
  63. package/src/hooks/useRealtimeEmails.ts +0 -111
  64. package/src/index.css +0 -140
  65. package/src/lib/api-config.ts +0 -42
  66. package/src/lib/api-old.ts +0 -228
  67. package/src/lib/api.ts +0 -421
  68. package/src/lib/migration-check.ts +0 -264
  69. package/src/lib/sounds.ts +0 -120
  70. package/src/lib/supabase-config.ts +0 -117
  71. package/src/lib/supabase.ts +0 -28
  72. package/src/lib/types.ts +0 -166
  73. package/src/lib/utils.ts +0 -6
  74. package/src/main.tsx +0 -10
@@ -1,458 +0,0 @@
1
- /**
2
- * MigrationModal Component
3
- *
4
- * Displays detailed migration instructions in a modal dialog.
5
- * Shows step-by-step guide for users to run the migration command.
6
- */
7
-
8
- import { useMemo, useState, useEffect, useRef } from "react";
9
- import {
10
- AlertTriangle,
11
- Copy,
12
- Check,
13
- ExternalLink,
14
- Info,
15
- Loader2,
16
- Terminal,
17
- } from "lucide-react";
18
- import {
19
- Dialog,
20
- DialogContent,
21
- DialogDescription,
22
- DialogFooter,
23
- DialogHeader,
24
- DialogTitle,
25
- } from "../ui/dialog";
26
- import { Button } from "../ui/button";
27
- import { Alert, AlertDescription } from "../ui/alert";
28
- import { Input } from "../ui/input";
29
- import { Label } from "../ui/label";
30
- import { toast } from "../Toast";
31
- import { getSupabaseConfig } from "../../lib/supabase-config";
32
- import type { MigrationStatus } from "../../lib/migration-check";
33
-
34
- interface MigrationModalProps {
35
- /** Whether the modal is open */
36
- open: boolean;
37
- /** Callback when modal is closed */
38
- onOpenChange: (open: boolean) => void;
39
- /** Migration status */
40
- status: MigrationStatus;
41
- }
42
-
43
- interface CodeBlockProps {
44
- code: string;
45
- label?: string;
46
- }
47
-
48
- function CodeBlock({ code, label }: CodeBlockProps) {
49
- const [copied, setCopied] = useState(false);
50
-
51
- const canCopy =
52
- typeof navigator !== "undefined" && !!navigator.clipboard?.writeText;
53
-
54
- const handleCopy = async () => {
55
- if (!canCopy) {
56
- toast.error("Clipboard not supported");
57
- return;
58
- }
59
-
60
- try {
61
- await navigator.clipboard.writeText(code);
62
- setCopied(true);
63
- window.setTimeout(() => setCopied(false), 2000);
64
- toast.success("Copied to clipboard");
65
- } catch (error) {
66
- console.error("Failed to copy:", error);
67
- toast.error("Failed to copy to clipboard");
68
- }
69
- };
70
-
71
- return (
72
- <div className="relative">
73
- {label && (
74
- <div className="mb-2 text-sm font-medium text-muted-foreground">
75
- {label}
76
- </div>
77
- )}
78
- <div className="group relative">
79
- <pre className="overflow-hidden rounded-md bg-muted p-3 pr-12 text-sm">
80
- <code className="block whitespace-pre-wrap break-all">{code}</code>
81
- </pre>
82
- <Button
83
- type="button"
84
- size="icon"
85
- variant="ghost"
86
- className="absolute right-2 top-2 h-8 w-8"
87
- onClick={handleCopy}
88
- disabled={!canCopy}
89
- >
90
- {copied ? (
91
- <Check className="h-4 w-4 text-green-600" />
92
- ) : (
93
- <Copy className="h-4 w-4" />
94
- )}
95
- <span className="sr-only">
96
- {copied ? "Copied" : "Copy code"}
97
- </span>
98
- </Button>
99
- </div>
100
- </div>
101
- );
102
- }
103
-
104
- export function MigrationModal({
105
- open,
106
- onOpenChange,
107
- status,
108
- }: MigrationModalProps) {
109
- const config = getSupabaseConfig();
110
-
111
- // Auto-migration state
112
- const [showAutoMigrate, setShowAutoMigrate] = useState(true);
113
- const [isMigrating, setIsMigrating] = useState(false);
114
- const [migrationLogs, setMigrationLogs] = useState<string[]>([]);
115
- const [dbPassword, setDbPassword] = useState("");
116
- const [accessToken, setAccessToken] = useState("");
117
- const logsEndRef = useRef<HTMLDivElement>(null);
118
-
119
- const projectId = useMemo(() => {
120
- const url = config?.url;
121
- if (!url) return "";
122
- try {
123
- const host = new URL(url).hostname;
124
- return host.split(".")[0] || "";
125
- } catch {
126
- return "";
127
- }
128
- }, [config?.url]);
129
-
130
- // Scroll logs to bottom
131
- useEffect(() => {
132
- if (logsEndRef.current) {
133
- logsEndRef.current.scrollIntoView({ behavior: "smooth" });
134
- }
135
- }, [migrationLogs]);
136
-
137
- const handleAutoMigrate = async () => {
138
- if (!projectId) {
139
- toast.error("Missing Project ID");
140
- return;
141
- }
142
-
143
- setIsMigrating(true);
144
- setMigrationLogs(["Initializing migration..."]);
145
-
146
- try {
147
- const response = await fetch("/api/migrate", {
148
- method: "POST",
149
- headers: { "Content-Type": "application/json" },
150
- body: JSON.stringify({
151
- projectRef: projectId,
152
- dbPassword,
153
- accessToken,
154
- }),
155
- });
156
-
157
- if (!response.ok) {
158
- throw new Error(
159
- `Server returned ${response.status}: ${response.statusText}`,
160
- );
161
- }
162
-
163
- const reader = response.body?.getReader();
164
- if (!reader) throw new Error("No response stream received.");
165
-
166
- const decoder = new TextDecoder();
167
-
168
- while (true) {
169
- const { done, value } = await reader.read();
170
- if (done) break;
171
-
172
- const text = decoder.decode(value);
173
- const lines = text.split("\n").filter(Boolean);
174
- setMigrationLogs((prev) => [...prev, ...lines]);
175
- }
176
- } catch (err) {
177
- console.error(err);
178
- setMigrationLogs((prev) => [
179
- ...prev,
180
- `Error: ${err instanceof Error ? err.message : String(err)}`,
181
- ]);
182
- toast.error("Migration failed. Check logs for details.");
183
- } finally {
184
- setIsMigrating(false);
185
- }
186
- };
187
-
188
- return (
189
- <Dialog
190
- open={open}
191
- onOpenChange={(val) => !isMigrating && onOpenChange(val)}
192
- >
193
- <DialogContent className="max-h-[90vh] sm:max-w-5xl overflow-y-auto">
194
- <DialogHeader>
195
- <DialogTitle className="flex items-center gap-2 text-xl">
196
- <AlertTriangle className="h-6 w-6 text-red-700 dark:text-red-600" />
197
- Database Migration Required
198
- </DialogTitle>
199
- <DialogDescription>
200
- Your application version ({status.appVersion}) requires a database schema update to function correctly.
201
- </DialogDescription>
202
- </DialogHeader>
203
-
204
- <div className="space-y-6">
205
- {/* Overview Alert */}
206
- <Alert>
207
- <Info className="h-4 w-4" />
208
- <AlertDescription>
209
- <strong>Why is this needed?</strong>
210
- <ul className="mt-2 list-inside list-disc space-y-1 text-sm">
211
- <li>
212
- Updates your database schema to match version {status.appVersion}
213
- </li>
214
- <li>
215
- Enables new features and performance improvements
216
- </li>
217
- <li>
218
- Your existing data will be preserved (safe migration)
219
- </li>
220
- </ul>
221
- </AlertDescription>
222
- </Alert>
223
-
224
- {/* Mode Selection Tabs */}
225
- <div className="flex border-b">
226
- <button
227
- className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${showAutoMigrate ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"}`}
228
- onClick={() => setShowAutoMigrate(true)}
229
- >
230
- Automatic (Recommended)
231
- </button>
232
- <button
233
- className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${!showAutoMigrate ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"}`}
234
- onClick={() => setShowAutoMigrate(false)}
235
- >
236
- Manual CLI
237
- </button>
238
- </div>
239
-
240
- {showAutoMigrate ? (
241
- <div className="space-y-4 py-2">
242
- <div className="rounded-lg border bg-card text-card-foreground shadow-sm p-6">
243
- <h3 className="text-lg font-semibold mb-2">
244
- Automated Migration
245
- </h3>
246
- <p className="text-sm text-muted-foreground mb-4">
247
- Run the migration directly from your browser. Requires your database password.
248
- </p>
249
-
250
- <div className="grid gap-4">
251
- <div className="grid gap-2">
252
- <Label htmlFor="project-id">
253
- Supabase Project ID
254
- </Label>
255
- <Input
256
- id="project-id"
257
- value={projectId}
258
- disabled
259
- readOnly
260
- className="bg-muted"
261
- />
262
- </div>
263
-
264
- <div className="grid gap-2">
265
- <div className="flex justify-between items-center">
266
- <Label htmlFor="access-token">
267
- Access Token (Optional)
268
- </Label>
269
- <a
270
- href="https://supabase.com/dashboard/account/tokens"
271
- target="_blank"
272
- rel="noopener noreferrer"
273
- className="text-xs text-primary hover:underline flex items-center gap-1"
274
- >
275
- Generate Token <ExternalLink className="h-3 w-3" />
276
- </a>
277
- </div>
278
- <Input
279
- id="access-token"
280
- type="password"
281
- placeholder="sbp_..."
282
- value={accessToken}
283
- onChange={(e) => setAccessToken(e.target.value)}
284
- disabled={isMigrating}
285
- />
286
- <p className="text-xs text-muted-foreground">
287
- Recommended for more reliable authentication.
288
- </p>
289
- </div>
290
-
291
- <div className="grid gap-2">
292
- <Label htmlFor="db-password">
293
- Database Password
294
- </Label>
295
- <Input
296
- id="db-password"
297
- type="password"
298
- placeholder="Your database password"
299
- value={dbPassword}
300
- onChange={(e) => setDbPassword(e.target.value)}
301
- disabled={isMigrating}
302
- />
303
- <p className="text-xs text-muted-foreground">
304
- Required if no access token is provided.
305
- </p>
306
- </div>
307
-
308
- <Button
309
- onClick={handleAutoMigrate}
310
- disabled={isMigrating}
311
- className="w-full"
312
- >
313
- {isMigrating ? (
314
- <>
315
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
316
- Migrating...
317
- </>
318
- ) : (
319
- <>
320
- <Terminal className="mr-2 h-4 w-4" />
321
- Start Migration
322
- </>
323
- )}
324
- </Button>
325
- </div>
326
- </div>
327
-
328
- {/* Logs Terminal */}
329
- <div className="rounded-lg border bg-black text-white font-mono text-xs p-4 h-64 overflow-y-auto">
330
- {migrationLogs.length === 0 ? (
331
- <div className="text-gray-500 italic">
332
- Waiting to start...
333
- </div>
334
- ) : (
335
- migrationLogs.map((log, i) => (
336
- <div key={i} className="mb-1 whitespace-pre-wrap">
337
- {log}
338
- </div>
339
- ))
340
- )}
341
- <div ref={logsEndRef} />
342
- </div>
343
- </div>
344
- ) : (
345
- // Manual Instructions
346
- <>
347
- {/* Step 1: Prerequisites */}
348
- <div>
349
- <h4 className="mb-3 flex items-center gap-2 font-semibold">
350
- <span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
351
- 1
352
- </span>
353
- Prerequisites
354
- </h4>
355
- <div className="ml-8 space-y-3">
356
- <p className="text-sm text-muted-foreground">
357
- You will need the following before proceeding:
358
- </p>
359
- <ul className="list-inside list-disc space-y-1 text-sm">
360
- <li>
361
- Supabase CLI installed
362
- </li>
363
- <li>
364
- Project ID:{" "}
365
- <code className="rounded bg-muted px-1 py-0.5 text-xs">
366
- {projectId || "your-project-id"}
367
- </code>
368
- </li>
369
- <li>
370
- Database Password
371
- </li>
372
- </ul>
373
- </div>
374
- </div>
375
-
376
- {/* Step 2: Install Supabase CLI */}
377
- <div>
378
- <h4 className="mb-3 flex items-center gap-2 font-semibold">
379
- <span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
380
- 2
381
- </span>
382
- Install Supabase CLI
383
- </h4>
384
- <div className="ml-8 space-y-3">
385
- <div className="space-y-2">
386
- <p className="text-sm font-medium">
387
- MacOS (Brew)
388
- </p>
389
- <CodeBlock code="brew install supabase/tap/supabase" />
390
- </div>
391
-
392
- <div className="space-y-2">
393
- <p className="text-sm font-medium">
394
- Windows (Scoop)
395
- </p>
396
- <CodeBlock
397
- code={`scoop bucket add supabase https://github.com/supabase/scoop-bucket.git\nscoop install supabase`}
398
- />
399
- </div>
400
-
401
- <div className="space-y-2">
402
- <p className="text-sm font-medium">
403
- NPM (Universal)
404
- </p>
405
- <CodeBlock code="npm install -g supabase" />
406
- </div>
407
- </div>
408
- </div>
409
-
410
- {/* Step 3: Run Migration */}
411
- <div>
412
- <h4 className="mb-3 flex items-center gap-2 font-semibold">
413
- <span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
414
- 3
415
- </span>
416
- Run Migration
417
- </h4>
418
- <div className="ml-8 space-y-3">
419
- <p className="text-sm text-muted-foreground">
420
- Run the built-in migration tool:
421
- </p>
422
- <CodeBlock code="npx email-automator migrate" />
423
- </div>
424
- </div>
425
- </>
426
- )}
427
-
428
- {/* Troubleshooting */}
429
- <Alert className="border-red-200 bg-red-50 dark:border-red-900/40 dark:bg-red-950/20">
430
- <AlertTriangle className="h-4 w-4 text-red-700 dark:text-red-600" />
431
- <AlertDescription>
432
- <strong>Troubleshooting</strong>
433
- <ul className="mt-2 list-inside list-disc space-y-1 text-sm">
434
- <li>
435
- Try logging out: <code>supabase logout</code>
436
- </li>
437
- <li>
438
- Verify your database password is correct
439
- </li>
440
- </ul>
441
- </AlertDescription>
442
- </Alert>
443
- </div>
444
-
445
- <DialogFooter>
446
- <Button
447
- type="button"
448
- variant="outline"
449
- onClick={() => onOpenChange(false)}
450
- disabled={isMigrating}
451
- >
452
- Close
453
- </Button>
454
- </DialogFooter>
455
- </DialogContent>
456
- </Dialog>
457
- );
458
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * MigrationPulseIndicator Component
3
- *
4
- * Shows a pulsing dot indicator when migration is needed but notification is dismissed.
5
- * Provides a subtle, persistent reminder without being intrusive.
6
- */
7
-
8
- import { AlertTriangle } from "lucide-react";
9
- import { Button } from "../ui/button";
10
- // Tooltip not yet imported/ported, so simplified for now to just a Button
11
- // If we want tooltip we need to port it too, but omitting for simplicity as it wasn't in list
12
-
13
- interface MigrationPulseIndicatorProps {
14
- /** Callback when user clicks the indicator */
15
- onClick: () => void;
16
- }
17
-
18
- export function MigrationPulseIndicator({
19
- onClick,
20
- }: MigrationPulseIndicatorProps) {
21
- return (
22
- <Button
23
- variant="ghost"
24
- size="icon"
25
- className="relative"
26
- onClick={onClick}
27
- title="Database Update Available"
28
- >
29
- <AlertTriangle className="h-5 w-5 text-red-700 dark:text-red-600" />
30
- {/* Pulsing dot */}
31
- <span className="absolute right-0 top-0 flex h-2 w-2">
32
- <span className="absolute inline-flex h-full w-full rounded-full bg-red-600/70 opacity-75 motion-safe:animate-ping motion-reduce:animate-none" />
33
- <span className="relative inline-flex h-2 w-2 rounded-full bg-red-700 dark:bg-red-600" />
34
- </span>
35
- <span className="sr-only">Database migration pending</span>
36
- </Button>
37
- );
38
- }
@@ -1,24 +0,0 @@
1
- import { Moon, Sun } from "lucide-react"
2
- import { useTheme } from "../components/theme-provider"
3
- import { Button } from "../components/ui/button"
4
-
5
- export function ModeToggle() {
6
- const { setTheme, theme } = useTheme()
7
-
8
- return (
9
- <Button
10
- variant="ghost"
11
- size="icon"
12
- onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
13
- className="text-muted-foreground hover:text-foreground"
14
- title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
15
- >
16
- {theme === 'dark' ? (
17
- <Sun className="h-[1.2rem] w-[1.2rem]" />
18
- ) : (
19
- <Moon className="h-[1.2rem] w-[1.2rem]" />
20
- )}
21
- <span className="sr-only">Toggle theme</span>
22
- </Button>
23
- )
24
- }
@@ -1,72 +0,0 @@
1
- import { createContext, useContext, useEffect, useState } from "react"
2
-
3
- type Theme = "dark" | "light" | "system"
4
-
5
- type ThemeProviderProps = {
6
- children: React.ReactNode
7
- defaultTheme?: Theme
8
- storageKey?: string
9
- }
10
-
11
- type ThemeProviderState = {
12
- theme: Theme
13
- setTheme: (theme: Theme) => void
14
- }
15
-
16
- const initialState: ThemeProviderState = {
17
- theme: "system",
18
- setTheme: () => null,
19
- }
20
-
21
- const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
22
-
23
- export function ThemeProvider({
24
- children,
25
- defaultTheme = "system",
26
- storageKey = "vite-ui-theme",
27
- }: ThemeProviderProps) {
28
- const [theme, setTheme] = useState<Theme>(
29
- () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
30
- )
31
-
32
- useEffect(() => {
33
- const root = window.document.documentElement
34
-
35
- root.classList.remove("light", "dark")
36
-
37
- if (theme === "system") {
38
- const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
39
- .matches
40
- ? "dark"
41
- : "light"
42
-
43
- root.classList.add(systemTheme)
44
- return
45
- }
46
-
47
- root.classList.add(theme)
48
- }, [theme])
49
-
50
- const value = {
51
- theme,
52
- setTheme: (theme: Theme) => {
53
- localStorage.setItem(storageKey, theme)
54
- setTheme(theme)
55
- },
56
- }
57
-
58
- return (
59
- <ThemeProviderContext.Provider value={value}>
60
- {children}
61
- </ThemeProviderContext.Provider>
62
- )
63
- }
64
-
65
- export const useTheme = () => {
66
- const context = useContext(ThemeProviderContext)
67
-
68
- if (context === undefined)
69
- throw new Error("useTheme must be used within a ThemeProvider")
70
-
71
- return context
72
- }
@@ -1,66 +0,0 @@
1
- import * as React from "react"
2
- import { cva, type VariantProps } from "class-variance-authority"
3
-
4
- import { cn } from "@/lib/utils"
5
-
6
- const alertVariants = cva(
7
- "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
- {
9
- variants: {
10
- variant: {
11
- default: "bg-card text-card-foreground",
12
- destructive:
13
- "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
- },
15
- },
16
- defaultVariants: {
17
- variant: "default",
18
- },
19
- }
20
- )
21
-
22
- function Alert({
23
- className,
24
- variant,
25
- ...props
26
- }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
- return (
28
- <div
29
- data-slot="alert"
30
- role="alert"
31
- className={cn(alertVariants({ variant }), className)}
32
- {...props}
33
- />
34
- )
35
- }
36
-
37
- function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
- return (
39
- <div
40
- data-slot="alert-title"
41
- className={cn(
42
- "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
- className
44
- )}
45
- {...props}
46
- />
47
- )
48
- }
49
-
50
- function AlertDescription({
51
- className,
52
- ...props
53
- }: React.ComponentProps<"div">) {
54
- return (
55
- <div
56
- data-slot="alert-description"
57
- className={cn(
58
- "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
- className
60
- )}
61
- {...props}
62
- />
63
- )
64
- }
65
-
66
- export { Alert, AlertTitle, AlertDescription }