@viliha/vui-ui 1.1.8 → 1.2.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 (137) hide show
  1. package/AGENT.md +79 -9
  2. package/README.md +60 -0
  3. package/bin/vui.mjs +211 -0
  4. package/package.json +10 -2
  5. package/template/.env.example +20 -0
  6. package/template/app/(app)/branches/branches-table.tsx +69 -0
  7. package/template/app/(app)/branches/page.tsx +13 -0
  8. package/template/app/(app)/businesses/businesses-table.tsx +39 -0
  9. package/template/app/(app)/businesses/page.tsx +13 -0
  10. package/template/app/(app)/calendar/page.tsx +725 -0
  11. package/template/app/(app)/charts/charts-content.tsx +190 -0
  12. package/template/app/(app)/charts/layout.tsx +8 -0
  13. package/template/app/(app)/charts/page.tsx +44 -0
  14. package/template/app/(app)/chat/layout.tsx +14 -0
  15. package/template/app/(app)/chat/page.tsx +311 -0
  16. package/template/app/(app)/components/layout.tsx +8 -0
  17. package/template/app/(app)/components/page.tsx +464 -0
  18. package/template/app/(app)/crm/companies/companies-table.tsx +56 -0
  19. package/template/app/(app)/crm/companies/page.tsx +13 -0
  20. package/template/app/(app)/crm/opportunities/opportunities-board.tsx +484 -0
  21. package/template/app/(app)/crm/opportunities/page.tsx +13 -0
  22. package/template/app/(app)/crm/people/page.tsx +13 -0
  23. package/template/app/(app)/crm/people/people-table.tsx +50 -0
  24. package/template/app/(app)/dashboard/layout.tsx +8 -0
  25. package/template/app/(app)/dashboard/page.tsx +239 -0
  26. package/template/app/(app)/departments/departments-table.tsx +55 -0
  27. package/template/app/(app)/departments/page.tsx +13 -0
  28. package/template/app/(app)/employees/employees-table.tsx +64 -0
  29. package/template/app/(app)/employees/page.tsx +13 -0
  30. package/template/app/(app)/forms/layout.tsx +8 -0
  31. package/template/app/(app)/forms/page.tsx +388 -0
  32. package/template/app/(app)/layout.tsx +74 -0
  33. package/template/app/(app)/markets/markets-table.tsx +76 -0
  34. package/template/app/(app)/markets/page.tsx +13 -0
  35. package/template/app/(app)/organizations/edit/page.tsx +72 -0
  36. package/template/app/(app)/organizations/new/page.tsx +46 -0
  37. package/template/app/(app)/organizations/organizations-config.tsx +97 -0
  38. package/template/app/(app)/organizations/organizations-table.tsx +42 -0
  39. package/template/app/(app)/organizations/page.tsx +13 -0
  40. package/template/app/(app)/settings/layout.tsx +8 -0
  41. package/template/app/(app)/settings/page.tsx +255 -0
  42. package/template/app/(app)/steps/page.tsx +263 -0
  43. package/template/app/(app)/support/layout.tsx +14 -0
  44. package/template/app/(app)/support/page.tsx +345 -0
  45. package/template/app/(app)/system/cities/cities-table.tsx +34 -0
  46. package/template/app/(app)/system/cities/page.tsx +13 -0
  47. package/template/app/(app)/system/countries/countries-table.tsx +34 -0
  48. package/template/app/(app)/system/countries/page.tsx +13 -0
  49. package/template/app/(app)/system/currencies/currencies-table.tsx +34 -0
  50. package/template/app/(app)/system/currencies/page.tsx +13 -0
  51. package/template/app/(app)/system/languages/languages-table.tsx +32 -0
  52. package/template/app/(app)/system/languages/page.tsx +13 -0
  53. package/template/app/(app)/system/regions/page.tsx +13 -0
  54. package/template/app/(app)/system/regions/regions-table.tsx +32 -0
  55. package/template/app/(app)/users/page.tsx +13 -0
  56. package/template/app/(app)/users/users-table.tsx +97 -0
  57. package/template/app/_components/app-sidebar.tsx +565 -0
  58. package/template/app/_components/auth.tsx +184 -0
  59. package/template/app/_components/breadcrumbs.tsx +26 -0
  60. package/template/app/_components/global-search.tsx +209 -0
  61. package/template/app/_components/logo.tsx +49 -0
  62. package/template/app/_components/nav-config.ts +138 -0
  63. package/template/app/_components/open-tabs.tsx +530 -0
  64. package/template/app/_components/quick-actions.tsx +140 -0
  65. package/template/app/_components/route-meta.ts +137 -0
  66. package/template/app/_components/set-page-title.tsx +15 -0
  67. package/template/app/_components/showcase.tsx +29 -0
  68. package/template/app/_components/stat-card.tsx +56 -0
  69. package/template/app/_components/theme-toggle.tsx +46 -0
  70. package/template/app/_components/top-bar.tsx +109 -0
  71. package/template/app/_components/user-menu.tsx +109 -0
  72. package/template/app/_components/wordmark.tsx +45 -0
  73. package/template/app/apple-icon.png +0 -0
  74. package/template/app/auth/forgot-password/page.tsx +102 -0
  75. package/template/app/auth/layout.tsx +43 -0
  76. package/template/app/auth/page.tsx +13 -0
  77. package/template/app/auth/reset-password/page.tsx +108 -0
  78. package/template/app/auth/signin/page.tsx +238 -0
  79. package/template/app/auth/signup/page.tsx +159 -0
  80. package/template/app/auth/verify/page.tsx +86 -0
  81. package/template/app/error.tsx +56 -0
  82. package/template/app/globals.css +6 -0
  83. package/template/app/icon.png +0 -0
  84. package/template/app/icon.svg +4 -0
  85. package/template/app/layout.tsx +103 -0
  86. package/template/app/not-found.tsx +29 -0
  87. package/template/app/onboarding/layout.tsx +31 -0
  88. package/template/app/onboarding/page.tsx +399 -0
  89. package/template/app/page.tsx +13 -0
  90. package/template/app/register-business/layout.tsx +30 -0
  91. package/template/app/register-business/page.tsx +234 -0
  92. package/template/components/ui/accordion.tsx +66 -0
  93. package/template/components/ui/alert-dialog.tsx +196 -0
  94. package/template/components/ui/alert.tsx +66 -0
  95. package/template/components/ui/aspect-ratio.tsx +11 -0
  96. package/template/components/ui/avatar.tsx +109 -0
  97. package/template/components/ui/badge.tsx +48 -0
  98. package/template/components/ui/breadcrumb.tsx +109 -0
  99. package/template/components/ui/button.tsx +64 -0
  100. package/template/components/ui/calendar.tsx +220 -0
  101. package/template/components/ui/card.tsx +92 -0
  102. package/template/components/ui/checkbox.tsx +32 -0
  103. package/template/components/ui/collapsible.tsx +33 -0
  104. package/template/components/ui/command.tsx +184 -0
  105. package/template/components/ui/dialog.tsx +158 -0
  106. package/template/components/ui/dropdown-menu.tsx +257 -0
  107. package/template/components/ui/form.tsx +167 -0
  108. package/template/components/ui/hover-card.tsx +44 -0
  109. package/template/components/ui/input-otp.tsx +77 -0
  110. package/template/components/ui/input.tsx +21 -0
  111. package/template/components/ui/label.tsx +24 -0
  112. package/template/components/ui/popover.tsx +89 -0
  113. package/template/components/ui/progress.tsx +31 -0
  114. package/template/components/ui/radio-group.tsx +45 -0
  115. package/template/components/ui/scroll-area.tsx +58 -0
  116. package/template/components/ui/select.tsx +190 -0
  117. package/template/components/ui/separator.tsx +28 -0
  118. package/template/components/ui/sheet.tsx +143 -0
  119. package/template/components/ui/skeleton.tsx +13 -0
  120. package/template/components/ui/slider.tsx +63 -0
  121. package/template/components/ui/sonner.tsx +40 -0
  122. package/template/components/ui/switch.tsx +35 -0
  123. package/template/components/ui/table.tsx +116 -0
  124. package/template/components/ui/tabs.tsx +91 -0
  125. package/template/components/ui/textarea.tsx +18 -0
  126. package/template/components/ui/toggle-group.tsx +83 -0
  127. package/template/components/ui/toggle.tsx +47 -0
  128. package/template/components/ui/tooltip.tsx +57 -0
  129. package/template/lib/auth-demo.ts +94 -0
  130. package/template/lib/crm-data.ts +80 -0
  131. package/template/lib/demo-data.ts +267 -0
  132. package/template/lib/mock-data.ts +169 -0
  133. package/template/lib/org-store.ts +50 -0
  134. package/template/lib/seo.ts +188 -0
  135. package/template/lib/utils.ts +3 -0
  136. package/template/next.config.mjs +13 -0
  137. package/template/postcss.config.mjs +7 -0
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import Link from "next/link";
5
+ import {
6
+ ExclamationTriangleIcon as AlertTriangle,
7
+ ReloadIcon as Reload,
8
+ } from "@radix-ui/react-icons";
9
+
10
+ import { Button } from "@viliha/vui-ui/button";
11
+
12
+ /** Root error boundary — shown when a route throws. Keep it self-contained
13
+ * (it renders in place of the page, outside the app shell). */
14
+ export default function Error({
15
+ error,
16
+ reset,
17
+ }: {
18
+ error: Error & { digest?: string };
19
+ reset: () => void;
20
+ }) {
21
+ React.useEffect(() => {
22
+ // Wire to your error reporter (Sentry, etc.) here.
23
+ console.error(error);
24
+ }, [error]);
25
+
26
+ return (
27
+ <main className="flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center">
28
+ <div className="grid size-14 place-items-center rounded-full bg-destructive/10 text-destructive">
29
+ <AlertTriangle className="size-7" />
30
+ </div>
31
+ <p className="text-6xl font-semibold tracking-tight text-muted-foreground">
32
+ 500
33
+ </p>
34
+ <h1 className="text-2xl font-semibold tracking-tight">
35
+ Something went wrong
36
+ </h1>
37
+ <p className="max-w-sm text-muted-foreground">
38
+ An unexpected error occurred. Try again, or head back home.
39
+ {error.digest && (
40
+ <span className="mt-1 block font-mono text-xs">
41
+ Ref: {error.digest}
42
+ </span>
43
+ )}
44
+ </p>
45
+ <div className="flex items-center gap-2">
46
+ <Button variant="primary" onClick={reset}>
47
+ <Reload className="size-4" />
48
+ Try again
49
+ </Button>
50
+ <Link href="/dashboard">
51
+ <Button variant="outline">Back to Home</Button>
52
+ </Link>
53
+ </div>
54
+ </main>
55
+ );
56
+ }
@@ -0,0 +1,6 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ /* Shared VUI Starter design system (tokens, theme mapping, base reset).
5
+ Package path (not a relative one) so it survives folder moves. */
6
+ @import "@viliha/vui-ui/theme.css";
Binary file
@@ -0,0 +1,4 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="24" height="24" rx="6" fill="#0866ff" />
3
+ <path d="M6.5 7.25 L12 16.75 L17.5 7.25" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" />
4
+ </svg>
@@ -0,0 +1,103 @@
1
+ import type { Metadata } from "next";
2
+ import { Inter, JetBrains_Mono } from "next/font/google";
3
+
4
+ import "./globals.css";
5
+ import { themeInitScript } from "./_components/theme-toggle";
6
+ import { SITE } from "@/lib/seo";
7
+
8
+ const inter = Inter({
9
+ variable: "--font-inter",
10
+ subsets: ["latin"],
11
+ });
12
+
13
+ const jetbrainsMono = JetBrains_Mono({
14
+ variable: "--font-jetbrains-mono",
15
+ subsets: ["latin"],
16
+ });
17
+
18
+ export const metadata: Metadata = {
19
+ metadataBase: new URL(SITE.url),
20
+ title: {
21
+ default: `${SITE.name} — ${SITE.tagline}`,
22
+ template: `%s · ${SITE.name}`,
23
+ },
24
+ description: SITE.description,
25
+ applicationName: SITE.name,
26
+ authors: [{ name: SITE.author }],
27
+ creator: SITE.author,
28
+ keywords: [
29
+ "react admin template",
30
+ "admin dashboard",
31
+ "crm",
32
+ "design system",
33
+ "component library",
34
+ "shadcn",
35
+ "next.js",
36
+ "tailwind css",
37
+ "open source",
38
+ ],
39
+ openGraph: {
40
+ type: "website",
41
+ siteName: SITE.name,
42
+ url: SITE.url,
43
+ title: `${SITE.name} — ${SITE.tagline}`,
44
+ description: SITE.description,
45
+ images: [{ url: SITE.ogImage, alt: SITE.name }],
46
+ },
47
+ twitter: {
48
+ card: "summary_large_image",
49
+ title: `${SITE.name} — ${SITE.tagline}`,
50
+ description: SITE.description,
51
+ images: [SITE.ogImage],
52
+ },
53
+ robots: { index: true, follow: true },
54
+ alternates: { canonical: "/" },
55
+ };
56
+
57
+ /** Structured data (schema.org). Indexable pages set their own canonical, so
58
+ * this describes the site + product once, at the root. */
59
+ const jsonLd = {
60
+ "@context": "https://schema.org",
61
+ "@graph": [
62
+ {
63
+ "@type": "WebSite",
64
+ name: SITE.name,
65
+ url: SITE.url,
66
+ description: SITE.description,
67
+ },
68
+ {
69
+ "@type": "SoftwareApplication",
70
+ name: SITE.name,
71
+ applicationCategory: "DeveloperApplication",
72
+ operatingSystem: "Web",
73
+ description: SITE.description,
74
+ url: SITE.url,
75
+ author: { "@type": "Person", name: SITE.author },
76
+ isAccessibleForFree: true,
77
+ offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
78
+ },
79
+ ],
80
+ };
81
+
82
+ export default function RootLayout({
83
+ children,
84
+ }: Readonly<{
85
+ children: React.ReactNode;
86
+ }>) {
87
+ return (
88
+ <html lang="en" data-scroll-behavior="smooth" suppressHydrationWarning>
89
+ <head>
90
+ <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
91
+ <script
92
+ type="application/ld+json"
93
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
94
+ />
95
+ </head>
96
+ <body
97
+ className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased`}
98
+ >
99
+ {children}
100
+ </body>
101
+ </html>
102
+ );
103
+ }
@@ -0,0 +1,29 @@
1
+ import Link from "next/link";
2
+ import { MagnifyingGlassIcon as Search } from "@radix-ui/react-icons";
3
+
4
+ import { Button } from "@viliha/vui-ui/button";
5
+
6
+ export default function NotFound() {
7
+ return (
8
+ <main className="flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center">
9
+ <div className="grid size-14 place-items-center rounded-full bg-muted text-muted-foreground">
10
+ <Search className="size-7" />
11
+ </div>
12
+ <p className="text-6xl font-semibold tracking-tight text-muted-foreground">
13
+ 404
14
+ </p>
15
+ <h1 className="text-2xl font-semibold tracking-tight">Page not found</h1>
16
+ <p className="max-w-sm text-muted-foreground">
17
+ The page you’re looking for doesn’t exist or may have moved.
18
+ </p>
19
+ <div className="flex items-center gap-2">
20
+ <Link href="/dashboard">
21
+ <Button variant="primary">Back to Home</Button>
22
+ </Link>
23
+ <Link href="/auth/signin">
24
+ <Button variant="outline">Sign in</Button>
25
+ </Link>
26
+ </div>
27
+ </main>
28
+ );
29
+ }
@@ -0,0 +1,31 @@
1
+ import type { Metadata } from "next";
2
+
3
+ import { Logo } from "@/app/_components/logo";
4
+ import { ThemeToggle } from "@/app/_components/theme-toggle";
5
+
6
+ // Onboarding is a post-signup flow — keep it out of the index.
7
+ export const metadata: Metadata = { robots: { index: false, follow: false } };
8
+
9
+ export default function OnboardingLayout({
10
+ children,
11
+ }: Readonly<{
12
+ children: React.ReactNode;
13
+ }>) {
14
+ return (
15
+ <div className="flex min-h-screen flex-col bg-muted/30">
16
+ <header className="flex items-center justify-between border-b border-border bg-background px-4 py-2.5">
17
+ <div className="flex items-center gap-2">
18
+ <Logo className="h-7 w-7" />
19
+ <span className="bg-gradient-to-r from-brand-indigo to-brand-violet bg-clip-text text-sm font-bold tracking-tight text-transparent">
20
+ Vui Starter
21
+ </span>
22
+ </div>
23
+ <div className="flex items-center gap-3 text-muted-foreground">
24
+ <span>Set up your workspace</span>
25
+ <ThemeToggle />
26
+ </div>
27
+ </header>
28
+ <main className="flex flex-1 justify-center px-4 py-8">{children}</main>
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,399 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import {
6
+ ArrowLeftIcon as ArrowLeft,
7
+ CheckIcon as Check,
8
+ CopyIcon as Copy,
9
+ CubeIcon as Building2,
10
+ LockClosedIcon as KeyRound,
11
+ PersonIcon as UserRound,
12
+ PersonIcon as Users,
13
+ PlusIcon as Plus,
14
+ TrashIcon as Trash2,
15
+ UploadIcon as Upload,
16
+ } from "@radix-ui/react-icons";
17
+
18
+ import { cn } from "@/lib/utils";
19
+ import { Badge } from "@viliha/vui-ui/badge";
20
+ import { Button } from "@viliha/vui-ui/button";
21
+ import { Input } from "@viliha/vui-ui/input";
22
+ import { Select } from "@viliha/vui-ui/select";
23
+ import { Field } from "@/app/_components/auth";
24
+ import { PLANS, TEAM_ROLES, TIMEZONES } from "@/lib/auth-demo";
25
+
26
+ const STEPS = [
27
+ { key: "profile", label: "Profile", icon: UserRound },
28
+ { key: "company", label: "Company", icon: Building2 },
29
+ { key: "token", label: "API token", icon: KeyRound },
30
+ { key: "plan", label: "Plan", icon: Check },
31
+ { key: "team", label: "Team", icon: Users },
32
+ ] as const;
33
+
34
+ export default function OnboardingPage() {
35
+ const router = useRouter();
36
+ const [step, setStep] = React.useState(0);
37
+
38
+ const [profile, setProfile] = React.useState({
39
+ firstName: "",
40
+ lastName: "",
41
+ email: "you@company.com",
42
+ });
43
+ const [company, setCompany] = React.useState({ name: "", timezone: "UTC" });
44
+ const [token, setToken] = React.useState<string | null>(null);
45
+ const [copied, setCopied] = React.useState(false);
46
+ const [plan, setPlan] = React.useState("growth");
47
+ const [invites, setInvites] = React.useState<
48
+ { email: string; role: string }[]
49
+ >([{ email: "", role: "Member" }]);
50
+
51
+ const last = STEPS.length - 1;
52
+ const next = () => (step === last ? finish() : setStep((s) => s + 1));
53
+ const back = () => setStep((s) => Math.max(0, s - 1));
54
+ function finish() {
55
+ router.push("/dashboard");
56
+ }
57
+
58
+ function generateToken() {
59
+ const rand =
60
+ Math.random().toString(36).slice(2, 12) +
61
+ Math.random().toString(36).slice(2, 12);
62
+ setToken(`vui_sk_${rand}`);
63
+ }
64
+ async function copyToken() {
65
+ if (!token) return;
66
+ try {
67
+ await navigator.clipboard.writeText(token);
68
+ setCopied(true);
69
+ window.setTimeout(() => setCopied(false), 1200);
70
+ } catch {
71
+ // clipboard unavailable — no-op
72
+ }
73
+ }
74
+
75
+ const canContinue =
76
+ step === 0
77
+ ? profile.firstName.trim() !== "" && profile.lastName.trim() !== ""
78
+ : step === 1
79
+ ? company.name.trim() !== ""
80
+ : true;
81
+ const skippable = step === 2 || step === 4; // API token, Team
82
+
83
+ return (
84
+ <div className="w-full max-w-xl">
85
+ <Stepper current={step} />
86
+
87
+ <div className="mt-6 rounded-xl border border-border bg-background p-6 shadow-sm">
88
+ {step === 0 && (
89
+ <Step
90
+ title="Set up your profile"
91
+ >
92
+ <div className="grid grid-cols-2 gap-3">
93
+ <Field label="First name" htmlFor="fn">
94
+ <Input
95
+ id="fn"
96
+ value={profile.firstName}
97
+ onChange={(e) =>
98
+ setProfile((p) => ({ ...p, firstName: e.target.value }))
99
+ }
100
+ placeholder="Ada"
101
+ />
102
+ </Field>
103
+ <Field label="Last name" htmlFor="ln">
104
+ <Input
105
+ id="ln"
106
+ value={profile.lastName}
107
+ onChange={(e) =>
108
+ setProfile((p) => ({ ...p, lastName: e.target.value }))
109
+ }
110
+ placeholder="Lovelace"
111
+ />
112
+ </Field>
113
+ </div>
114
+ <Field label="Admin email" htmlFor="email">
115
+ <Input
116
+ id="email"
117
+ type="email"
118
+ value={profile.email}
119
+ readOnly
120
+ disabled
121
+ className="cursor-not-allowed"
122
+ />
123
+ </Field>
124
+ </Step>
125
+ )}
126
+
127
+ {step === 1 && (
128
+ <Step
129
+ title="Company information"
130
+ >
131
+ <Field label="Company name" htmlFor="co">
132
+ <Input
133
+ id="co"
134
+ value={company.name}
135
+ onChange={(e) =>
136
+ setCompany((c) => ({ ...c, name: e.target.value }))
137
+ }
138
+ placeholder="Acme Inc."
139
+ />
140
+ </Field>
141
+ <div className="space-y-1">
142
+ <span className="font-medium">Logo</span>
143
+ <button
144
+ type="button"
145
+ className="flex w-full items-center gap-3 rounded-md border border-dashed border-input px-3 py-2.5 text-left text-muted-foreground hover:bg-muted/50"
146
+ >
147
+ <span className="grid size-8 place-items-center rounded-md bg-muted">
148
+ <Upload className="size-4" />
149
+ </span>
150
+ Upload a logo (optional)
151
+ </button>
152
+ </div>
153
+ <Field label="Timezone" htmlFor="tz">
154
+ <Select
155
+ id="tz"
156
+ ariaLabel="Timezone"
157
+ value={company.timezone}
158
+ onValueChange={(v) =>
159
+ setCompany((c) => ({ ...c, timezone: v }))
160
+ }
161
+ options={TIMEZONES.map((t) => ({ value: t, label: t }))}
162
+ />
163
+ </Field>
164
+ </Step>
165
+ )}
166
+
167
+ {step === 2 && (
168
+ <Step title="Generate your API token">
169
+ <div className="rounded-md border border-border p-3">
170
+ {!token ? (
171
+ <div className="flex items-center justify-between gap-3">
172
+ <span className="font-medium">API token</span>
173
+ <Button size="sm" variant="outline" onClick={generateToken}>
174
+ <KeyRound className="size-3.5" />
175
+ Generate
176
+ </Button>
177
+ </div>
178
+ ) : (
179
+ <div className="flex items-center gap-2">
180
+ <code className="min-w-0 flex-1 truncate rounded bg-muted px-2 py-1 font-mono">
181
+ {token}
182
+ </code>
183
+ <button
184
+ type="button"
185
+ onClick={copyToken}
186
+ aria-label="Copy API token"
187
+ className="grid size-7 shrink-0 place-items-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground"
188
+ >
189
+ {copied ? (
190
+ <Check className="size-3.5 text-emerald-600" />
191
+ ) : (
192
+ <Copy className="size-3.5" />
193
+ )}
194
+ </button>
195
+ </div>
196
+ )}
197
+ </div>
198
+ </Step>
199
+ )}
200
+
201
+ {step === 3 && (
202
+ <Step
203
+ title="Choose your plan"
204
+ >
205
+ <div className="space-y-2">
206
+ {PLANS.map((p) => {
207
+ const selected = plan === p.id;
208
+ return (
209
+ <button
210
+ key={p.id}
211
+ type="button"
212
+ onClick={() => setPlan(p.id)}
213
+ aria-pressed={selected}
214
+ className={cn(
215
+ "w-full rounded-lg border p-3 text-left transition-colors",
216
+ selected
217
+ ? "border-primary bg-primary/5 ring-1 ring-primary"
218
+ : "border-border hover:bg-muted/50",
219
+ )}
220
+ >
221
+ <div className="flex items-center justify-between">
222
+ <div className="flex items-center gap-2">
223
+ <span className="text-sm font-semibold">
224
+ {p.name}
225
+ </span>
226
+ {p.popular && (
227
+ <Badge variant="default">Most popular</Badge>
228
+ )}
229
+ </div>
230
+ <span className="text-muted-foreground">
231
+ <span className="font-semibold text-foreground">
232
+ {p.price}
233
+ </span>{" "}
234
+ {p.cadence}
235
+ </span>
236
+ </div>
237
+ <ul className="mt-2 flex flex-wrap gap-x-4 gap-y-1">
238
+ {p.features.map((f) => (
239
+ <li
240
+ key={f}
241
+ className="flex items-center gap-1 text-muted-foreground"
242
+ >
243
+ <Check className="size-3.5 text-primary" />
244
+ {f}
245
+ </li>
246
+ ))}
247
+ </ul>
248
+ </button>
249
+ );
250
+ })}
251
+ </div>
252
+ </Step>
253
+ )}
254
+
255
+ {step === 4 && (
256
+ <Step
257
+ title="Invite your team"
258
+ >
259
+ <div className="space-y-2">
260
+ {invites.map((row, i) => (
261
+ <div key={i} className="flex items-center gap-2">
262
+ <Input
263
+ type="email"
264
+ value={row.email}
265
+ onChange={(e) =>
266
+ setInvites((rows) =>
267
+ rows.map((r, j) =>
268
+ j === i ? { ...r, email: e.target.value } : r,
269
+ ),
270
+ )
271
+ }
272
+ placeholder="teammate@company.com"
273
+ aria-label={`Invite email ${i + 1}`}
274
+ />
275
+ <Select
276
+ value={row.role}
277
+ onValueChange={(v) =>
278
+ setInvites((rows) =>
279
+ rows.map((r, j) => (j === i ? { ...r, role: v } : r)),
280
+ )
281
+ }
282
+ ariaLabel={`Invite role ${i + 1}`}
283
+ className="w-32 shrink-0"
284
+ options={TEAM_ROLES.map((r) => ({ value: r, label: r }))}
285
+ />
286
+ <button
287
+ type="button"
288
+ onClick={() =>
289
+ setInvites((rows) =>
290
+ rows.length > 1 ? rows.filter((_, j) => j !== i) : rows,
291
+ )
292
+ }
293
+ aria-label="Remove invite"
294
+ className="grid size-8 shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-destructive/10 hover:text-destructive disabled:opacity-40"
295
+ disabled={invites.length === 1}
296
+ >
297
+ <Trash2 className="size-4" />
298
+ </button>
299
+ </div>
300
+ ))}
301
+ </div>
302
+ <button
303
+ type="button"
304
+ onClick={() =>
305
+ setInvites((rows) => [...rows, { email: "", role: "Member" }])
306
+ }
307
+ className="mt-2 inline-flex items-center gap-1.5 font-medium text-primary hover:underline"
308
+ >
309
+ <Plus className="size-3.5" />
310
+ Add another
311
+ </button>
312
+ </Step>
313
+ )}
314
+
315
+ {/* Navigation */}
316
+ <div className="mt-6 flex items-center justify-between gap-2">
317
+ <Button
318
+ variant="ghost"
319
+ onClick={back}
320
+ disabled={step === 0}
321
+ className={cn(step === 0 && "invisible")}
322
+ >
323
+ <ArrowLeft className="size-4" />
324
+ Back
325
+ </Button>
326
+ <div className="flex items-center gap-2">
327
+ {skippable && (
328
+ <Button variant="ghost" onClick={next}>
329
+ Skip
330
+ </Button>
331
+ )}
332
+ <Button onClick={next} disabled={!canContinue}>
333
+ {step === last ? "Finish & enter workspace" : "Continue"}
334
+ </Button>
335
+ </div>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ );
340
+ }
341
+
342
+ function Stepper({ current }: { current: number }) {
343
+ return (
344
+ <ol className="flex items-center">
345
+ {STEPS.map((s, i) => {
346
+ const done = i < current;
347
+ const active = i === current;
348
+ const Icon = s.icon;
349
+ return (
350
+ <li key={s.key} className="flex flex-1 items-center last:flex-none">
351
+ <div className="flex flex-col items-center gap-1">
352
+ <span
353
+ className={cn(
354
+ "grid size-7 place-items-center rounded-full border transition-colors",
355
+ done && "border-primary bg-primary text-primary-foreground",
356
+ active && "border-primary text-primary",
357
+ !done && !active && "border-border text-muted-foreground",
358
+ )}
359
+ >
360
+ {done ? <Check className="size-4" /> : <Icon className="size-4" />}
361
+ </span>
362
+ <span
363
+ className={cn(
364
+ "hidden sm:block",
365
+ active ? "font-medium text-foreground" : "text-muted-foreground",
366
+ )}
367
+ >
368
+ {s.label}
369
+ </span>
370
+ </div>
371
+ {i < STEPS.length - 1 && (
372
+ <span
373
+ className={cn(
374
+ "mx-2 h-px flex-1 self-start mt-3.5",
375
+ i < current ? "bg-primary" : "bg-border",
376
+ )}
377
+ />
378
+ )}
379
+ </li>
380
+ );
381
+ })}
382
+ </ol>
383
+ );
384
+ }
385
+
386
+ function Step({
387
+ title,
388
+ children,
389
+ }: {
390
+ title: string;
391
+ children: React.ReactNode;
392
+ }) {
393
+ return (
394
+ <div>
395
+ <h1 className="text-base font-semibold tracking-tight">{title}</h1>
396
+ <div className="mt-4 space-y-3">{children}</div>
397
+ </div>
398
+ );
399
+ }
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { useRouter } from "next/navigation";
5
+
6
+ /** The app opens on the dashboard. Client redirect (works with static export). */
7
+ export default function RootPage() {
8
+ const router = useRouter();
9
+ React.useEffect(() => {
10
+ router.replace("/dashboard");
11
+ }, [router]);
12
+ return null;
13
+ }
@@ -0,0 +1,30 @@
1
+ import type { Metadata } from "next";
2
+
3
+ import { Logo } from "@/app/_components/logo";
4
+ import { ThemeToggle } from "@/app/_components/theme-toggle";
5
+
6
+ export const metadata: Metadata = {
7
+ title: "Register your business",
8
+ robots: { index: false, follow: false },
9
+ };
10
+
11
+ export default function RegisterBusinessLayout({
12
+ children,
13
+ }: Readonly<{
14
+ children: React.ReactNode;
15
+ }>) {
16
+ return (
17
+ <div className="flex min-h-screen flex-col bg-muted/30">
18
+ <header className="flex items-center justify-between border-b border-border bg-background px-4 py-2.5">
19
+ <div className="flex items-center gap-2">
20
+ <Logo className="h-7 w-7" />
21
+ <span className="bg-gradient-to-r from-brand-indigo to-brand-violet bg-clip-text text-sm font-bold tracking-tight text-transparent">
22
+ Vui Starter
23
+ </span>
24
+ </div>
25
+ <ThemeToggle />
26
+ </header>
27
+ <main className="flex flex-1 justify-center px-4 py-8">{children}</main>
28
+ </div>
29
+ );
30
+ }