@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,239 @@
1
+ "use client";
2
+
3
+ import {
4
+ CubeIcon as Building2,
5
+ HomeIcon as Home,
6
+ } from "@radix-ui/react-icons";
7
+
8
+ import { cn } from "@/lib/utils";
9
+ import { Avatar, AvatarFallback } from "@viliha/vui-ui/avatar";
10
+ import { Badge } from "@viliha/vui-ui/badge";
11
+ import {
12
+ Table,
13
+ TableBody,
14
+ TableCell,
15
+ TableHead,
16
+ TableHeader,
17
+ TableRow,
18
+ } from "@viliha/vui-ui/table";
19
+ import { Breadcrumbs } from "@/app/_components/breadcrumbs";
20
+ import { SetPageTitle } from "@/app/_components/set-page-title";
21
+ import { StatCard } from "@/app/_components/stat-card";
22
+ import {
23
+ employees,
24
+ organizations,
25
+ regionBreakdown,
26
+ stats,
27
+ type OrganizationStatus,
28
+ } from "@/lib/demo-data";
29
+
30
+ const statusBadge: Record<
31
+ OrganizationStatus,
32
+ { label: string; variant: "success" | "warning" | "destructive" }
33
+ > = {
34
+ active: { label: "Active", variant: "success" },
35
+ trial: { label: "Trial", variant: "warning" },
36
+ suspended: { label: "Suspended", variant: "destructive" },
37
+ };
38
+
39
+ const statCards = [
40
+ stats.organizations,
41
+ stats.employees,
42
+ stats.activeMarkets,
43
+ stats.branches,
44
+ ];
45
+
46
+ export default function HomePage() {
47
+ const maxRegion = Math.max(
48
+ ...regionBreakdown.map((entry) => entry.organizations),
49
+ 1,
50
+ );
51
+
52
+ return (
53
+ <div className="flex h-full flex-col">
54
+ <SetPageTitle title="Home" icon={Home} />
55
+
56
+ {/* Action header — breadcrumbs (left) + overview note (right) */}
57
+ <div className="flex h-12 shrink-0 items-center justify-between gap-3 border-b border-border px-4">
58
+ <Breadcrumbs />
59
+ <span className="hidden truncate text-muted-foreground md:block">
60
+ Overview of organizations, teams, and markets across the platform.
61
+ </span>
62
+ </div>
63
+
64
+ {/* Scrollable content — padded, card sections (matches the settings layout) */}
65
+ <div className="min-h-0 flex-1 overflow-y-auto">
66
+ <div className="flex flex-col gap-4 p-4">
67
+ {/* Stats — bordered cards */}
68
+ <section
69
+ aria-label="Key metrics"
70
+ className="grid grid-cols-2 gap-4 lg:grid-cols-4"
71
+ >
72
+ {statCards.map((stat) => (
73
+ <StatCard
74
+ key={stat.label}
75
+ label={stat.label}
76
+ value={stat.value}
77
+ delta={stat.delta}
78
+ trend={stat.trend}
79
+ className="rounded-lg border border-border bg-card"
80
+ />
81
+ ))}
82
+ </section>
83
+
84
+ {/* Content sections */}
85
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
86
+ <section className="overflow-hidden rounded-lg border border-border bg-card lg:col-span-2">
87
+ <SectionHeader>Recent organizations</SectionHeader>
88
+ {organizations.length === 0 ? (
89
+ <EmptyState />
90
+ ) : (
91
+ <div className="overflow-x-auto">
92
+ <Table>
93
+ <TableHeader>
94
+ <TableRow>
95
+ <TableHead>Organization</TableHead>
96
+ <TableHead>Country</TableHead>
97
+ <TableHead className="text-right">Branches</TableHead>
98
+ <TableHead className="text-right">Employees</TableHead>
99
+ <TableHead>Status</TableHead>
100
+ <TableHead className="text-right">Updated</TableHead>
101
+ </TableRow>
102
+ </TableHeader>
103
+ <TableBody>
104
+ {organizations.map((org) => {
105
+ const status = statusBadge[org.status];
106
+ return (
107
+ <TableRow key={org.id}>
108
+ <TableCell>
109
+ <div className="flex items-center gap-3">
110
+ <Avatar className="size-8">
111
+ <AvatarFallback>{org.initials}</AvatarFallback>
112
+ </Avatar>
113
+ <div className="min-w-0">
114
+ <p className="truncate">{org.name}</p>
115
+ <p className="truncate text-muted-foreground">
116
+ {org.url}
117
+ </p>
118
+ </div>
119
+ </div>
120
+ </TableCell>
121
+ <TableCell className="text-muted-foreground">
122
+ {org.country}
123
+ </TableCell>
124
+ <TableCell className="text-right tabular-nums">
125
+ {org.branches}
126
+ </TableCell>
127
+ <TableCell className="text-right tabular-nums">
128
+ {org.employees.toLocaleString()}
129
+ </TableCell>
130
+ <TableCell>
131
+ <Badge variant={status.variant}>
132
+ {status.label}
133
+ </Badge>
134
+ </TableCell>
135
+ <TableCell className="text-right text-muted-foreground">
136
+ {org.updated}
137
+ </TableCell>
138
+ </TableRow>
139
+ );
140
+ })}
141
+ </TableBody>
142
+ </Table>
143
+ </div>
144
+ )}
145
+ </section>
146
+
147
+ {/* Side column — separate boxes */}
148
+ <aside className="grid grid-cols-1 content-start gap-4">
149
+ {/* Region breakdown */}
150
+ <section className="overflow-hidden rounded-lg border border-border bg-card">
151
+ <SectionHeader>Organizations by region</SectionHeader>
152
+ <div className="space-y-3 p-4">
153
+ {regionBreakdown.map((entry) => (
154
+ <div key={entry.region} className="space-y-1">
155
+ <div className="flex items-center justify-between">
156
+ <span>{entry.region}</span>
157
+ <span className="tabular-nums text-muted-foreground">
158
+ {entry.organizations}
159
+ </span>
160
+ </div>
161
+ <div className="h-2 overflow-hidden rounded-full bg-muted">
162
+ <div
163
+ className="h-full rounded-full bg-primary"
164
+ style={{
165
+ width: `${(entry.organizations / maxRegion) * 100}%`,
166
+ }}
167
+ />
168
+ </div>
169
+ </div>
170
+ ))}
171
+ </div>
172
+ </section>
173
+
174
+ {/* Recently added employees */}
175
+ <section className="overflow-hidden rounded-lg border border-border bg-card">
176
+ <SectionHeader>New team members</SectionHeader>
177
+ <div className="divide-y divide-border">
178
+ {employees.slice(0, 5).map((person) => (
179
+ <div
180
+ key={person.id}
181
+ className="flex items-center gap-3 px-4 py-3"
182
+ >
183
+ <Avatar className="size-8">
184
+ <AvatarFallback>
185
+ {person.firstName[0]}
186
+ {person.lastName[0]}
187
+ </AvatarFallback>
188
+ </Avatar>
189
+ <div className="min-w-0 flex-1">
190
+ <p className="truncate">
191
+ {person.firstName} {person.lastName}
192
+ </p>
193
+ <p className="truncate text-muted-foreground">
194
+ {person.department} · {person.organization}
195
+ </p>
196
+ </div>
197
+ <span
198
+ className={cn(
199
+ "size-2 shrink-0 rounded-full",
200
+ person.isActive
201
+ ? "bg-emerald-500"
202
+ : "bg-muted-foreground/40",
203
+ )}
204
+ aria-label={person.isActive ? "Active" : "Inactive"}
205
+ />
206
+ </div>
207
+ ))}
208
+ </div>
209
+ </section>
210
+ </aside>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ );
216
+ }
217
+
218
+ /** Section header bar — same treatment as the datatable sub-toolbar. */
219
+ function SectionHeader({ children }: { children: React.ReactNode }) {
220
+ return (
221
+ <div className="border-b border-border bg-muted/40 px-4 py-2 font-medium">
222
+ {children}
223
+ </div>
224
+ );
225
+ }
226
+
227
+ function EmptyState() {
228
+ return (
229
+ <div className="flex flex-col items-center justify-center gap-2 py-12 text-center">
230
+ <div className="flex size-10 items-center justify-center rounded-full bg-muted">
231
+ <Building2 className="size-5 text-muted-foreground" aria-hidden="true" />
232
+ </div>
233
+ <p className="font-medium">No organizations yet</p>
234
+ <p className="text-muted-foreground">
235
+ New tenants will appear here once they are onboarded.
236
+ </p>
237
+ </div>
238
+ );
239
+ }
@@ -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
+ }