@xk2800/nextjs-template 0.7.1-beta.7 → 0.7.1-beta.9

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 (46) hide show
  1. package/components/dashboard/admin/broadcastActions.tsx +148 -0
  2. package/components/dashboard/admin/confirmSendDialog.tsx +44 -0
  3. package/components/dashboard/admin/marketingEmailForm.tsx +115 -0
  4. package/components/dashboard/admin/marketingSection.tsx +67 -0
  5. package/components/dashboard/admin/subscribersCard.tsx +62 -0
  6. package/components/dashboard/marketingUpdatesCard.tsx +62 -0
  7. package/components/dashboard/marketingUpdatesSection.tsx +10 -0
  8. package/components/ui/alert-dialog.tsx +1 -1
  9. package/components/ui/badge.tsx +1 -1
  10. package/components/ui/card.tsx +1 -1
  11. package/components/ui/dialog.tsx +1 -1
  12. package/components/ui/dropdown-menu.tsx +2 -2
  13. package/components/ui/select.tsx +1 -1
  14. package/components/ui/switch.tsx +2 -2
  15. package/dist/api/feedback.mjs +2 -1
  16. package/dist/api/feedback.mjs.map +1 -1
  17. package/dist/api/marketing.d.mts +36 -0
  18. package/dist/api/marketing.d.ts +36 -0
  19. package/dist/api/marketing.js +1357 -0
  20. package/dist/api/marketing.js.map +1 -0
  21. package/dist/api/marketing.mjs +170 -0
  22. package/dist/api/marketing.mjs.map +1 -0
  23. package/dist/auth/helpers.d.mts +3 -3
  24. package/dist/auth/helpers.d.ts +3 -3
  25. package/dist/auth/helpers.mjs +5 -60
  26. package/dist/auth/helpers.mjs.map +1 -1
  27. package/dist/auth/index.mjs +2 -1
  28. package/dist/auth-client/index.d.mts +16 -16
  29. package/dist/auth-client/index.d.ts +16 -16
  30. package/dist/chunk-6Z3WUSDJ.mjs +62 -0
  31. package/dist/chunk-6Z3WUSDJ.mjs.map +1 -0
  32. package/dist/{chunk-6B45N7ZB.mjs → chunk-7QVIOQ47.mjs} +4 -13
  33. package/dist/chunk-7QVIOQ47.mjs.map +1 -0
  34. package/dist/chunk-C6RGN6QA.mjs +60 -0
  35. package/dist/chunk-C6RGN6QA.mjs.map +1 -0
  36. package/dist/chunk-OVKVD2HH.mjs +25 -0
  37. package/dist/chunk-OVKVD2HH.mjs.map +1 -0
  38. package/dist/marketing/index.d.mts +35 -0
  39. package/dist/marketing/index.d.ts +35 -0
  40. package/dist/marketing/index.js +125 -0
  41. package/dist/marketing/index.js.map +1 -0
  42. package/dist/marketing/index.mjs +6 -0
  43. package/dist/marketing/index.mjs.map +1 -0
  44. package/package.json +22 -2
  45. package/styles/theme.css +17 -3
  46. package/dist/chunk-6B45N7ZB.mjs.map +0 -1
@@ -0,0 +1,148 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from "react"
4
+ import { useRouter } from "next/navigation"
5
+ import { toast } from "sonner"
6
+ import { Button } from "../../ui/button"
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "../../ui/dialog"
14
+ import ConfirmSendDialog from "./confirmSendDialog"
15
+
16
+ interface Preview {
17
+ subject: string | null
18
+ from: string | null
19
+ previewText: string | null
20
+ html: string | null
21
+ text: string | null
22
+ }
23
+
24
+ export default function BroadcastActions({ id, name, status }: { id: string; name: string; status: string }) {
25
+ const router = useRouter()
26
+ const [open, setOpen] = useState(false)
27
+ const [preview, setPreview] = useState<Preview | null>(null)
28
+ const [loading, setLoading] = useState(false)
29
+ const [sending, setSending] = useState(false)
30
+ const [confirming, setConfirming] = useState(false)
31
+
32
+ const openPreview = async () => {
33
+ setOpen(true)
34
+ if (preview) return
35
+ setLoading(true)
36
+ try {
37
+ const response = await fetch(`/api/admin/marketing/${id}`)
38
+ const data = await response.json()
39
+ if (!response.ok) throw new Error(data.error || "Failed to load preview")
40
+ setPreview(data)
41
+ } catch (error) {
42
+ toast.error(error instanceof Error ? error.message : "Failed to load preview")
43
+ setOpen(false)
44
+ } finally {
45
+ setLoading(false)
46
+ }
47
+ }
48
+
49
+ const send = async () => {
50
+ setSending(true)
51
+ try {
52
+ const response = await fetch(`/api/admin/marketing/${id}`, { method: "POST" })
53
+ const data = await response.json()
54
+ if (!response.ok) throw new Error(data.error || "Failed to send broadcast")
55
+ toast.success("Broadcast sent")
56
+ setOpen(false)
57
+ router.refresh()
58
+ } catch (error) {
59
+ toast.error(error instanceof Error ? error.message : "Failed to send broadcast")
60
+ } finally {
61
+ setSending(false)
62
+ }
63
+ }
64
+
65
+ // Resend has no push for broadcast status, so poll while it's queued and
66
+ // refresh the list once it moves on (e.g. to "sent").
67
+ // ponytail: stops after ~5 min; a broadcast scheduled for later stays
68
+ // "queued" until it goes out, so it's not worth polling that long.
69
+ useEffect(() => {
70
+ if (status !== "queued") return
71
+ let tries = 0
72
+ const timer = setInterval(async () => {
73
+ if (++tries > 100) return clearInterval(timer)
74
+ try {
75
+ const response = await fetch(`/api/admin/marketing/${id}`)
76
+ if (!response.ok) return
77
+ const data = await response.json()
78
+ if (data.status !== "queued") {
79
+ clearInterval(timer)
80
+ router.refresh()
81
+ }
82
+ } catch {
83
+ // network blip, try again next tick
84
+ }
85
+ }, 3000)
86
+ return () => clearInterval(timer)
87
+ }, [id, status, router])
88
+
89
+ const isDraft = status === "draft"
90
+
91
+ return (
92
+ <>
93
+ {status === "queued" && (
94
+ <span className="flex items-center gap-1.5 text-xs">
95
+ <span className="size-2 animate-pulse rounded-full bg-amber-500" />
96
+ Sending…
97
+ </span>
98
+ )}
99
+ <Button size="sm" variant="outline" onClick={openPreview}>Preview</Button>
100
+ {isDraft && (
101
+ <Button size="sm" disabled={sending} onClick={() => setConfirming(true)}>
102
+ {sending ? "Sending…" : "Send"}
103
+ </Button>
104
+ )}
105
+
106
+ <Dialog open={open} onOpenChange={setOpen}>
107
+ <DialogContent className="sm:max-w-2xl">
108
+ <DialogHeader>
109
+ <DialogTitle>{preview?.subject || name}</DialogTitle>
110
+ <DialogDescription>
111
+ {preview ? `From ${preview.from ?? "—"}${preview.previewText ? ` · ${preview.previewText}` : ""}` : "Loading…"}
112
+ </DialogDescription>
113
+ </DialogHeader>
114
+ {loading ? (
115
+ <div className="h-[60vh] animate-pulse rounded-md bg-muted" />
116
+ ) : preview?.html ? (
117
+ // sandbox="" — no scripts, no navigation out of the frame
118
+ <iframe
119
+ title="Email preview"
120
+ sandbox=""
121
+ srcDoc={preview.html}
122
+ className="h-[60vh] w-full rounded-md border bg-white"
123
+ />
124
+ ) : (
125
+ <pre className="h-[60vh] overflow-auto whitespace-pre-wrap rounded-md border p-4 text-sm">
126
+ {preview?.text ?? ""}
127
+ </pre>
128
+ )}
129
+ {isDraft && (
130
+ <div className="flex justify-end">
131
+ <Button disabled={sending || loading} onClick={() => setConfirming(true)}>
132
+ {sending ? "Sending…" : "Send now"}
133
+ </Button>
134
+ </div>
135
+ )}
136
+ </DialogContent>
137
+ </Dialog>
138
+
139
+ <ConfirmSendDialog
140
+ open={confirming}
141
+ onOpenChange={setConfirming}
142
+ subject={preview?.subject || name}
143
+ audience="its segment"
144
+ onConfirm={send}
145
+ />
146
+ </>
147
+ )
148
+ }
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ } from "../../ui/alert-dialog"
13
+
14
+ export default function ConfirmSendDialog({
15
+ open,
16
+ onOpenChange,
17
+ subject,
18
+ audience,
19
+ onConfirm,
20
+ }: {
21
+ open: boolean
22
+ onOpenChange: (open: boolean) => void
23
+ subject: string
24
+ audience: string
25
+ onConfirm: () => void
26
+ }) {
27
+ return (
28
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
29
+ <AlertDialogContent>
30
+ <AlertDialogHeader>
31
+ <AlertDialogTitle>Send this broadcast?</AlertDialogTitle>
32
+ <AlertDialogDescription>
33
+ &ldquo;{subject}&rdquo; goes to every subscribed contact in {audience} right away.
34
+ This can&apos;t be undone.
35
+ </AlertDialogDescription>
36
+ </AlertDialogHeader>
37
+ <AlertDialogFooter>
38
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
39
+ <AlertDialogAction onClick={onConfirm}>Send now</AlertDialogAction>
40
+ </AlertDialogFooter>
41
+ </AlertDialogContent>
42
+ </AlertDialog>
43
+ )
44
+ }
@@ -0,0 +1,115 @@
1
+ 'use client'
2
+
3
+ import { useState } from "react"
4
+ import { useRouter } from "next/navigation"
5
+ import { toast } from "sonner"
6
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../ui/card"
7
+ import { Input } from "../../ui/input"
8
+ import { Label } from "../../ui/label"
9
+ import { Textarea } from "../../ui/textarea"
10
+ import { Button } from "../../ui/button"
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../ui/select"
12
+ import ConfirmSendDialog from "./confirmSendDialog"
13
+
14
+ export default function MarketingEmailForm({ segments }: { segments: { id: string; name: string }[] }) {
15
+ const router = useRouter()
16
+ const [segmentId, setSegmentId] = useState(segments[0]?.id ?? "")
17
+ const [subject, setSubject] = useState("")
18
+ const [previewText, setPreviewText] = useState("")
19
+ const [body, setBody] = useState("")
20
+ const [pending, setPending] = useState<"draft" | "send" | null>(null)
21
+
22
+ const [confirming, setConfirming] = useState(false)
23
+ const segmentName = segments.find((s) => s.id === segmentId)?.name ?? "this segment"
24
+
25
+ const submit = async (send: boolean) => {
26
+ setPending(send ? "send" : "draft")
27
+ try {
28
+ const response = await fetch("/api/admin/marketing", {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/json" },
31
+ body: JSON.stringify({ segmentId, subject, previewText, body, send }),
32
+ })
33
+ const data = await response.json()
34
+ if (!response.ok) throw new Error(data.error || "Failed to create broadcast")
35
+
36
+ toast.success(send ? "Broadcast sent" : "Draft saved in Resend")
37
+ setSubject("")
38
+ setPreviewText("")
39
+ setBody("")
40
+ router.refresh()
41
+ } catch (error) {
42
+ toast.error(error instanceof Error ? error.message : "Failed to create broadcast")
43
+ } finally {
44
+ setPending(null)
45
+ }
46
+ }
47
+
48
+ if (segments.length === 0) {
49
+ return (
50
+ <Card>
51
+ <CardHeader>
52
+ <CardTitle>No segments yet</CardTitle>
53
+ <CardDescription>
54
+ Create a segment and add contacts in the Resend dashboard (Audience), then reload this page.
55
+ </CardDescription>
56
+ </CardHeader>
57
+ </Card>
58
+ )
59
+ }
60
+
61
+ const canSubmit = !!segmentId && !!subject.trim() && !!body.trim() && !pending
62
+
63
+ return (
64
+ <Card>
65
+ <CardHeader>
66
+ <CardTitle>New broadcast</CardTitle>
67
+ <CardDescription>
68
+ Plain text. Blank lines start a new paragraph. An unsubscribe link is added automatically.
69
+ </CardDescription>
70
+ </CardHeader>
71
+ <CardContent className="space-y-4">
72
+ <div className="space-y-2">
73
+ <Label htmlFor="segment">Segment</Label>
74
+ <Select value={segmentId} onValueChange={setSegmentId}>
75
+ <SelectTrigger id="segment" className="w-full">
76
+ <SelectValue placeholder="Choose a segment" />
77
+ </SelectTrigger>
78
+ <SelectContent>
79
+ {segments.map((s) => (
80
+ <SelectItem key={s.id} value={s.id}>{s.name}</SelectItem>
81
+ ))}
82
+ </SelectContent>
83
+ </Select>
84
+ </div>
85
+ <div className="space-y-2">
86
+ <Label htmlFor="subject">Subject</Label>
87
+ <Input id="subject" value={subject} maxLength={200} onChange={(e) => setSubject(e.target.value)} />
88
+ </div>
89
+ <div className="space-y-2">
90
+ <Label htmlFor="preview">Preview text (optional)</Label>
91
+ <Input id="preview" value={previewText} maxLength={200} onChange={(e) => setPreviewText(e.target.value)} />
92
+ </div>
93
+ <div className="space-y-2">
94
+ <Label htmlFor="body">Message</Label>
95
+ <Textarea id="body" rows={12} value={body} onChange={(e) => setBody(e.target.value)} />
96
+ </div>
97
+ <div className="flex flex-col gap-2 sm:flex-row sm:justify-end">
98
+ <Button variant="outline" disabled={!canSubmit} onClick={() => submit(false)}>
99
+ {pending === "draft" ? "Saving…" : "Save as draft"}
100
+ </Button>
101
+ <Button disabled={!canSubmit} onClick={() => setConfirming(true)}>
102
+ {pending === "send" ? "Sending…" : "Send now"}
103
+ </Button>
104
+ </div>
105
+ <ConfirmSendDialog
106
+ open={confirming}
107
+ onOpenChange={setConfirming}
108
+ subject={subject}
109
+ audience={`"${segmentName}"`}
110
+ onConfirm={() => submit(true)}
111
+ />
112
+ </CardContent>
113
+ </Card>
114
+ )
115
+ }
@@ -0,0 +1,67 @@
1
+ import { Suspense } from "react"
2
+ import { getMarketingOverview } from "@xk2800/nextjs-template/marketing"
3
+ import { formatDateTime } from "@xk2800/nextjs-template/lib/formatters"
4
+ import { Skeleton } from "../../ui/skeleton"
5
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../ui/card"
6
+ import { Badge } from "../../ui/badge"
7
+ import MarketingEmailForm from "./marketingEmailForm"
8
+ import BroadcastActions from "./broadcastActions"
9
+ import SubscribersCard from "./subscribersCard"
10
+
11
+ // Admin marketing email page body: compose form, recent broadcasts and
12
+ // subscribers. `segmentId` picks the Subscribers segment (e.g. from
13
+ // ?segment=); unknown or missing falls back to the first segment.
14
+ export default async function MarketingSection({ segmentId }: { segmentId?: string }) {
15
+ const { segments, broadcasts, error } = await getMarketingOverview()
16
+
17
+ if (error) {
18
+ return (
19
+ <Card>
20
+ <CardHeader>
21
+ <CardTitle>Resend isn&apos;t reachable</CardTitle>
22
+ <CardDescription>
23
+ {error} Broadcasts need a <strong>full access</strong> API key (a sending-only key can&apos;t list segments).
24
+ </CardDescription>
25
+ </CardHeader>
26
+ </Card>
27
+ )
28
+ }
29
+
30
+ return (
31
+ <div className="space-y-6">
32
+ <MarketingEmailForm segments={segments} />
33
+
34
+ {broadcasts.length > 0 && (
35
+ <Card>
36
+ <CardHeader>
37
+ <CardTitle>Recent broadcasts</CardTitle>
38
+ <CardDescription>Open, click and unsubscribe stats are in the Resend dashboard</CardDescription>
39
+ </CardHeader>
40
+ <CardContent>
41
+ <ul className="divide-y">
42
+ {broadcasts.map((b) => (
43
+ <li key={b.id} className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 py-2 text-sm">
44
+ <span className="truncate">{b.name || "(untitled)"}</span>
45
+ <span className="flex shrink-0 items-center gap-2 text-muted-foreground">
46
+ {formatDateTime(b.sent_at ?? b.created_at)}
47
+ <Badge variant={b.status === "sent" ? "default" : "secondary"}>{b.status}</Badge>
48
+ <BroadcastActions id={b.id} name={b.name || "(untitled)"} status={b.status} />
49
+ </span>
50
+ </li>
51
+ ))}
52
+ </ul>
53
+ </CardContent>
54
+ </Card>
55
+ )}
56
+
57
+ {segments.length > 0 && (
58
+ <Suspense fallback={<Skeleton className="h-48 w-full" />}>
59
+ <SubscribersCard
60
+ segments={segments}
61
+ segmentId={segments.some((s) => s.id === segmentId) ? segmentId! : segments[0].id}
62
+ />
63
+ </Suspense>
64
+ )}
65
+ </div>
66
+ )
67
+ }
@@ -0,0 +1,62 @@
1
+ import Link from "next/link"
2
+ import { listSegmentContacts, SEGMENT_CONTACTS_LIMIT as LIMIT, type MarketingSegment } from "@xk2800/nextjs-template/marketing"
3
+ import { formatDateTime } from "@xk2800/nextjs-template/lib/formatters"
4
+ import { Button } from "../../ui/button"
5
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../ui/card"
6
+ import { Badge } from "../../ui/badge"
7
+
8
+ export default async function SubscribersCard({
9
+ segments,
10
+ segmentId,
11
+ }: {
12
+ segments: MarketingSegment[]
13
+ segmentId: string
14
+ }) {
15
+ const { contacts, hasMore, error } = await listSegmentContacts(segmentId)
16
+ const subscribed = contacts.filter((c) => !c.unsubscribed).length
17
+
18
+ return (
19
+ <Card>
20
+ <CardHeader>
21
+ <CardTitle>Subscribers</CardTitle>
22
+ <CardDescription>
23
+ {error
24
+ ? `Couldn't load contacts: ${error}`
25
+ : `${subscribed} subscribed, ${contacts.length - subscribed} unsubscribed${hasMore ?` (first ${LIMIT} shown)` : ""}. Add or import contacts in Resend → Audience.`}
26
+ </CardDescription>
27
+ {segments.length > 1 && (
28
+ <div className="flex flex-wrap gap-2 pt-2">
29
+ {segments.map((s) => (
30
+ <Button key={s.id} asChild size="sm" variant={s.id === segmentId ? "default" : "outline"}>
31
+ <Link href={`?segment=${s.id}`} scroll={false}>{s.name}</Link>
32
+ </Button>
33
+ ))}
34
+ </div>
35
+ )}
36
+ </CardHeader>
37
+ {contacts.length > 0 && (
38
+ <CardContent>
39
+ <ul className="divide-y">
40
+ {contacts.map((c) => {
41
+ const name = [c.first_name, c.last_name].filter(Boolean).join(" ")
42
+ return (
43
+ <li key={c.id} className="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 py-2 text-sm">
44
+ <span className="min-w-0 truncate">
45
+ {c.email}
46
+ {name && <span className="text-muted-foreground"> · {name}</span>}
47
+ </span>
48
+ <span className="flex shrink-0 items-center gap-2 text-muted-foreground">
49
+ {formatDateTime(c.created_at)}
50
+ <Badge variant={c.unsubscribed ? "secondary" : "default"}>
51
+ {c.unsubscribed ? "unsubscribed" : "subscribed"}
52
+ </Badge>
53
+ </span>
54
+ </li>
55
+ )
56
+ })}
57
+ </ul>
58
+ </CardContent>
59
+ )}
60
+ </Card>
61
+ )
62
+ }
@@ -0,0 +1,62 @@
1
+ 'use client'
2
+
3
+ import { useState } from "react"
4
+ import { toast } from "sonner"
5
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../ui/card"
6
+ import { Switch } from "../ui/switch"
7
+ import { Label } from "../ui/label"
8
+
9
+ export default function MarketingUpdatesCard({
10
+ initialSubscribed,
11
+ emailVerified,
12
+ }: {
13
+ initialSubscribed: boolean
14
+ emailVerified: boolean
15
+ }) {
16
+ const [subscribed, setSubscribed] = useState(initialSubscribed)
17
+ const [saving, setSaving] = useState(false)
18
+
19
+ const toggle = async (next: boolean) => {
20
+ setSaving(true)
21
+ try {
22
+ const response = await fetch("/api/marketing/subscription", {
23
+ method: "PUT",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify({ subscribed: next }),
26
+ })
27
+ const data = await response.json()
28
+ if (!response.ok) throw new Error(data.error || "Failed to update subscription")
29
+ setSubscribed(next)
30
+ toast.success(next ? "Subscribed to product updates" : "Unsubscribed from product updates")
31
+ } catch (error) {
32
+ toast.error(error instanceof Error ? error.message : "Failed to update subscription")
33
+ } finally {
34
+ setSaving(false)
35
+ }
36
+ }
37
+
38
+ return (
39
+ <Card>
40
+ <CardHeader>
41
+ <CardTitle>Email updates</CardTitle>
42
+ <CardDescription>
43
+ Occasional news about new features. You can turn this off here or with the link in any email.
44
+ </CardDescription>
45
+ </CardHeader>
46
+ <CardContent className="space-y-2">
47
+ <div className="flex items-center justify-between gap-4">
48
+ <Label htmlFor="marketing-updates">Send me product updates</Label>
49
+ <Switch
50
+ id="marketing-updates"
51
+ checked={subscribed}
52
+ disabled={saving || (!subscribed && !emailVerified)}
53
+ onCheckedChange={toggle}
54
+ />
55
+ </div>
56
+ {!subscribed && !emailVerified && (
57
+ <p className="text-sm text-muted-foreground">Verify your email address to subscribe.</p>
58
+ )}
59
+ </CardContent>
60
+ </Card>
61
+ )
62
+ }
@@ -0,0 +1,10 @@
1
+ import { isSubscribed, MARKETING_SEGMENT_ID } from "@xk2800/nextjs-template/marketing"
2
+ import MarketingUpdatesCard from "./marketingUpdatesCard"
3
+
4
+ // Dashboard opt-in for product updates. Renders nothing until
5
+ // RESEND_MARKETING_SEGMENT_ID is set.
6
+ export default async function MarketingUpdatesSection({ email, emailVerified }: { email: string; emailVerified: boolean }) {
7
+ if (!MARKETING_SEGMENT_ID) return null
8
+ const subscribed = await isSubscribed(email)
9
+ return <MarketingUpdatesCard initialSubscribed={subscribed} emailVerified={emailVerified} />
10
+ }
@@ -47,7 +47,7 @@ function AlertDialogContent({
47
47
  <AlertDialogOverlay />
48
48
  <AlertDialogPrimitive.Content
49
49
  className={cn(
50
- "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
50
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border-(length:--surface-border-width) border-(--surface-border-color) p-6 shadow-(--dialog-shadow) duration-200 sm:max-w-lg",
51
51
  className
52
52
  )}
53
53
  {...props}
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
5
5
  import { cn } from "../../lib/utils"
6
6
 
7
7
  const badgeVariants = cva(
8
- "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
8
+ "inline-flex items-center justify-center rounded-(--pill-radius) border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
9
  {
10
10
  variants: {
11
11
  variant: {
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
7
7
  <div
8
8
  data-slot="card"
9
9
  className={cn(
10
- "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border-(length:--surface-border-width) border-(--surface-border-color) py-6 shadow-(--card-shadow)",
11
11
  className
12
12
  )}
13
13
  {...props}
@@ -60,7 +60,7 @@ function DialogContent({
60
60
  <DialogPrimitive.Content
61
61
  data-slot="dialog-content"
62
62
  className={cn(
63
- "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
63
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border-(length:--surface-border-width) border-(--surface-border-color) p-6 shadow-(--dialog-shadow) duration-200 sm:max-w-lg",
64
64
  className
65
65
  )}
66
66
  {...props}
@@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
47
47
  <DropdownMenuPrimitive.SubContent
48
48
  ref={ref}
49
49
  className={cn(
50
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
50
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border-(length:--surface-border-width) border-(--surface-border-color) bg-popover p-1 text-popover-foreground shadow-(--popover-shadow) data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
51
  className
52
52
  )}
53
53
  {...props}
@@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
65
65
  ref={ref}
66
66
  sideOffset={sideOffset}
67
67
  className={cn(
68
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
68
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border-(length:--surface-border-width) border-(--surface-border-color) bg-popover p-1 text-popover-foreground shadow-(--popover-shadow) data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
69
69
  className
70
70
  )}
71
71
  {...props}
@@ -61,7 +61,7 @@ function SelectContent({
61
61
  <SelectPrimitive.Content
62
62
  data-slot="select-content"
63
63
  className={cn(
64
- "relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
64
+ "relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border-(length:--surface-border-width) border-(--surface-border-color) bg-popover text-popover-foreground shadow-(--popover-shadow) data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
65
65
  position === "popper" &&
66
66
  "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
67
67
  className
@@ -17,7 +17,7 @@ function Switch({
17
17
  data-slot="switch"
18
18
  data-size={size}
19
19
  className={cn(
20
- "peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
20
+ "peer group/switch inline-flex shrink-0 items-center rounded-(--pill-radius) border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
21
21
  className
22
22
  )}
23
23
  {...props}
@@ -25,7 +25,7 @@ function Switch({
25
25
  <SwitchPrimitive.Thumb
26
26
  data-slot="switch-thumb"
27
27
  className={cn(
28
- "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground"
28
+ "pointer-events-none block rounded-(--pill-radius) bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground"
29
29
  )}
30
30
  />
31
31
  </SwitchPrimitive.Root>
@@ -1,8 +1,9 @@
1
1
  import { lookupErrorDetail, unexpectedErrorMessage } from '../chunk-6MDQRT6U.mjs';
2
- import { auth, getResend, EMAIL_FROM } from '../chunk-6B45N7ZB.mjs';
2
+ import { auth } from '../chunk-7QVIOQ47.mjs';
3
3
  import '../chunk-7F2POZM6.mjs';
4
4
  import '../chunk-6OAHCEDJ.mjs';
5
5
  import { getSystemSettings } from '../chunk-P2G23JMY.mjs';
6
+ import { getResend, EMAIL_FROM } from '../chunk-OVKVD2HH.mjs';
6
7
  import '../chunk-ZJMFE5B2.mjs';
7
8
  import '../chunk-TR5XFBTD.mjs';
8
9
  import { config } from '../chunk-TKWNEKK6.mjs';