@viliha/vui-ui 1.1.8 → 1.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 (138) 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/src/record-view.tsx +59 -17
  6. package/template/.env.example +20 -0
  7. package/template/app/(app)/branches/branches-table.tsx +69 -0
  8. package/template/app/(app)/branches/page.tsx +13 -0
  9. package/template/app/(app)/businesses/businesses-table.tsx +39 -0
  10. package/template/app/(app)/businesses/page.tsx +13 -0
  11. package/template/app/(app)/calendar/page.tsx +725 -0
  12. package/template/app/(app)/charts/charts-content.tsx +190 -0
  13. package/template/app/(app)/charts/layout.tsx +8 -0
  14. package/template/app/(app)/charts/page.tsx +44 -0
  15. package/template/app/(app)/chat/layout.tsx +14 -0
  16. package/template/app/(app)/chat/page.tsx +311 -0
  17. package/template/app/(app)/components/layout.tsx +8 -0
  18. package/template/app/(app)/components/page.tsx +464 -0
  19. package/template/app/(app)/crm/companies/companies-table.tsx +56 -0
  20. package/template/app/(app)/crm/companies/page.tsx +13 -0
  21. package/template/app/(app)/crm/opportunities/opportunities-board.tsx +359 -0
  22. package/template/app/(app)/crm/opportunities/page.tsx +13 -0
  23. package/template/app/(app)/crm/people/page.tsx +13 -0
  24. package/template/app/(app)/crm/people/people-table.tsx +50 -0
  25. package/template/app/(app)/dashboard/layout.tsx +8 -0
  26. package/template/app/(app)/dashboard/page.tsx +239 -0
  27. package/template/app/(app)/departments/departments-table.tsx +55 -0
  28. package/template/app/(app)/departments/page.tsx +13 -0
  29. package/template/app/(app)/employees/employees-table.tsx +64 -0
  30. package/template/app/(app)/employees/page.tsx +13 -0
  31. package/template/app/(app)/forms/layout.tsx +8 -0
  32. package/template/app/(app)/forms/page.tsx +388 -0
  33. package/template/app/(app)/layout.tsx +74 -0
  34. package/template/app/(app)/markets/markets-table.tsx +76 -0
  35. package/template/app/(app)/markets/page.tsx +13 -0
  36. package/template/app/(app)/organizations/edit/page.tsx +72 -0
  37. package/template/app/(app)/organizations/new/page.tsx +46 -0
  38. package/template/app/(app)/organizations/organizations-config.tsx +97 -0
  39. package/template/app/(app)/organizations/organizations-table.tsx +42 -0
  40. package/template/app/(app)/organizations/page.tsx +13 -0
  41. package/template/app/(app)/settings/layout.tsx +8 -0
  42. package/template/app/(app)/settings/page.tsx +255 -0
  43. package/template/app/(app)/steps/page.tsx +263 -0
  44. package/template/app/(app)/support/layout.tsx +14 -0
  45. package/template/app/(app)/support/page.tsx +345 -0
  46. package/template/app/(app)/system/cities/cities-table.tsx +34 -0
  47. package/template/app/(app)/system/cities/page.tsx +13 -0
  48. package/template/app/(app)/system/countries/countries-table.tsx +34 -0
  49. package/template/app/(app)/system/countries/page.tsx +13 -0
  50. package/template/app/(app)/system/currencies/currencies-table.tsx +34 -0
  51. package/template/app/(app)/system/currencies/page.tsx +13 -0
  52. package/template/app/(app)/system/languages/languages-table.tsx +32 -0
  53. package/template/app/(app)/system/languages/page.tsx +13 -0
  54. package/template/app/(app)/system/regions/page.tsx +13 -0
  55. package/template/app/(app)/system/regions/regions-table.tsx +32 -0
  56. package/template/app/(app)/users/page.tsx +13 -0
  57. package/template/app/(app)/users/users-table.tsx +97 -0
  58. package/template/app/_components/app-sidebar.tsx +565 -0
  59. package/template/app/_components/auth.tsx +184 -0
  60. package/template/app/_components/breadcrumbs.tsx +26 -0
  61. package/template/app/_components/global-search.tsx +209 -0
  62. package/template/app/_components/logo.tsx +49 -0
  63. package/template/app/_components/nav-config.ts +138 -0
  64. package/template/app/_components/open-tabs.tsx +530 -0
  65. package/template/app/_components/quick-actions.tsx +140 -0
  66. package/template/app/_components/route-meta.ts +137 -0
  67. package/template/app/_components/set-page-title.tsx +15 -0
  68. package/template/app/_components/showcase.tsx +29 -0
  69. package/template/app/_components/stat-card.tsx +56 -0
  70. package/template/app/_components/theme-toggle.tsx +46 -0
  71. package/template/app/_components/top-bar.tsx +109 -0
  72. package/template/app/_components/user-menu.tsx +109 -0
  73. package/template/app/_components/wordmark.tsx +45 -0
  74. package/template/app/apple-icon.png +0 -0
  75. package/template/app/auth/forgot-password/page.tsx +102 -0
  76. package/template/app/auth/layout.tsx +43 -0
  77. package/template/app/auth/page.tsx +13 -0
  78. package/template/app/auth/reset-password/page.tsx +108 -0
  79. package/template/app/auth/signin/page.tsx +238 -0
  80. package/template/app/auth/signup/page.tsx +159 -0
  81. package/template/app/auth/verify/page.tsx +86 -0
  82. package/template/app/error.tsx +56 -0
  83. package/template/app/globals.css +6 -0
  84. package/template/app/icon.png +0 -0
  85. package/template/app/icon.svg +4 -0
  86. package/template/app/layout.tsx +103 -0
  87. package/template/app/not-found.tsx +29 -0
  88. package/template/app/onboarding/layout.tsx +31 -0
  89. package/template/app/onboarding/page.tsx +399 -0
  90. package/template/app/page.tsx +13 -0
  91. package/template/app/register-business/layout.tsx +30 -0
  92. package/template/app/register-business/page.tsx +234 -0
  93. package/template/components/ui/accordion.tsx +66 -0
  94. package/template/components/ui/alert-dialog.tsx +196 -0
  95. package/template/components/ui/alert.tsx +66 -0
  96. package/template/components/ui/aspect-ratio.tsx +11 -0
  97. package/template/components/ui/avatar.tsx +109 -0
  98. package/template/components/ui/badge.tsx +48 -0
  99. package/template/components/ui/breadcrumb.tsx +109 -0
  100. package/template/components/ui/button.tsx +64 -0
  101. package/template/components/ui/calendar.tsx +220 -0
  102. package/template/components/ui/card.tsx +92 -0
  103. package/template/components/ui/checkbox.tsx +32 -0
  104. package/template/components/ui/collapsible.tsx +33 -0
  105. package/template/components/ui/command.tsx +184 -0
  106. package/template/components/ui/dialog.tsx +158 -0
  107. package/template/components/ui/dropdown-menu.tsx +257 -0
  108. package/template/components/ui/form.tsx +167 -0
  109. package/template/components/ui/hover-card.tsx +44 -0
  110. package/template/components/ui/input-otp.tsx +77 -0
  111. package/template/components/ui/input.tsx +21 -0
  112. package/template/components/ui/label.tsx +24 -0
  113. package/template/components/ui/popover.tsx +89 -0
  114. package/template/components/ui/progress.tsx +31 -0
  115. package/template/components/ui/radio-group.tsx +45 -0
  116. package/template/components/ui/scroll-area.tsx +58 -0
  117. package/template/components/ui/select.tsx +190 -0
  118. package/template/components/ui/separator.tsx +28 -0
  119. package/template/components/ui/sheet.tsx +143 -0
  120. package/template/components/ui/skeleton.tsx +13 -0
  121. package/template/components/ui/slider.tsx +63 -0
  122. package/template/components/ui/sonner.tsx +40 -0
  123. package/template/components/ui/switch.tsx +35 -0
  124. package/template/components/ui/table.tsx +116 -0
  125. package/template/components/ui/tabs.tsx +91 -0
  126. package/template/components/ui/textarea.tsx +18 -0
  127. package/template/components/ui/toggle-group.tsx +83 -0
  128. package/template/components/ui/toggle.tsx +47 -0
  129. package/template/components/ui/tooltip.tsx +57 -0
  130. package/template/lib/auth-demo.ts +94 -0
  131. package/template/lib/crm-data.ts +80 -0
  132. package/template/lib/demo-data.ts +267 -0
  133. package/template/lib/mock-data.ts +169 -0
  134. package/template/lib/org-store.ts +50 -0
  135. package/template/lib/seo.ts +188 -0
  136. package/template/lib/utils.ts +3 -0
  137. package/template/next.config.mjs +13 -0
  138. package/template/postcss.config.mjs +7 -0
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import {
4
+ CodeIcon as Hash,
5
+ CubeIcon as Building,
6
+ DashboardIcon as LayoutGrid,
7
+ PersonIcon as Users,
8
+ } from "@radix-ui/react-icons";
9
+
10
+ import { usePathname } from "next/navigation";
11
+
12
+ import { RecordView, type RecordField } from "@viliha/vui-ui/record-view";
13
+ import { departments, type Department } from "@/lib/mock-data";
14
+
15
+ const fields: RecordField<Department>[] = [
16
+ { key: "title", label: "Title", editable: true, required: true, group: "General", hideInTable: true },
17
+ { key: "organization", label: "Organization", icon: Building, editable: true, width: 200, group: "General" },
18
+ { key: "code", label: "Code", icon: Hash, editable: true, group: "General" },
19
+ {
20
+ key: "employees",
21
+ label: "Employees",
22
+ icon: Users,
23
+ // No explicit align — auto-aligns center (numeric). See RecordView.
24
+ group: "System",
25
+ render: (row) => (
26
+ <span className="tabular-nums">{row.employees.toLocaleString()}</span>
27
+ ),
28
+ },
29
+ ];
30
+
31
+ export function DepartmentsTable() {
32
+ const pathname = usePathname();
33
+ return (
34
+ <RecordView
35
+ persistKey={pathname}
36
+ title="Departments"
37
+ singular="Department"
38
+ icon={LayoutGrid}
39
+ fields={fields}
40
+ initialData={departments}
41
+ getPrimary={(row) => ({
42
+ title: row.title,
43
+ subtitle: row.organization,
44
+ initials: (row.code || row.title).slice(0, 2).toUpperCase(),
45
+ })}
46
+ makeEmptyRow={() => ({
47
+ id: Date.now(),
48
+ organization: "",
49
+ title: "",
50
+ code: "",
51
+ employees: 0,
52
+ })}
53
+ />
54
+ );
55
+ }
@@ -0,0 +1,13 @@
1
+ import { pageMeta } from "@/lib/seo";
2
+
3
+ import { DepartmentsTable } from "./departments-table";
4
+
5
+ export const metadata = pageMeta("/departments");
6
+
7
+ export default function DepartmentsPage() {
8
+ return (
9
+ <main className="h-full">
10
+ <DepartmentsTable />
11
+ </main>
12
+ );
13
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import {
4
+ CodeIcon as Hash,
5
+ CubeIcon as Building,
6
+ DotFilledIcon as CircleDot,
7
+ EnvelopeClosedIcon as Mail,
8
+ PersonIcon as Users,
9
+ Share2Icon as Network,
10
+ } from "@radix-ui/react-icons";
11
+
12
+ import { Badge } from "@viliha/vui-ui/badge";
13
+ import { RecordView, type RecordField } from "@viliha/vui-ui/record-view";
14
+ import { employees, type DemoEmployee } from "@/lib/demo-data";
15
+
16
+ const fields: RecordField<DemoEmployee>[] = [
17
+ { key: "firstName", label: "First name", editable: true, required: true, group: "General", hideInTable: true },
18
+ { key: "lastName", label: "Last name", editable: true, required: true, group: "General", hideInTable: true },
19
+ { key: "email", label: "Email", icon: Mail, editable: true, copyable: true, width: 240, group: "General" },
20
+ { key: "code", label: "Code", icon: Hash, editable: true, group: "Work" },
21
+ { key: "department", label: "Department", editable: true, group: "Work" },
22
+ { key: "branch", label: "Branch", icon: Network, editable: true, group: "Work" },
23
+ { key: "organization", label: "Organization", icon: Building, editable: true, width: 180, group: "Work" },
24
+ {
25
+ key: "isActive",
26
+ label: "Status",
27
+ icon: CircleDot,
28
+ group: "System",
29
+ render: (row) =>
30
+ row.isActive ? (
31
+ <Badge variant="success">Active</Badge>
32
+ ) : (
33
+ <Badge variant="muted">Inactive</Badge>
34
+ ),
35
+ },
36
+ ];
37
+
38
+ export function EmployeesTable() {
39
+ return (
40
+ <RecordView
41
+ title="Employees"
42
+ singular="Employee"
43
+ icon={Users}
44
+ fields={fields}
45
+ initialData={employees}
46
+ getPrimary={(row) => ({
47
+ title: `${row.firstName} ${row.lastName}`.trim(),
48
+ subtitle: row.email,
49
+ initials: `${row.firstName[0] ?? ""}${row.lastName[0] ?? ""}`.toUpperCase(),
50
+ })}
51
+ makeEmptyRow={() => ({
52
+ id: Date.now(),
53
+ code: "",
54
+ firstName: "",
55
+ lastName: "",
56
+ email: "",
57
+ department: "",
58
+ branch: "",
59
+ organization: "",
60
+ isActive: true,
61
+ })}
62
+ />
63
+ );
64
+ }
@@ -0,0 +1,13 @@
1
+ import { pageMeta } from "@/lib/seo";
2
+
3
+ import { EmployeesTable } from "./employees-table";
4
+
5
+ export const metadata = pageMeta("/employees");
6
+
7
+ export default function EmployeesPage() {
8
+ return (
9
+ <main className="h-full">
10
+ <EmployeesTable />
11
+ </main>
12
+ );
13
+ }
@@ -0,0 +1,8 @@
1
+ import { pageMeta } from "@/lib/seo";
2
+
3
+ // The page is a Client Component and can't export metadata, so it lives here.
4
+ export const metadata = pageMeta("/forms");
5
+
6
+ export default function Layout({ children }: { children: React.ReactNode }) {
7
+ return children;
8
+ }
@@ -0,0 +1,388 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { useForm } from "react-hook-form";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import * as z from "zod";
7
+ import { format } from "date-fns";
8
+ import { CalendarIcon, Check, ChevronsUpDown } from "lucide-react";
9
+ import { Pencil2Icon } from "@radix-ui/react-icons";
10
+ import { toast } from "sonner";
11
+
12
+ import { cn } from "@/lib/utils";
13
+ import { Breadcrumbs } from "@/app/_components/breadcrumbs";
14
+ import { SetPageTitle } from "@/app/_components/set-page-title";
15
+ import { Demo } from "@/app/_components/showcase";
16
+
17
+ import { Button } from "@/components/ui/button";
18
+ import { Calendar } from "@/components/ui/calendar";
19
+ import { Checkbox } from "@/components/ui/checkbox";
20
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
21
+ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
22
+ import { Input } from "@/components/ui/input";
23
+ import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
24
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
25
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
26
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
27
+ import { Slider } from "@/components/ui/slider";
28
+ import { Switch } from "@/components/ui/switch";
29
+ import { Textarea } from "@/components/ui/textarea";
30
+ import { Toaster } from "@/components/ui/sonner";
31
+
32
+ const COUNTRIES = [
33
+ { value: "us", label: "United States" },
34
+ { value: "gb", label: "United Kingdom" },
35
+ { value: "in", label: "India" },
36
+ { value: "de", label: "Germany" },
37
+ { value: "jp", label: "Japan" },
38
+ ];
39
+
40
+ const schema = z.object({
41
+ username: z.string().min(2, "At least 2 characters."),
42
+ email: z.string().email("Enter a valid email."),
43
+ bio: z.string().max(160, "160 characters max.").optional(),
44
+ role: z.string().min(1, "Pick a role."),
45
+ country: z.string().min(1, "Pick a country."),
46
+ dob: z.date({ message: "Pick a date." }),
47
+ plan: z.enum(["free", "pro", "team"], { message: "Choose a plan." }),
48
+ volume: z.number().min(0).max(100),
49
+ marketing: z.boolean(),
50
+ terms: z.boolean().refine((v) => v, "You must accept the terms."),
51
+ pin: z.string().length(6, "Enter all 6 digits."),
52
+ });
53
+
54
+ type FormValues = z.infer<typeof schema>;
55
+
56
+ export default function FormsPage() {
57
+ const [comboOpen, setComboOpen] = React.useState(false);
58
+
59
+ const form = useForm<FormValues>({
60
+ resolver: zodResolver(schema),
61
+ defaultValues: {
62
+ username: "",
63
+ email: "",
64
+ bio: "",
65
+ role: "",
66
+ country: "",
67
+ plan: undefined,
68
+ volume: 50,
69
+ marketing: true,
70
+ terms: false,
71
+ pin: "",
72
+ },
73
+ });
74
+
75
+ function onSubmit(values: FormValues) {
76
+ toast("Form submitted", {
77
+ description: (
78
+ <pre className="mt-1 max-h-48 overflow-auto rounded bg-muted p-2 text-xs">
79
+ {JSON.stringify(values, null, 2)}
80
+ </pre>
81
+ ),
82
+ });
83
+ }
84
+
85
+ return (
86
+ <div className="flex h-full flex-col">
87
+ <SetPageTitle title="Forms" icon={Pencil2Icon} />
88
+ <Toaster />
89
+
90
+ {/* Action header */}
91
+ <div className="flex h-12 shrink-0 items-center justify-between gap-3 border-b border-border px-4">
92
+ <Breadcrumbs />
93
+ <span className="hidden truncate text-muted-foreground md:block">
94
+ shadcn/ui Form — react-hook-form + Zod, validated inline.
95
+ </span>
96
+ </div>
97
+
98
+ {/* Content */}
99
+ <div className="min-h-0 flex-1 overflow-y-auto">
100
+ <div className="flex flex-col gap-4 p-4">
101
+ <div className="rounded-lg border border-border bg-muted/40 px-4 py-3 text-sm leading-relaxed text-muted-foreground">
102
+ One <code className="font-mono text-foreground">Form</code> wiring
103
+ every shadcn field type — input, textarea, select, combobox, date
104
+ picker, radio, checkbox, switch, slider and OTP — with{" "}
105
+ <strong className="text-foreground">Zod</strong> validation and
106
+ inline <code className="font-mono text-foreground">FormMessage</code>{" "}
107
+ errors. Submit with empty fields to see validation.
108
+ </div>
109
+
110
+ <Demo title="Everything form" desc="Fully validated, themed by tokens.">
111
+ <Form {...form}>
112
+ <form
113
+ onSubmit={form.handleSubmit(onSubmit)}
114
+ className="grid w-full grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-2"
115
+ >
116
+ <FormField
117
+ control={form.control}
118
+ name="username"
119
+ render={({ field }) => (
120
+ <FormItem>
121
+ <FormLabel>Username</FormLabel>
122
+ <FormControl>
123
+ <Input placeholder="ada" {...field} />
124
+ </FormControl>
125
+ <FormDescription>Your public display name.</FormDescription>
126
+ <FormMessage />
127
+ </FormItem>
128
+ )}
129
+ />
130
+
131
+ <FormField
132
+ control={form.control}
133
+ name="email"
134
+ render={({ field }) => (
135
+ <FormItem>
136
+ <FormLabel>Email</FormLabel>
137
+ <FormControl>
138
+ <Input type="email" placeholder="you@example.com" {...field} />
139
+ </FormControl>
140
+ <FormMessage />
141
+ </FormItem>
142
+ )}
143
+ />
144
+
145
+ <FormField
146
+ control={form.control}
147
+ name="bio"
148
+ render={({ field }) => (
149
+ <FormItem className="sm:col-span-2">
150
+ <FormLabel>Bio</FormLabel>
151
+ <FormControl>
152
+ <Textarea placeholder="Tell us about yourself" {...field} />
153
+ </FormControl>
154
+ <FormMessage />
155
+ </FormItem>
156
+ )}
157
+ />
158
+
159
+ <FormField
160
+ control={form.control}
161
+ name="role"
162
+ render={({ field }) => (
163
+ <FormItem>
164
+ <FormLabel>Role</FormLabel>
165
+ <Select onValueChange={field.onChange} value={field.value}>
166
+ <FormControl>
167
+ <SelectTrigger>
168
+ <SelectValue placeholder="Select a role" />
169
+ </SelectTrigger>
170
+ </FormControl>
171
+ <SelectContent>
172
+ <SelectItem value="administrator">Administrator</SelectItem>
173
+ <SelectItem value="manager">Manager</SelectItem>
174
+ <SelectItem value="member">Member</SelectItem>
175
+ </SelectContent>
176
+ </Select>
177
+ <FormMessage />
178
+ </FormItem>
179
+ )}
180
+ />
181
+
182
+ <FormField
183
+ control={form.control}
184
+ name="country"
185
+ render={({ field }) => (
186
+ <FormItem className="flex flex-col">
187
+ <FormLabel>Country</FormLabel>
188
+ <Popover open={comboOpen} onOpenChange={setComboOpen}>
189
+ <PopoverTrigger asChild>
190
+ <FormControl>
191
+ <Button
192
+ variant="outline"
193
+ role="combobox"
194
+ className={cn(
195
+ "justify-between",
196
+ !field.value && "text-muted-foreground",
197
+ )}
198
+ >
199
+ {field.value
200
+ ? COUNTRIES.find((c) => c.value === field.value)?.label
201
+ : "Select country"}
202
+ <ChevronsUpDown className="ml-2 size-4 opacity-50" />
203
+ </Button>
204
+ </FormControl>
205
+ </PopoverTrigger>
206
+ <PopoverContent className="w-[--radix-popover-trigger-width] p-0">
207
+ <Command>
208
+ <CommandInput placeholder="Search country…" />
209
+ <CommandList>
210
+ <CommandEmpty>No country found.</CommandEmpty>
211
+ <CommandGroup>
212
+ {COUNTRIES.map((c) => (
213
+ <CommandItem
214
+ key={c.value}
215
+ value={c.label}
216
+ onSelect={() => {
217
+ form.setValue("country", c.value, {
218
+ shouldValidate: true,
219
+ });
220
+ setComboOpen(false);
221
+ }}
222
+ >
223
+ <Check
224
+ className={cn(
225
+ "mr-2 size-4",
226
+ field.value === c.value
227
+ ? "opacity-100"
228
+ : "opacity-0",
229
+ )}
230
+ />
231
+ {c.label}
232
+ </CommandItem>
233
+ ))}
234
+ </CommandGroup>
235
+ </CommandList>
236
+ </Command>
237
+ </PopoverContent>
238
+ </Popover>
239
+ <FormDescription>Searchable combobox.</FormDescription>
240
+ <FormMessage />
241
+ </FormItem>
242
+ )}
243
+ />
244
+
245
+ <FormField
246
+ control={form.control}
247
+ name="dob"
248
+ render={({ field }) => (
249
+ <FormItem className="flex flex-col">
250
+ <FormLabel>Date of birth</FormLabel>
251
+ <Popover>
252
+ <PopoverTrigger asChild>
253
+ <FormControl>
254
+ <Button
255
+ variant="outline"
256
+ className={cn(
257
+ "justify-start text-left font-normal",
258
+ !field.value && "text-muted-foreground",
259
+ )}
260
+ >
261
+ <CalendarIcon className="mr-2 size-4" />
262
+ {field.value ? format(field.value, "PPP") : "Pick a date"}
263
+ </Button>
264
+ </FormControl>
265
+ </PopoverTrigger>
266
+ <PopoverContent className="w-auto p-0" align="start">
267
+ <Calendar
268
+ mode="single"
269
+ selected={field.value}
270
+ onSelect={field.onChange}
271
+ captionLayout="dropdown"
272
+ />
273
+ </PopoverContent>
274
+ </Popover>
275
+ <FormMessage />
276
+ </FormItem>
277
+ )}
278
+ />
279
+
280
+ <FormField
281
+ control={form.control}
282
+ name="plan"
283
+ render={({ field }) => (
284
+ <FormItem>
285
+ <FormLabel>Plan</FormLabel>
286
+ <FormControl>
287
+ <RadioGroup
288
+ onValueChange={field.onChange}
289
+ value={field.value}
290
+ className="flex gap-6"
291
+ >
292
+ {["free", "pro", "team"].map((p) => (
293
+ <label key={p} className="flex items-center gap-2 text-sm capitalize">
294
+ <RadioGroupItem value={p} /> {p}
295
+ </label>
296
+ ))}
297
+ </RadioGroup>
298
+ </FormControl>
299
+ <FormMessage />
300
+ </FormItem>
301
+ )}
302
+ />
303
+
304
+ <FormField
305
+ control={form.control}
306
+ name="volume"
307
+ render={({ field }) => (
308
+ <FormItem className="sm:col-span-2">
309
+ <FormLabel>Volume — {field.value}</FormLabel>
310
+ <FormControl>
311
+ <Slider
312
+ value={[field.value]}
313
+ max={100}
314
+ step={1}
315
+ onValueChange={(v) => field.onChange(v[0])}
316
+ />
317
+ </FormControl>
318
+ <FormMessage />
319
+ </FormItem>
320
+ )}
321
+ />
322
+
323
+ <FormField
324
+ control={form.control}
325
+ name="pin"
326
+ render={({ field }) => (
327
+ <FormItem className="sm:col-span-2">
328
+ <FormLabel>Verification code</FormLabel>
329
+ <FormControl>
330
+ <InputOTP maxLength={6} value={field.value} onChange={field.onChange}>
331
+ <InputOTPGroup>
332
+ {Array.from({ length: 6 }, (_, i) => (
333
+ <InputOTPSlot key={i} index={i} />
334
+ ))}
335
+ </InputOTPGroup>
336
+ </InputOTP>
337
+ </FormControl>
338
+ <FormMessage />
339
+ </FormItem>
340
+ )}
341
+ />
342
+
343
+ <FormField
344
+ control={form.control}
345
+ name="marketing"
346
+ render={({ field }) => (
347
+ <FormItem className="flex items-center justify-between rounded-lg border border-border p-3 sm:col-span-2">
348
+ <div className="space-y-0.5">
349
+ <FormLabel>Marketing emails</FormLabel>
350
+ <FormDescription>Product news and offers.</FormDescription>
351
+ </div>
352
+ <FormControl>
353
+ <Switch checked={field.value} onCheckedChange={field.onChange} />
354
+ </FormControl>
355
+ </FormItem>
356
+ )}
357
+ />
358
+
359
+ <FormField
360
+ control={form.control}
361
+ name="terms"
362
+ render={({ field }) => (
363
+ <FormItem className="flex items-start gap-2 sm:col-span-2">
364
+ <FormControl>
365
+ <Checkbox checked={field.value} onCheckedChange={field.onChange} />
366
+ </FormControl>
367
+ <div className="space-y-1 leading-none">
368
+ <FormLabel>Accept terms and conditions</FormLabel>
369
+ <FormMessage />
370
+ </div>
371
+ </FormItem>
372
+ )}
373
+ />
374
+
375
+ <div className="flex gap-2 sm:col-span-2">
376
+ <Button type="submit">Submit</Button>
377
+ <Button type="button" variant="outline" onClick={() => form.reset()}>
378
+ Reset
379
+ </Button>
380
+ </div>
381
+ </form>
382
+ </Form>
383
+ </Demo>
384
+ </div>
385
+ </div>
386
+ </div>
387
+ );
388
+ }
@@ -0,0 +1,74 @@
1
+ import { PageChromeProvider } from "@viliha/vui-ui/record-view";
2
+ import { FOOTER_NOTICE, FOOTER_OVERRIDDEN, SITE } from "@/lib/seo";
3
+ import {
4
+ AppSidebar,
5
+ MobileNav,
6
+ SidebarProvider,
7
+ } from "@/app/_components/app-sidebar";
8
+ import { TopBar } from "@/app/_components/top-bar";
9
+ import { Breadcrumbs } from "@/app/_components/breadcrumbs";
10
+ import { QuickActionsProvider } from "@/app/_components/quick-actions";
11
+ import { GlobalSearchProvider } from "@/app/_components/global-search";
12
+ import {
13
+ KeepAliveTabs,
14
+ OpenTabsProvider,
15
+ TabStrip,
16
+ } from "@/app/_components/open-tabs";
17
+
18
+ export default function AppLayout({
19
+ children,
20
+ }: Readonly<{
21
+ children: React.ReactNode;
22
+ }>) {
23
+ return (
24
+ <SidebarProvider>
25
+ <QuickActionsProvider>
26
+ <GlobalSearchProvider>
27
+ <OpenTabsProvider>
28
+ <div className="flex h-screen overflow-hidden">
29
+ <AppSidebar />
30
+ <PageChromeProvider titleLeading={<Breadcrumbs />}>
31
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col">
32
+ <TopBar />
33
+ <TabStrip />
34
+ {/* Single scroll owner is each page's own inner content area; this
35
+ wrapper clips (overflow-hidden) and is `relative` so any
36
+ absolutely-positioned descendant (e.g. cmdk's hidden label) is
37
+ contained here instead of escaping to extend the document. */}
38
+ <div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden pb-14 md:pb-0">
39
+ {/* Keep-alive: open tabs stay mounted so switching is instant
40
+ (no remount/flash) and each page keeps its live state. */}
41
+ <KeepAliveTabs>{children}</KeepAliveTabs>
42
+ </div>
43
+ <footer className="hidden shrink-0 border-t border-border bg-background px-4 py-1 text-center text-[10px] text-muted-foreground md:block">
44
+ {FOOTER_OVERRIDDEN ? (
45
+ FOOTER_NOTICE
46
+ ) : (
47
+ <>
48
+ © {SITE.copyrightYear}{" "}
49
+ {SITE.companyUrl ? (
50
+ <a
51
+ href={SITE.companyUrl}
52
+ target="_blank"
53
+ rel="noopener noreferrer"
54
+ className="underline-offset-2 hover:text-foreground hover:underline"
55
+ >
56
+ {SITE.company}
57
+ </a>
58
+ ) : (
59
+ SITE.company
60
+ )}{" "}
61
+ · {SITE.license}
62
+ </>
63
+ )}
64
+ </footer>
65
+ </div>
66
+ </PageChromeProvider>
67
+ <MobileNav />
68
+ </div>
69
+ </OpenTabsProvider>
70
+ </GlobalSearchProvider>
71
+ </QuickActionsProvider>
72
+ </SidebarProvider>
73
+ );
74
+ }