@m5kdev/web-ui 0.1.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 (127) hide show
  1. package/LICENSE +621 -0
  2. package/README.md +17 -0
  3. package/package.json +169 -0
  4. package/src/animations/card.motion.ts +9 -0
  5. package/src/components/AvatarUpload.tsx +133 -0
  6. package/src/components/Button.tsx +14 -0
  7. package/src/components/Calendar.css +684 -0
  8. package/src/components/Calendar.tsx +32 -0
  9. package/src/components/CardsSelect.tsx +155 -0
  10. package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
  11. package/src/components/ColorPicker.tsx +56 -0
  12. package/src/components/CopyButton.tsx +45 -0
  13. package/src/components/CropDialog.tsx +154 -0
  14. package/src/components/DialogProvider.tsx +105 -0
  15. package/src/components/ErrorFallback.tsx +17 -0
  16. package/src/components/FileDropzone.tsx +120 -0
  17. package/src/components/MultiSelectDropdown.tsx +233 -0
  18. package/src/components/Orb.tsx +288 -0
  19. package/src/components/PageAlert.tsx +121 -0
  20. package/src/components/SelectChips.tsx +40 -0
  21. package/src/components/SidebarItem.tsx +26 -0
  22. package/src/components/Steps.tsx +340 -0
  23. package/src/components/TablerIconPicker.tsx +4260 -0
  24. package/src/components/app-header.tsx +40 -0
  25. package/src/components/blur-card.tsx +132 -0
  26. package/src/components/features-section-demo-1.tsx +127 -0
  27. package/src/components/features-section-demo-2.tsx +102 -0
  28. package/src/components/features-section-demo-3.tsx +272 -0
  29. package/src/components/mode-toggle.tsx +31 -0
  30. package/src/components/nav-main.tsx +69 -0
  31. package/src/components/pricing-cards.tsx +133 -0
  32. package/src/components/shared/ButtonCopy.tsx +50 -0
  33. package/src/components/team-switcher.tsx +83 -0
  34. package/src/components/theme-provider.tsx +74 -0
  35. package/src/components/typewriter.tsx +90 -0
  36. package/src/components/ui/alert-dialog.tsx +133 -0
  37. package/src/components/ui/alert.tsx +60 -0
  38. package/src/components/ui/avatar.tsx +47 -0
  39. package/src/components/ui/badge.tsx +33 -0
  40. package/src/components/ui/bento-grid.tsx +54 -0
  41. package/src/components/ui/bento-grid2.tsx +66 -0
  42. package/src/components/ui/breadcrumb.tsx +101 -0
  43. package/src/components/ui/button.tsx +50 -0
  44. package/src/components/ui/card.tsx +55 -0
  45. package/src/components/ui/checkbox.tsx +26 -0
  46. package/src/components/ui/collapsible.tsx +9 -0
  47. package/src/components/ui/dialog.tsx +119 -0
  48. package/src/components/ui/dropdown-menu.tsx +186 -0
  49. package/src/components/ui/floating-navbar.tsx +78 -0
  50. package/src/components/ui/form.tsx +167 -0
  51. package/src/components/ui/image.tsx +55 -0
  52. package/src/components/ui/input.tsx +22 -0
  53. package/src/components/ui/label.tsx +19 -0
  54. package/src/components/ui/pagination.tsx +105 -0
  55. package/src/components/ui/progress.tsx +23 -0
  56. package/src/components/ui/resizable-navbar.tsx +260 -0
  57. package/src/components/ui/segment-control.tsx +143 -0
  58. package/src/components/ui/select.tsx +153 -0
  59. package/src/components/ui/separator.tsx +24 -0
  60. package/src/components/ui/sheet.tsx +121 -0
  61. package/src/components/ui/sidebar.tsx +736 -0
  62. package/src/components/ui/skeleton.tsx +7 -0
  63. package/src/components/ui/slider.tsx +23 -0
  64. package/src/components/ui/sonner.tsx +27 -0
  65. package/src/components/ui/spinner.tsx +45 -0
  66. package/src/components/ui/switch.tsx +27 -0
  67. package/src/components/ui/table.tsx +90 -0
  68. package/src/components/ui/tabs.tsx +52 -0
  69. package/src/components/ui/textarea.tsx +18 -0
  70. package/src/components/ui/timeline.tsx +95 -0
  71. package/src/components/ui/toast.tsx +126 -0
  72. package/src/components/ui/tooltip.tsx +55 -0
  73. package/src/components/ui/typewriter-effect.tsx +181 -0
  74. package/src/hooks/use-mobile.ts +19 -0
  75. package/src/hooks/useDialog.ts +25 -0
  76. package/src/icons/GoogleIcon.tsx +32 -0
  77. package/src/icons/LinkedInIcon.tsx +30 -0
  78. package/src/icons/MicrosoftIcon.tsx +21 -0
  79. package/src/lib/chatwoot.ts +51 -0
  80. package/src/lib/utils.ts +6 -0
  81. package/src/modules/app/components/AppLoader.tsx +9 -0
  82. package/src/modules/app/components/AppShell.tsx +21 -0
  83. package/src/modules/app/components/AppSidebar.tsx +26 -0
  84. package/src/modules/app/components/AppSidebarContent.tsx +73 -0
  85. package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
  86. package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
  87. package/src/modules/app/components/AppSidebarUser.tsx +128 -0
  88. package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
  89. package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
  90. package/src/modules/auth/components/AuthLayout.tsx +13 -0
  91. package/src/modules/auth/components/AuthProviders.tsx +105 -0
  92. package/src/modules/auth/components/AuthRouter.tsx +29 -0
  93. package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
  94. package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
  95. package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
  96. package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
  97. package/src/modules/auth/components/InviteFriends.tsx +273 -0
  98. package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
  99. package/src/modules/auth/components/LoginForm.tsx +104 -0
  100. package/src/modules/auth/components/LoginRoute.tsx +31 -0
  101. package/src/modules/auth/components/LogoutRoute.tsx +21 -0
  102. package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
  103. package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
  104. package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
  105. package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
  106. package/src/modules/auth/components/ProfileRoute.tsx +104 -0
  107. package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
  108. package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
  109. package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
  110. package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
  111. package/src/modules/auth/components/SignupRoute.tsx +53 -0
  112. package/src/modules/auth/components/UserPreferences.tsx +144 -0
  113. package/src/modules/auth/components/WaitlistCard.tsx +78 -0
  114. package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
  115. package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
  116. package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
  117. package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
  118. package/src/modules/billing/components/BillingRouter.tsx +20 -0
  119. package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
  120. package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
  121. package/src/modules/table/components/NuqsTable.tsx +396 -0
  122. package/src/modules/table/components/TableFiltering.tsx +520 -0
  123. package/src/modules/table/components/TablePagination.tsx +59 -0
  124. package/src/modules/table/components/table.types.ts +11 -0
  125. package/src/modules/table/filterTransformers.ts +323 -0
  126. package/src/types.ts +4 -0
  127. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,396 @@
1
+ import { Checkbox, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
2
+ import type { QueryFilters } from "@m5kdev/commons/modules/schemas/query.schema";
3
+ import type { FilterMethods } from "@m5kdev/commons/modules/table/filter.types";
4
+ import type { TableParams } from "@m5kdev/frontend/modules/table/hooks/useNuqsTable";
5
+ import type { ColumnDef } from "@tanstack/react-table";
6
+ import {
7
+ type ColumnOrderState,
8
+ flexRender,
9
+ getCoreRowModel,
10
+ getPaginationRowModel,
11
+ type Table as ReactTable,
12
+ useReactTable,
13
+ type VisibilityState,
14
+ } from "@tanstack/react-table";
15
+ import { ChevronDown, ChevronUp } from "lucide-react";
16
+ import { useEffect, useMemo, useState } from "react";
17
+ import { Button } from "#components/ui/button";
18
+ import {
19
+ Table,
20
+ TableBody,
21
+ TableCell,
22
+ TableHead,
23
+ TableHeader,
24
+ TableRow,
25
+ } from "#components/ui/table";
26
+ import { ColumnOrderAndVisibility } from "./ColumnOrderAndVisibility";
27
+ import { TableFiltering } from "./TableFiltering";
28
+ import { TablePagination } from "./TablePagination";
29
+ import type { ColumnDataType, ColumnItem } from "./table.types";
30
+
31
+ function getStorageKey(columnIds: string[]): string {
32
+ const sortedIds = [...columnIds].sort().join(",");
33
+ return `table-column-layout-${sortedIds}`;
34
+ }
35
+
36
+ function loadLayoutFromStorage(
37
+ columnIds: string[]
38
+ ): { order: string[]; visibility: Record<string, boolean> } | null {
39
+ if (typeof window === "undefined") return null;
40
+ try {
41
+ const key = getStorageKey(columnIds);
42
+ const stored = localStorage.getItem(key);
43
+ if (!stored) return null;
44
+ const parsed = JSON.parse(stored) as { order: string[]; visibility: Record<string, boolean> };
45
+ return parsed;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function saveLayoutToStorage(
52
+ columnIds: string[],
53
+ order: string[],
54
+ visibility: Record<string, boolean>
55
+ ): void {
56
+ if (typeof window === "undefined") return;
57
+ try {
58
+ const key = getStorageKey(columnIds);
59
+ localStorage.setItem(key, JSON.stringify({ order, visibility }));
60
+ } catch {
61
+ // Ignore storage errors
62
+ }
63
+ }
64
+
65
+ export type NuqsTableColumn<T> = ColumnDef<T> & {
66
+ visible?: boolean;
67
+ type?: ColumnDataType;
68
+ options?: { label: string; value: string }[];
69
+ endColumnId?: string;
70
+ };
71
+
72
+ type NuqsTableParams<T> = {
73
+ data: T[];
74
+ total?: number;
75
+ columns: NuqsTableColumn<T>[];
76
+ tableProps: TableParams;
77
+ singleFilter?: boolean;
78
+ filterMethods?: Partial<FilterMethods>;
79
+ };
80
+
81
+ function applyOrder(prev: ColumnItem[], nextOrder: string[]): ColumnItem[] {
82
+ const byId = new Map(prev.map((i) => [i.id, i]));
83
+ const ordered = nextOrder.map((id) => byId.get(id)).filter(Boolean) as ColumnItem[];
84
+ // append any newly added columns (if any appear later)
85
+ for (const item of prev) if (!nextOrder.includes(item.id)) ordered.push(item);
86
+ return ordered;
87
+ }
88
+
89
+ function applyVisibility(prev: ColumnItem[], visibility: VisibilityState): ColumnItem[] {
90
+ return prev.map((column) => ({
91
+ ...column,
92
+ visibility: visibility[String(column.id)] ?? column.visibility,
93
+ }));
94
+ }
95
+
96
+ export const NuqsTable = <T,>({
97
+ data,
98
+ total,
99
+ columns,
100
+ tableProps,
101
+ singleFilter = false,
102
+ filterMethods,
103
+ }: NuqsTableParams<T>) => {
104
+ const columnIds = useMemo(() => columns.map((col) => String(col.id)), [columns]);
105
+ console.log("data", data);
106
+ // const columnsWithId = useMemo(() => {
107
+ // const idColumn: NuqsTableColumn<T> = {
108
+ // id: "__row_id",
109
+ // accessorKey: "id",
110
+ // header: "",
111
+ // enableSorting: false,
112
+ // enableHiding: false,
113
+ // visible: false,
114
+ // };
115
+ // return [idColumn, ...columns];
116
+ // }, [columns]);
117
+
118
+ const initialLayout = useMemo(() => {
119
+ const defaultLayout = columns.map((column) => ({
120
+ id: String(column.id),
121
+ label: column.header as string,
122
+ visibility: column.visible !== undefined ? column.visible : true,
123
+ options: column.options ?? [],
124
+ type: column.type ?? undefined,
125
+ }));
126
+
127
+ const saved = loadLayoutFromStorage(columnIds);
128
+ if (!saved) return defaultLayout;
129
+
130
+ // Merge saved layout with default layout
131
+ const savedById = new Map<string, { order: number; visibility: boolean }>();
132
+ saved.order.forEach((id, index) => {
133
+ savedById.set(id, { order: index, visibility: saved.visibility[id] ?? true });
134
+ });
135
+
136
+ const merged: ColumnItem[] = [];
137
+ const processedIds = new Set<string>();
138
+
139
+ // Add columns in saved order
140
+ saved.order.forEach((id) => {
141
+ const defaultCol = defaultLayout.find((col) => col.id === id);
142
+ if (defaultCol) {
143
+ merged.push({
144
+ ...defaultCol,
145
+ visibility: saved.visibility[id] ?? defaultCol.visibility,
146
+ });
147
+ processedIds.add(id);
148
+ }
149
+ });
150
+
151
+ // Add any new columns that weren't in saved layout
152
+ defaultLayout.forEach((col) => {
153
+ if (!processedIds.has(col.id)) {
154
+ merged.push(col);
155
+ }
156
+ });
157
+
158
+ return merged;
159
+ }, [columns, columnIds]);
160
+
161
+ const [layout, setLayout] = useState<ColumnItem[]>(initialLayout);
162
+
163
+ // Sync layout when columns change
164
+ useEffect(() => {
165
+ setLayout(initialLayout);
166
+ }, [initialLayout]);
167
+
168
+ // Save to localStorage whenever layout changes
169
+ useEffect(() => {
170
+ const order = layout.map((col) => col.id);
171
+ const visibility = Object.fromEntries(layout.map((col) => [col.id, col.visibility]));
172
+ saveLayoutToStorage(columnIds, order, visibility);
173
+ }, [layout, columnIds]);
174
+
175
+ const columnOrder = useMemo(() => layout.map((column) => column.id), [layout]);
176
+ const columnVisibility = useMemo(
177
+ () => Object.fromEntries(layout.map((column) => [column.id, column.visibility])),
178
+ [layout]
179
+ );
180
+
181
+ const {
182
+ limit = 10,
183
+ page = 1,
184
+ sorting,
185
+ setSorting,
186
+ setPagination,
187
+ pagination,
188
+ rowSelection,
189
+ setRowSelection,
190
+ setFilters,
191
+ filters,
192
+ } = tableProps;
193
+
194
+ // Redirect back if we're on an empty page (past the last page)
195
+ useEffect(() => {
196
+ if (data.length === 0 && page > 1 && total !== undefined) {
197
+ setPagination?.({ pageIndex: page - 2, pageSize: limit });
198
+ }
199
+ }, [data.length, page, limit, total, setPagination]);
200
+
201
+ // Calculate pageCount from total if available, otherwise use heuristic
202
+ const pageCount =
203
+ total !== undefined
204
+ ? Math.ceil(total / limit) || 1
205
+ : data.length === limit
206
+ ? page + 1
207
+ : Math.max(page, Math.ceil(data.length / limit));
208
+
209
+ const table = useReactTable({
210
+ data,
211
+ columns,
212
+ getRowId: (row) => String((row as { id: string | number }).id),
213
+ manualSorting: true,
214
+ manualPagination: true,
215
+ state: { pagination, sorting, rowSelection, columnOrder, columnVisibility },
216
+ pageCount,
217
+ manualFiltering: true,
218
+ onSortingChange: setSorting,
219
+ onPaginationChange: setPagination,
220
+ onRowSelectionChange: setRowSelection,
221
+ getCoreRowModel: getCoreRowModel(),
222
+ getPaginationRowModel: getPaginationRowModel(),
223
+ onColumnOrderChange: (updater) => {
224
+ setLayout((prev) =>
225
+ applyOrder(prev, typeof updater === "function" ? updater(prev.map((i) => i.id)) : updater)
226
+ );
227
+ },
228
+ onColumnVisibilityChange: (updater) => {
229
+ setLayout((prev) =>
230
+ applyVisibility(
231
+ prev,
232
+ typeof updater === "function"
233
+ ? updater(Object.fromEntries(prev.map((i) => [i.id, i.visibility])))
234
+ : updater
235
+ )
236
+ );
237
+ },
238
+ }) as ReactTable<T>;
239
+
240
+ const onChangeOrder = (order: ColumnOrderState) => {
241
+ setLayout((prev) => applyOrder(prev, order));
242
+ };
243
+ const onChangeVisibility = (visibility: VisibilityState) => {
244
+ setLayout((prev) => applyVisibility(prev, visibility));
245
+ };
246
+
247
+ const onFiltersChange = (filters: QueryFilters) => {
248
+ setFilters?.(filters);
249
+ };
250
+
251
+ const filterableColumns = useMemo(() => {
252
+ const baseColumns = columns
253
+ .filter((column) => Boolean(column.type))
254
+ .map((column) => ({
255
+ id: String(column.id),
256
+ label: String(column.header),
257
+ type: column.type,
258
+ options: column.options ?? [],
259
+ endColumnId: column.endColumnId,
260
+ periodStartColumnId: null,
261
+ periodEndColumnId: null,
262
+ }));
263
+
264
+ // Add Period pseudo-columns for date columns with endColumnId
265
+ const periodColumns = columns
266
+ .filter((column) => column.type === "date" && column.endColumnId)
267
+ .map((column) => ({
268
+ id: `${String(column.id)}__period`,
269
+ label: "Period",
270
+ type: "date" as ColumnDataType,
271
+ options: [],
272
+ endColumnId: null,
273
+ periodStartColumnId: String(column.id),
274
+ periodEndColumnId: column.endColumnId,
275
+ }));
276
+
277
+ return [...baseColumns, ...periodColumns];
278
+ }, [columns]);
279
+
280
+ const [isFiltersOpen, setIsFiltersOpen] = useState(false);
281
+ const [isColumnsOpen, setIsColumnsOpen] = useState(false);
282
+
283
+ return (
284
+ <>
285
+ <div className="flex w-full items-center gap-2 justify-end">
286
+ <Popover
287
+ placement="bottom"
288
+ isOpen={isFiltersOpen}
289
+ onOpenChange={setIsFiltersOpen}
290
+ portalContainer={document.body}
291
+ >
292
+ <PopoverTrigger>
293
+ <Button variant="outline" size="sm">
294
+ <div className="flex items-center gap-2">
295
+ Filters
296
+ <ChevronDown className="h-4 w-4" />
297
+ </div>
298
+ </Button>
299
+ </PopoverTrigger>
300
+ <PopoverContent>
301
+ <TableFiltering
302
+ columns={filterableColumns}
303
+ onFiltersChange={onFiltersChange}
304
+ filters={filters ?? []}
305
+ onClose={() => setIsFiltersOpen(false)}
306
+ singleFilter={singleFilter}
307
+ filterMethods={filterMethods}
308
+ />
309
+ </PopoverContent>
310
+ </Popover>
311
+ <Popover
312
+ placement="bottom"
313
+ isOpen={isColumnsOpen}
314
+ onOpenChange={setIsColumnsOpen}
315
+ portalContainer={document.body}
316
+ >
317
+ <PopoverTrigger>
318
+ <Button variant="outline" size="sm">
319
+ <div className="flex items-center gap-2">
320
+ Columns
321
+ <ChevronDown className="h-4 w-4" />
322
+ </div>
323
+ </Button>
324
+ </PopoverTrigger>
325
+ <PopoverContent>
326
+ <ColumnOrderAndVisibility
327
+ layout={layout}
328
+ onChangeOrder={onChangeOrder}
329
+ onChangeVisibility={onChangeVisibility}
330
+ onClose={() => setIsColumnsOpen(false)}
331
+ />
332
+ </PopoverContent>
333
+ </Popover>
334
+ </div>
335
+ <Table>
336
+ <TableHeader>
337
+ <TableRow>
338
+ <TableHead>
339
+ <Checkbox
340
+ isSelected={table.getIsAllRowsSelected()}
341
+ onValueChange={(checked) => {
342
+ table.toggleAllRowsSelected(checked);
343
+ }}
344
+ />
345
+ </TableHead>
346
+ {table.getHeaderGroups()[0].headers.map((header) => (
347
+ <TableHead
348
+ onClick={
349
+ header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined
350
+ }
351
+ key={header.id}
352
+ >
353
+ {flexRender(header.column.columnDef.header, header.getContext())}
354
+ {header.column.getCanSort() && (
355
+ <>
356
+ {header.column.getIsSorted() === "asc" && (
357
+ <ChevronUp className="h-4 w-4 inline ml-1" />
358
+ )}
359
+ {header.column.getIsSorted() === "desc" && (
360
+ <ChevronDown className="h-4 w-4 inline ml-1" />
361
+ )}
362
+ </>
363
+ )}
364
+ </TableHead>
365
+ ))}
366
+ </TableRow>
367
+ </TableHeader>
368
+ <TableBody>
369
+ {table.getRowModel().rows.map((row) => (
370
+ <TableRow key={row.id}>
371
+ <TableCell>
372
+ <Checkbox
373
+ isSelected={row.getIsSelected()}
374
+ onValueChange={(checked) => {
375
+ row.toggleSelected(checked);
376
+ }}
377
+ />
378
+ </TableCell>
379
+ {row.getVisibleCells().map((cell) => (
380
+ <TableCell key={cell.id}>
381
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
382
+ </TableCell>
383
+ ))}
384
+ </TableRow>
385
+ ))}
386
+ </TableBody>
387
+ </Table>
388
+ <TablePagination
389
+ pageCount={pageCount}
390
+ page={page}
391
+ limit={limit}
392
+ setPagination={setPagination}
393
+ />
394
+ </>
395
+ );
396
+ };