omnira-ui 0.2.0 → 0.3.1

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 (29) hide show
  1. package/cli/omnira-init.mjs +197 -16
  2. package/components/ui/ActivityGauge/ActivityGauge.module.css +109 -0
  3. package/components/ui/ActivityGauge/ActivityGauge.tsx +87 -0
  4. package/components/ui/ActivityGauge/index.ts +2 -0
  5. package/components/ui/Calendar/Calendar.module.css +492 -0
  6. package/components/ui/Calendar/Calendar.tsx +445 -0
  7. package/components/ui/Calendar/config.ts +130 -0
  8. package/components/ui/Calendar/index.ts +4 -0
  9. package/components/ui/CardHeader/CardHeader.module.css +79 -0
  10. package/components/ui/CardHeader/CardHeader.tsx +45 -0
  11. package/components/ui/CardHeader/index.ts +2 -0
  12. package/components/ui/EmptyState/EmptyState.module.css +65 -0
  13. package/components/ui/EmptyState/EmptyState.tsx +37 -0
  14. package/components/ui/EmptyState/index.ts +2 -0
  15. package/components/ui/Metric/Metric.module.css +140 -0
  16. package/components/ui/Metric/Metric.tsx +78 -0
  17. package/components/ui/Metric/index.ts +2 -0
  18. package/components/ui/PageHeader/PageHeader.module.css +128 -0
  19. package/components/ui/PageHeader/PageHeader.tsx +61 -0
  20. package/components/ui/PageHeader/index.ts +2 -0
  21. package/components/ui/Table/Table.module.css +444 -0
  22. package/components/ui/Table/Table.tsx +547 -0
  23. package/components/ui/Table/customers.json +74 -0
  24. package/components/ui/Table/index.ts +14 -0
  25. package/components/ui/Table/invoices.json +92 -0
  26. package/components/ui/Table/orders.json +108 -0
  27. package/components/ui/Table/team-members.json +130 -0
  28. package/components/ui/Table/uploaded-files.json +53 -0
  29. package/package.json +1 -1
@@ -0,0 +1,547 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ useRef,
8
+ useEffect,
9
+ useCallback,
10
+ useMemo,
11
+ forwardRef,
12
+ } from "react";
13
+ import { ArrowUp2, More, InfoCircle, Edit, Trash, ExportSquare, Copy } from "iconsax-react";
14
+ import { cn } from "@/lib/cn";
15
+ import styles from "./Table.module.css";
16
+
17
+ /* ══════════════════════════════════════════════
18
+ Types
19
+ ══════════════════════════════════════════════ */
20
+
21
+ export type SortDirection = "ascending" | "descending";
22
+
23
+ export interface SortDescriptor {
24
+ column: string;
25
+ direction: SortDirection;
26
+ }
27
+
28
+ /* ══════════════════════════════════════════════
29
+ Table Context
30
+ ══════════════════════════════════════════════ */
31
+
32
+ interface TableCtx {
33
+ sortDescriptor?: SortDescriptor;
34
+ onSortChange?: (descriptor: SortDescriptor) => void;
35
+ selectionMode?: "none" | "single" | "multiple";
36
+ selectedKeys: Set<string>;
37
+ toggleSelection: (key: string) => void;
38
+ toggleAll: () => void;
39
+ allKeys: string[];
40
+ registerKey: (key: string) => void;
41
+ unregisterKey: (key: string) => void;
42
+ }
43
+
44
+ const TableContext = createContext<TableCtx>({
45
+ selectedKeys: new Set(),
46
+ toggleSelection: () => {},
47
+ toggleAll: () => {},
48
+ allKeys: [],
49
+ registerKey: () => {},
50
+ unregisterKey: () => {},
51
+ });
52
+
53
+ /* ══════════════════════════════════════════════
54
+ TableCard — Card wrapper with header/footer
55
+ ══════════════════════════════════════════════ */
56
+
57
+ interface TableCardRootProps {
58
+ children: React.ReactNode;
59
+ size?: "sm" | "md";
60
+ className?: string;
61
+ }
62
+
63
+ function TableCardRoot({ children, size = "md", className }: TableCardRootProps) {
64
+ return (
65
+ <div className={cn(styles.cardRoot, size === "sm" && styles.cardRootSm, className)}>
66
+ {children}
67
+ </div>
68
+ );
69
+ }
70
+
71
+ interface TableCardHeaderProps {
72
+ title: string;
73
+ description?: string;
74
+ badge?: string;
75
+ contentTrailing?: React.ReactNode;
76
+ className?: string;
77
+ }
78
+
79
+ function TableCardHeader({ title, description, badge, contentTrailing, className }: TableCardHeaderProps) {
80
+ return (
81
+ <div className={cn(styles.cardHeader, className)}>
82
+ <div>
83
+ <h3 className={styles.cardTitle}>{title}</h3>
84
+ {description && <p className={styles.cardDescription}>{description}</p>}
85
+ </div>
86
+ {badge && <span className={styles.cardBadge}>{badge}</span>}
87
+ {contentTrailing}
88
+ </div>
89
+ );
90
+ }
91
+
92
+ export const TableCard = {
93
+ Root: TableCardRoot,
94
+ Header: TableCardHeader,
95
+ };
96
+
97
+ /* ══════════════════════════════════════════════
98
+ Table — Root table element
99
+ ══════════════════════════════════════════════ */
100
+
101
+ export interface TableProps {
102
+ children: React.ReactNode;
103
+ "aria-label"?: string;
104
+ selectionMode?: "none" | "single" | "multiple";
105
+ sortDescriptor?: SortDescriptor;
106
+ onSortChange?: (descriptor: SortDescriptor) => void;
107
+ selectedKeys?: Set<string>;
108
+ onSelectionChange?: (keys: Set<string>) => void;
109
+ className?: string;
110
+ }
111
+
112
+ export function Table({
113
+ children,
114
+ "aria-label": ariaLabel,
115
+ selectionMode = "none",
116
+ sortDescriptor,
117
+ onSortChange,
118
+ selectedKeys: controlledKeys,
119
+ onSelectionChange,
120
+ className,
121
+ }: TableProps) {
122
+ const [internalKeys, setInternalKeys] = useState<Set<string>>(new Set());
123
+ const [allKeys, setAllKeys] = useState<string[]>([]);
124
+
125
+ const selectedKeys = controlledKeys ?? internalKeys;
126
+
127
+ const setSelectedKeys = useCallback(
128
+ (keys: Set<string>) => {
129
+ if (!controlledKeys) setInternalKeys(keys);
130
+ onSelectionChange?.(keys);
131
+ },
132
+ [controlledKeys, onSelectionChange],
133
+ );
134
+
135
+ const toggleSelection = useCallback(
136
+ (key: string) => {
137
+ const next = new Set(selectedKeys);
138
+ if (selectionMode === "single") {
139
+ if (next.has(key)) {
140
+ next.clear();
141
+ } else {
142
+ next.clear();
143
+ next.add(key);
144
+ }
145
+ } else {
146
+ if (next.has(key)) next.delete(key);
147
+ else next.add(key);
148
+ }
149
+ setSelectedKeys(next);
150
+ },
151
+ [selectedKeys, selectionMode, setSelectedKeys],
152
+ );
153
+
154
+ const toggleAll = useCallback(() => {
155
+ if (selectedKeys.size === allKeys.length) {
156
+ setSelectedKeys(new Set());
157
+ } else {
158
+ setSelectedKeys(new Set(allKeys));
159
+ }
160
+ }, [selectedKeys, allKeys, setSelectedKeys]);
161
+
162
+ const registerKey = useCallback((key: string) => {
163
+ setAllKeys((prev) => (prev.includes(key) ? prev : [...prev, key]));
164
+ }, []);
165
+
166
+ const unregisterKey = useCallback((key: string) => {
167
+ setAllKeys((prev) => prev.filter((k) => k !== key));
168
+ }, []);
169
+
170
+ const ctx = useMemo<TableCtx>(
171
+ () => ({
172
+ sortDescriptor,
173
+ onSortChange,
174
+ selectionMode,
175
+ selectedKeys,
176
+ toggleSelection,
177
+ toggleAll,
178
+ allKeys,
179
+ registerKey,
180
+ unregisterKey,
181
+ }),
182
+ [sortDescriptor, onSortChange, selectionMode, selectedKeys, toggleSelection, toggleAll, allKeys, registerKey, unregisterKey],
183
+ );
184
+
185
+ return (
186
+ <TableContext.Provider value={ctx}>
187
+ <div className={styles.scrollWrap}>
188
+ <table className={cn(styles.table, className)} aria-label={ariaLabel}>
189
+ {children}
190
+ </table>
191
+ </div>
192
+ </TableContext.Provider>
193
+ );
194
+ }
195
+
196
+ /* ══════════════════════════════════════════════
197
+ Table.Header
198
+ ══════════════════════════════════════════════ */
199
+
200
+ interface TableHeaderProps {
201
+ children: React.ReactNode;
202
+ className?: string;
203
+ }
204
+
205
+ function TableHeader({ children, className }: TableHeaderProps) {
206
+ const { selectionMode, selectedKeys, allKeys, toggleAll } = useContext(TableContext);
207
+ const allSelected = allKeys.length > 0 && selectedKeys.size === allKeys.length;
208
+ const someSelected = selectedKeys.size > 0 && !allSelected;
209
+
210
+ return (
211
+ <thead className={cn(styles.thead, className)}>
212
+ <tr>
213
+ {selectionMode === "multiple" && (
214
+ <th className={styles.checkboxCell}>
215
+ <input
216
+ type="checkbox"
217
+ className={cn(
218
+ styles.headerCheckbox,
219
+ someSelected && styles.headerCheckboxIndeterminate,
220
+ )}
221
+ checked={allSelected}
222
+ onChange={toggleAll}
223
+ aria-label="Select all rows"
224
+ />
225
+ </th>
226
+ )}
227
+ {children}
228
+ </tr>
229
+ </thead>
230
+ );
231
+ }
232
+
233
+ /* ══════════════════════════════════════════════
234
+ Table.Head — Column header cell
235
+ ══════════════════════════════════════════════ */
236
+
237
+ interface TableHeadProps {
238
+ id?: string;
239
+ label?: string;
240
+ allowsSorting?: boolean;
241
+ isRowHeader?: boolean;
242
+ tooltip?: string;
243
+ className?: string;
244
+ children?: React.ReactNode;
245
+ }
246
+
247
+ function TableHead({
248
+ id,
249
+ label,
250
+ allowsSorting = false,
251
+ tooltip,
252
+ className,
253
+ children,
254
+ }: TableHeadProps) {
255
+ const { sortDescriptor, onSortChange } = useContext(TableContext);
256
+ const [showTooltip, setShowTooltip] = useState(false);
257
+ const isActive = sortDescriptor?.column === id;
258
+ const direction = isActive ? sortDescriptor?.direction : undefined;
259
+
260
+ const handleSort = () => {
261
+ if (!allowsSorting || !id || !onSortChange) return;
262
+ const nextDir: SortDirection =
263
+ isActive && direction === "ascending" ? "descending" : "ascending";
264
+ onSortChange({ column: id, direction: nextDir });
265
+ };
266
+
267
+ return (
268
+ <th
269
+ className={cn(allowsSorting && styles.sortableHead, className)}
270
+ onClick={allowsSorting ? handleSort : undefined}
271
+ aria-sort={isActive ? (direction === "ascending" ? "ascending" : "descending") : undefined}
272
+ >
273
+ <span className={styles.headInner}>
274
+ {label ?? children}
275
+ {allowsSorting && (
276
+ <span
277
+ className={cn(
278
+ styles.sortIcon,
279
+ isActive && styles.sortIconActive,
280
+ direction === "descending" && styles.sortIconDesc,
281
+ )}
282
+ >
283
+ <ArrowUp2 size={12} variant="Bold" color="currentColor" />
284
+ </span>
285
+ )}
286
+ {tooltip && (
287
+ <span
288
+ className={styles.headTooltipWrap}
289
+ onMouseEnter={() => setShowTooltip(true)}
290
+ onMouseLeave={() => setShowTooltip(false)}
291
+ >
292
+ <span className={styles.headTooltipIcon}>
293
+ <InfoCircle size={14} variant="Bulk" color="currentColor" />
294
+ </span>
295
+ {showTooltip && (
296
+ <span className={styles.headTooltipBubble}>{tooltip}</span>
297
+ )}
298
+ </span>
299
+ )}
300
+ </span>
301
+ </th>
302
+ );
303
+ }
304
+
305
+ /* ══════════════════════════════════════════════
306
+ Table.Body
307
+ ══════════════════════════════════════════════ */
308
+
309
+ interface TableBodyProps<T> {
310
+ items?: T[];
311
+ children: ((item: T) => React.ReactNode) | React.ReactNode;
312
+ className?: string;
313
+ }
314
+
315
+ function TableBody<T>({ items, children, className }: TableBodyProps<T>) {
316
+ if (items && typeof children === "function") {
317
+ return (
318
+ <tbody className={cn(styles.tbody, className)}>
319
+ {items.map((item, i) => (children as (item: T) => React.ReactNode)(item))}
320
+ </tbody>
321
+ );
322
+ }
323
+
324
+ return (
325
+ <tbody className={cn(styles.tbody, className)}>
326
+ {children as React.ReactNode}
327
+ </tbody>
328
+ );
329
+ }
330
+
331
+ /* ══════════════════════════════════════════════
332
+ Table.Row
333
+ ══════════════════════════════════════════════ */
334
+
335
+ interface TableRowProps {
336
+ id: string;
337
+ children: React.ReactNode;
338
+ className?: string;
339
+ }
340
+
341
+ function TableRow({ id, children, className }: TableRowProps) {
342
+ const { selectionMode, selectedKeys, toggleSelection, registerKey, unregisterKey } =
343
+ useContext(TableContext);
344
+ const isSelected = selectedKeys.has(id);
345
+
346
+ useEffect(() => {
347
+ registerKey(id);
348
+ return () => unregisterKey(id);
349
+ }, [id, registerKey, unregisterKey]);
350
+
351
+ return (
352
+ <tr className={cn(isSelected && styles.rowSelected, className)}>
353
+ {selectionMode === "multiple" && (
354
+ <td className={cn(styles.td, styles.checkboxCell)}>
355
+ <input
356
+ type="checkbox"
357
+ className={styles.rowCheckbox}
358
+ checked={isSelected}
359
+ onChange={() => toggleSelection(id)}
360
+ aria-label={`Select row ${id}`}
361
+ />
362
+ </td>
363
+ )}
364
+ {children}
365
+ </tr>
366
+ );
367
+ }
368
+
369
+ /* ══════════════════════════════════════════════
370
+ Table.Cell
371
+ ══════════════════════════════════════════════ */
372
+
373
+ interface TableCellProps {
374
+ children?: React.ReactNode;
375
+ className?: string;
376
+ }
377
+
378
+ function TableCell({ children, className }: TableCellProps) {
379
+ return <td className={cn(styles.td, className)}>{children}</td>;
380
+ }
381
+
382
+ /* ══════════════════════════════════════════════
383
+ TableRowActionsDropdown
384
+ ══════════════════════════════════════════════ */
385
+
386
+ export interface TableRowActionsDropdownProps {
387
+ items?: {
388
+ label: string;
389
+ icon?: React.ReactNode;
390
+ destructive?: boolean;
391
+ onSelect?: () => void;
392
+ }[];
393
+ className?: string;
394
+ }
395
+
396
+ export function TableRowActionsDropdown({ items, className }: TableRowActionsDropdownProps) {
397
+ const [open, setOpen] = useState(false);
398
+ const wrapRef = useRef<HTMLDivElement>(null);
399
+
400
+ const defaultItems: TableRowActionsDropdownProps["items"] = [
401
+ { label: "Edit", icon: <Edit size={14} variant="Linear" color="currentColor" /> },
402
+ { label: "Duplicate", icon: <Copy size={14} variant="Linear" color="currentColor" /> },
403
+ { label: "Export", icon: <ExportSquare size={14} variant="Linear" color="currentColor" /> },
404
+ { label: "Delete", icon: <Trash size={14} variant="Linear" color="currentColor" />, destructive: true },
405
+ ];
406
+
407
+ const menuItems = items ?? defaultItems;
408
+
409
+ useEffect(() => {
410
+ if (!open) return;
411
+ const handler = (e: MouseEvent) => {
412
+ if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
413
+ setOpen(false);
414
+ }
415
+ };
416
+ document.addEventListener("mousedown", handler);
417
+ return () => document.removeEventListener("mousedown", handler);
418
+ }, [open]);
419
+
420
+ useEffect(() => {
421
+ if (!open) return;
422
+ const handler = (e: KeyboardEvent) => {
423
+ if (e.key === "Escape") setOpen(false);
424
+ };
425
+ document.addEventListener("keydown", handler);
426
+ return () => document.removeEventListener("keydown", handler);
427
+ }, [open]);
428
+
429
+ return (
430
+ <div ref={wrapRef} className={cn(styles.rowActionsWrap, className)}>
431
+ <button
432
+ type="button"
433
+ className={styles.rowActionsBtn}
434
+ onClick={() => setOpen((v) => !v)}
435
+ aria-label="Row actions"
436
+ aria-expanded={open}
437
+ >
438
+ <More size={18} variant="Linear" color="currentColor" />
439
+ </button>
440
+ {open && (
441
+ <div className={styles.rowActionsMenu} role="menu">
442
+ {menuItems.map((item, i) => (
443
+ <div key={item.label}>
444
+ {item.destructive && i > 0 && <div className={styles.rowActionsSeparator} />}
445
+ <button
446
+ type="button"
447
+ className={cn(
448
+ styles.rowActionsMenuItem,
449
+ item.destructive && styles.rowActionsMenuItemDestructive,
450
+ )}
451
+ role="menuitem"
452
+ onClick={() => {
453
+ item.onSelect?.();
454
+ setOpen(false);
455
+ }}
456
+ >
457
+ {item.icon && (
458
+ <span className={styles.rowActionsMenuItemIcon}>{item.icon}</span>
459
+ )}
460
+ {item.label}
461
+ </button>
462
+ </div>
463
+ ))}
464
+ </div>
465
+ )}
466
+ </div>
467
+ );
468
+ }
469
+
470
+ /* ══════════════════════════════════════════════
471
+ PaginationMinimal
472
+ ══════════════════════════════════════════════ */
473
+
474
+ export interface PaginationMinimalProps {
475
+ page: number;
476
+ total: number;
477
+ onPageChange?: (page: number) => void;
478
+ align?: "left" | "right" | "between";
479
+ className?: string;
480
+ }
481
+
482
+ export function PaginationMinimal({
483
+ page,
484
+ total,
485
+ onPageChange,
486
+ align = "right",
487
+ className,
488
+ }: PaginationMinimalProps) {
489
+ return (
490
+ <div
491
+ className={cn(
492
+ styles.paginationFooter,
493
+ align === "right" && styles.paginationFooterRight,
494
+ className,
495
+ )}
496
+ >
497
+ {align === "between" && (
498
+ <span className={styles.paginationInfo}>
499
+ Page {page} of {total}
500
+ </span>
501
+ )}
502
+ <div className={styles.paginationActions}>
503
+ <button
504
+ type="button"
505
+ className={styles.paginationBtn}
506
+ disabled={page <= 1}
507
+ onClick={() => onPageChange?.(page - 1)}
508
+ aria-label="Previous page"
509
+ >
510
+ <ArrowUp2
511
+ size={14}
512
+ variant="Bold"
513
+ color="currentColor"
514
+ style={{ transform: "rotate(-90deg)" }}
515
+ />
516
+ </button>
517
+ <span className={styles.paginationInfo}>
518
+ {page} / {total}
519
+ </span>
520
+ <button
521
+ type="button"
522
+ className={styles.paginationBtn}
523
+ disabled={page >= total}
524
+ onClick={() => onPageChange?.(page + 1)}
525
+ aria-label="Next page"
526
+ >
527
+ <ArrowUp2
528
+ size={14}
529
+ variant="Bold"
530
+ color="currentColor"
531
+ style={{ transform: "rotate(90deg)" }}
532
+ />
533
+ </button>
534
+ </div>
535
+ </div>
536
+ );
537
+ }
538
+
539
+ /* ══════════════════════════════════════════════
540
+ Compound export
541
+ ══════════════════════════════════════════════ */
542
+
543
+ Table.Header = TableHeader;
544
+ Table.Head = TableHead;
545
+ Table.Body = TableBody;
546
+ Table.Row = TableRow;
547
+ Table.Cell = TableCell;
@@ -0,0 +1,74 @@
1
+ {
2
+ "items": [
3
+ {
4
+ "name": "Catalog",
5
+ "website": "catalogapp.io",
6
+ "logoUrl": "https://i.pravatar.cc/150?u=catalog-co",
7
+ "status": "Customer",
8
+ "aboutTitle": "Content curating app",
9
+ "aboutDescription": "Brings all your news into one place",
10
+ "users": 5,
11
+ "licenseUse": 80
12
+ },
13
+ {
14
+ "name": "Circooles",
15
+ "website": "getcircooles.com",
16
+ "logoUrl": "https://i.pravatar.cc/150?u=circooles-co",
17
+ "status": "Customer",
18
+ "aboutTitle": "Design software",
19
+ "aboutDescription": "Super lightweight design app",
20
+ "users": 5,
21
+ "licenseUse": 65
22
+ },
23
+ {
24
+ "name": "Command+R",
25
+ "website": "cmdr.ai",
26
+ "logoUrl": "https://i.pravatar.cc/150?u=commandr-co",
27
+ "status": "Customer",
28
+ "aboutTitle": "Data prediction",
29
+ "aboutDescription": "AI and machine learning",
30
+ "users": 5,
31
+ "licenseUse": 92
32
+ },
33
+ {
34
+ "name": "Hourglass",
35
+ "website": "hourglass.app",
36
+ "logoUrl": "https://i.pravatar.cc/150?u=hourglass-co",
37
+ "status": "Churned",
38
+ "aboutTitle": "Productivity app",
39
+ "aboutDescription": "Time management and planning",
40
+ "users": 5,
41
+ "licenseUse": 40
42
+ },
43
+ {
44
+ "name": "Layers",
45
+ "website": "getlayers.io",
46
+ "logoUrl": "https://i.pravatar.cc/150?u=layers-co",
47
+ "status": "Customer",
48
+ "aboutTitle": "Web app integrations",
49
+ "aboutDescription": "Connect your apps seamlessly",
50
+ "users": 5,
51
+ "licenseUse": 55
52
+ },
53
+ {
54
+ "name": "Quotient",
55
+ "website": "quotient.co",
56
+ "logoUrl": "https://i.pravatar.cc/150?u=quotient-co",
57
+ "status": "Customer",
58
+ "aboutTitle": "Sales CRM",
59
+ "aboutDescription": "Web-based sales doc management",
60
+ "users": 5,
61
+ "licenseUse": 70
62
+ },
63
+ {
64
+ "name": "Sisyphus",
65
+ "website": "sisyphus.com",
66
+ "logoUrl": "https://i.pravatar.cc/150?u=sisyphus-co",
67
+ "status": "Customer",
68
+ "aboutTitle": "Automation and workflow",
69
+ "aboutDescription": "Workflow automation made easy",
70
+ "users": 5,
71
+ "licenseUse": 48
72
+ }
73
+ ]
74
+ }
@@ -0,0 +1,14 @@
1
+ export {
2
+ Table,
3
+ TableCard,
4
+ TableRowActionsDropdown,
5
+ PaginationMinimal,
6
+ } from "./Table";
7
+
8
+ export type {
9
+ TableProps,
10
+ SortDescriptor,
11
+ SortDirection,
12
+ TableRowActionsDropdownProps,
13
+ PaginationMinimalProps,
14
+ } from "./Table";