@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.
- package/AGENT.md +79 -9
- package/README.md +60 -0
- package/bin/vui.mjs +211 -0
- package/package.json +10 -2
- package/src/record-view.tsx +59 -17
- package/template/.env.example +20 -0
- package/template/app/(app)/branches/branches-table.tsx +69 -0
- package/template/app/(app)/branches/page.tsx +13 -0
- package/template/app/(app)/businesses/businesses-table.tsx +39 -0
- package/template/app/(app)/businesses/page.tsx +13 -0
- package/template/app/(app)/calendar/page.tsx +725 -0
- package/template/app/(app)/charts/charts-content.tsx +190 -0
- package/template/app/(app)/charts/layout.tsx +8 -0
- package/template/app/(app)/charts/page.tsx +44 -0
- package/template/app/(app)/chat/layout.tsx +14 -0
- package/template/app/(app)/chat/page.tsx +311 -0
- package/template/app/(app)/components/layout.tsx +8 -0
- package/template/app/(app)/components/page.tsx +464 -0
- package/template/app/(app)/crm/companies/companies-table.tsx +56 -0
- package/template/app/(app)/crm/companies/page.tsx +13 -0
- package/template/app/(app)/crm/opportunities/opportunities-board.tsx +359 -0
- package/template/app/(app)/crm/opportunities/page.tsx +13 -0
- package/template/app/(app)/crm/people/page.tsx +13 -0
- package/template/app/(app)/crm/people/people-table.tsx +50 -0
- package/template/app/(app)/dashboard/layout.tsx +8 -0
- package/template/app/(app)/dashboard/page.tsx +239 -0
- package/template/app/(app)/departments/departments-table.tsx +55 -0
- package/template/app/(app)/departments/page.tsx +13 -0
- package/template/app/(app)/employees/employees-table.tsx +64 -0
- package/template/app/(app)/employees/page.tsx +13 -0
- package/template/app/(app)/forms/layout.tsx +8 -0
- package/template/app/(app)/forms/page.tsx +388 -0
- package/template/app/(app)/layout.tsx +74 -0
- package/template/app/(app)/markets/markets-table.tsx +76 -0
- package/template/app/(app)/markets/page.tsx +13 -0
- package/template/app/(app)/organizations/edit/page.tsx +72 -0
- package/template/app/(app)/organizations/new/page.tsx +46 -0
- package/template/app/(app)/organizations/organizations-config.tsx +97 -0
- package/template/app/(app)/organizations/organizations-table.tsx +42 -0
- package/template/app/(app)/organizations/page.tsx +13 -0
- package/template/app/(app)/settings/layout.tsx +8 -0
- package/template/app/(app)/settings/page.tsx +255 -0
- package/template/app/(app)/steps/page.tsx +263 -0
- package/template/app/(app)/support/layout.tsx +14 -0
- package/template/app/(app)/support/page.tsx +345 -0
- package/template/app/(app)/system/cities/cities-table.tsx +34 -0
- package/template/app/(app)/system/cities/page.tsx +13 -0
- package/template/app/(app)/system/countries/countries-table.tsx +34 -0
- package/template/app/(app)/system/countries/page.tsx +13 -0
- package/template/app/(app)/system/currencies/currencies-table.tsx +34 -0
- package/template/app/(app)/system/currencies/page.tsx +13 -0
- package/template/app/(app)/system/languages/languages-table.tsx +32 -0
- package/template/app/(app)/system/languages/page.tsx +13 -0
- package/template/app/(app)/system/regions/page.tsx +13 -0
- package/template/app/(app)/system/regions/regions-table.tsx +32 -0
- package/template/app/(app)/users/page.tsx +13 -0
- package/template/app/(app)/users/users-table.tsx +97 -0
- package/template/app/_components/app-sidebar.tsx +565 -0
- package/template/app/_components/auth.tsx +184 -0
- package/template/app/_components/breadcrumbs.tsx +26 -0
- package/template/app/_components/global-search.tsx +209 -0
- package/template/app/_components/logo.tsx +49 -0
- package/template/app/_components/nav-config.ts +138 -0
- package/template/app/_components/open-tabs.tsx +530 -0
- package/template/app/_components/quick-actions.tsx +140 -0
- package/template/app/_components/route-meta.ts +137 -0
- package/template/app/_components/set-page-title.tsx +15 -0
- package/template/app/_components/showcase.tsx +29 -0
- package/template/app/_components/stat-card.tsx +56 -0
- package/template/app/_components/theme-toggle.tsx +46 -0
- package/template/app/_components/top-bar.tsx +109 -0
- package/template/app/_components/user-menu.tsx +109 -0
- package/template/app/_components/wordmark.tsx +45 -0
- package/template/app/apple-icon.png +0 -0
- package/template/app/auth/forgot-password/page.tsx +102 -0
- package/template/app/auth/layout.tsx +43 -0
- package/template/app/auth/page.tsx +13 -0
- package/template/app/auth/reset-password/page.tsx +108 -0
- package/template/app/auth/signin/page.tsx +238 -0
- package/template/app/auth/signup/page.tsx +159 -0
- package/template/app/auth/verify/page.tsx +86 -0
- package/template/app/error.tsx +56 -0
- package/template/app/globals.css +6 -0
- package/template/app/icon.png +0 -0
- package/template/app/icon.svg +4 -0
- package/template/app/layout.tsx +103 -0
- package/template/app/not-found.tsx +29 -0
- package/template/app/onboarding/layout.tsx +31 -0
- package/template/app/onboarding/page.tsx +399 -0
- package/template/app/page.tsx +13 -0
- package/template/app/register-business/layout.tsx +30 -0
- package/template/app/register-business/page.tsx +234 -0
- package/template/components/ui/accordion.tsx +66 -0
- package/template/components/ui/alert-dialog.tsx +196 -0
- package/template/components/ui/alert.tsx +66 -0
- package/template/components/ui/aspect-ratio.tsx +11 -0
- package/template/components/ui/avatar.tsx +109 -0
- package/template/components/ui/badge.tsx +48 -0
- package/template/components/ui/breadcrumb.tsx +109 -0
- package/template/components/ui/button.tsx +64 -0
- package/template/components/ui/calendar.tsx +220 -0
- package/template/components/ui/card.tsx +92 -0
- package/template/components/ui/checkbox.tsx +32 -0
- package/template/components/ui/collapsible.tsx +33 -0
- package/template/components/ui/command.tsx +184 -0
- package/template/components/ui/dialog.tsx +158 -0
- package/template/components/ui/dropdown-menu.tsx +257 -0
- package/template/components/ui/form.tsx +167 -0
- package/template/components/ui/hover-card.tsx +44 -0
- package/template/components/ui/input-otp.tsx +77 -0
- package/template/components/ui/input.tsx +21 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/popover.tsx +89 -0
- package/template/components/ui/progress.tsx +31 -0
- package/template/components/ui/radio-group.tsx +45 -0
- package/template/components/ui/scroll-area.tsx +58 -0
- package/template/components/ui/select.tsx +190 -0
- package/template/components/ui/separator.tsx +28 -0
- package/template/components/ui/sheet.tsx +143 -0
- package/template/components/ui/skeleton.tsx +13 -0
- package/template/components/ui/slider.tsx +63 -0
- package/template/components/ui/sonner.tsx +40 -0
- package/template/components/ui/switch.tsx +35 -0
- package/template/components/ui/table.tsx +116 -0
- package/template/components/ui/tabs.tsx +91 -0
- package/template/components/ui/textarea.tsx +18 -0
- package/template/components/ui/toggle-group.tsx +83 -0
- package/template/components/ui/toggle.tsx +47 -0
- package/template/components/ui/tooltip.tsx +57 -0
- package/template/lib/auth-demo.ts +94 -0
- package/template/lib/crm-data.ts +80 -0
- package/template/lib/demo-data.ts +267 -0
- package/template/lib/mock-data.ts +169 -0
- package/template/lib/org-store.ts +50 -0
- package/template/lib/seo.ts +188 -0
- package/template/lib/utils.ts +3 -0
- package/template/next.config.mjs +13 -0
- 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
|
+
}
|