@gunjo/ui 0.0.1-alpha.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 (88) hide show
  1. package/README.md +129 -0
  2. package/design/atoms-metadata.json +82 -0
  3. package/design/molecules-metadata.json +130 -0
  4. package/design/organisms-metadata.json +38 -0
  5. package/design/templates-metadata.json +38 -0
  6. package/package.json +158 -0
  7. package/src/components/atoms/Alert.tsx +63 -0
  8. package/src/components/atoms/Avatar.tsx +57 -0
  9. package/src/components/atoms/Badge.tsx +30 -0
  10. package/src/components/atoms/Button.tsx +29 -0
  11. package/src/components/atoms/ButtonVariants.ts +37 -0
  12. package/src/components/atoms/Checkbox.tsx +52 -0
  13. package/src/components/atoms/Img.tsx +102 -0
  14. package/src/components/atoms/Input.tsx +37 -0
  15. package/src/components/atoms/Kbd.tsx +22 -0
  16. package/src/components/atoms/Label.tsx +22 -0
  17. package/src/components/atoms/Progress.tsx +38 -0
  18. package/src/components/atoms/RadioGroup.tsx +86 -0
  19. package/src/components/atoms/Select.tsx +28 -0
  20. package/src/components/atoms/Separator.tsx +33 -0
  21. package/src/components/atoms/Skeleton.tsx +36 -0
  22. package/src/components/atoms/Slider.tsx +26 -0
  23. package/src/components/atoms/Spinner.tsx +34 -0
  24. package/src/components/atoms/Switch.tsx +47 -0
  25. package/src/components/atoms/Textarea.tsx +34 -0
  26. package/src/components/atoms/ToggleGroup.tsx +60 -0
  27. package/src/components/atoms/ToolPill.tsx +77 -0
  28. package/src/components/atoms/generated/default-variant-keys.ts +36 -0
  29. package/src/components/atoms/generated/variant-keys.ts +61 -0
  30. package/src/components/generated/component-manifest.ts +741 -0
  31. package/src/components/generated/component-style-hints.ts +1262 -0
  32. package/src/components/molecules/AIChatInput.tsx +140 -0
  33. package/src/components/molecules/AIChatMessage.tsx +109 -0
  34. package/src/components/molecules/Accordion.tsx +99 -0
  35. package/src/components/molecules/Breadcrumb.tsx +115 -0
  36. package/src/components/molecules/Calendar.tsx +60 -0
  37. package/src/components/molecules/Card.tsx +78 -0
  38. package/src/components/molecules/Carousel.tsx +261 -0
  39. package/src/components/molecules/Command.tsx +152 -0
  40. package/src/components/molecules/ContextMenu.tsx +200 -0
  41. package/src/components/molecules/Dialog.tsx +122 -0
  42. package/src/components/molecules/DropdownMenu.tsx +200 -0
  43. package/src/components/molecules/FilterButton.tsx +133 -0
  44. package/src/components/molecules/Form.tsx +90 -0
  45. package/src/components/molecules/HoverCard.tsx +29 -0
  46. package/src/components/molecules/List.tsx +120 -0
  47. package/src/components/molecules/Menubar.tsx +231 -0
  48. package/src/components/molecules/Modal.tsx +66 -0
  49. package/src/components/molecules/NotificationCenter.tsx +118 -0
  50. package/src/components/molecules/Pagination.tsx +118 -0
  51. package/src/components/molecules/Popover.tsx +31 -0
  52. package/src/components/molecules/ProgressWidget.tsx +40 -0
  53. package/src/components/molecules/Resizable.tsx +47 -0
  54. package/src/components/molecules/ScrollArea.tsx +48 -0
  55. package/src/components/molecules/Sheet.tsx +140 -0
  56. package/src/components/molecules/SidebarItem.tsx +134 -0
  57. package/src/components/molecules/SortButton.tsx +56 -0
  58. package/src/components/molecules/StatusBar.tsx +41 -0
  59. package/src/components/molecules/Stepper.tsx +108 -0
  60. package/src/components/molecules/Table.tsx +117 -0
  61. package/src/components/molecules/Tabs.tsx +64 -0
  62. package/src/components/molecules/Toast.tsx +57 -0
  63. package/src/components/molecules/Tooltip.tsx +30 -0
  64. package/src/components/molecules/generated/default-variant-keys.ts +22 -0
  65. package/src/components/molecules/generated/variant-keys.ts +33 -0
  66. package/src/components/organisms/AppRail.tsx +28 -0
  67. package/src/components/organisms/CommandPalette.tsx +58 -0
  68. package/src/components/organisms/FileUploader.tsx +151 -0
  69. package/src/components/organisms/FloatingPanel.tsx +46 -0
  70. package/src/components/organisms/InspectorPanel.tsx +65 -0
  71. package/src/components/organisms/RightRail.tsx +29 -0
  72. package/src/components/organisms/ShareModal.tsx +182 -0
  73. package/src/components/organisms/SpatialCanvas.tsx +36 -0
  74. package/src/components/organisms/ToastProvider.tsx +49 -0
  75. package/src/components/templates/AuthTemplate.tsx +58 -0
  76. package/src/components/templates/BannalyzeTemplate.tsx +55 -0
  77. package/src/components/templates/ChatTemplate.tsx +55 -0
  78. package/src/components/templates/DashboardTemplate.tsx +34 -0
  79. package/src/components/templates/EditorTemplate.tsx +46 -0
  80. package/src/components/templates/KanbanTemplate.tsx +38 -0
  81. package/src/components/templates/LandingTemplate.tsx +53 -0
  82. package/src/components/templates/MediaLibraryTemplate.tsx +55 -0
  83. package/src/components/templates/SettingsTemplate.tsx +48 -0
  84. package/src/globals.css +108 -0
  85. package/src/index.ts +89 -0
  86. package/src/lib/utils.ts +6 -0
  87. package/tailwind-preset.js +11 -0
  88. package/tailwind-theme-extend.cjs +86 -0
@@ -0,0 +1,40 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../lib/utils"
5
+ import { Progress } from "../atoms/Progress"
6
+ import { Card, CardContent, CardHeader, CardTitle } from "./Card"
7
+
8
+ export interface ProgressWidgetProps extends React.HTMLAttributes<HTMLDivElement> {
9
+ title: string
10
+ value: number
11
+ max?: number
12
+ label?: string
13
+ subtext?: string
14
+ icon?: React.ReactNode
15
+ }
16
+
17
+ const ProgressWidget = React.forwardRef<HTMLDivElement, ProgressWidgetProps>(
18
+ ({ className, title, value, max = 100, label, subtext, icon, ...props }, ref) => {
19
+ const percentage = Math.round((value / max) * 100);
20
+
21
+ return (
22
+ <Card ref={ref} className={cn("flex flex-col w-[320px] rounded-lg border overflow-hidden", className)} {...props}>
23
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
24
+ <CardTitle className="text-sm font-medium">{title}</CardTitle>
25
+ {icon && <div className="text-muted-foreground">{icon}</div>}
26
+ </CardHeader>
27
+ <CardContent>
28
+ <div className="text-2xl font-bold">{label || `${percentage}%`}</div>
29
+ <Progress value={percentage} className="mt-2 h-2" />
30
+ {subtext && (
31
+ <p className="text-xs text-muted-foreground mt-2">{subtext}</p>
32
+ )}
33
+ </CardContent>
34
+ </Card>
35
+ )
36
+ }
37
+ )
38
+ ProgressWidget.displayName = "ProgressWidget"
39
+
40
+ export { ProgressWidget }
@@ -0,0 +1,47 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { GripVertical } from "lucide-react"
5
+ // @ts-ignore - Runtime inspection shows Group/Separator but types might verify PanelGroup
6
+ import { Panel, Group as PanelGroup, Separator as PanelResizeHandle } from "react-resizable-panels"
7
+
8
+ import { cn } from "../../lib/utils"
9
+
10
+ const ResizablePanelGroup = ({
11
+ className,
12
+ ...props
13
+ }: React.HTMLAttributes<HTMLDivElement> & { direction: "vertical" | "horizontal"; autoSaveId?: string; onLayout?: (sizes: number[]) => void; id?: string }) => (
14
+ <PanelGroup
15
+ className={cn(
16
+ "flex h-full w-full data-[panel-group-direction=vertical]:flex-col flex-col w-[520px] h-[240px]",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+
23
+ const ResizablePanel = Panel
24
+
25
+ const ResizableHandle = ({
26
+ withHandle,
27
+ className,
28
+ ...props
29
+ }: React.ComponentProps<typeof PanelResizeHandle> & {
30
+ withHandle?: boolean
31
+ }) => (
32
+ <PanelResizeHandle
33
+ className={cn(
34
+ "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
35
+ className
36
+ )}
37
+ {...props}
38
+ >
39
+ {withHandle && (
40
+ <div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
41
+ <GripVertical className="h-2.5 w-2.5" />
42
+ </div>
43
+ )}
44
+ </PanelResizeHandle>
45
+ )
46
+
47
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
@@ -0,0 +1,48 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ const ScrollArea = React.forwardRef<
9
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
11
+ >(({ className, children, ...props }, ref) => (
12
+ <ScrollAreaPrimitive.Root
13
+ ref={ref}
14
+ className={cn("relative overflow-hidden flex flex-col w-[320px] h-[240px]", className)}
15
+ {...props}
16
+ >
17
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
18
+ {children}
19
+ </ScrollAreaPrimitive.Viewport>
20
+ <ScrollBar />
21
+ <ScrollAreaPrimitive.Corner />
22
+ </ScrollAreaPrimitive.Root>
23
+ ))
24
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25
+
26
+ const ScrollBar = React.forwardRef<
27
+ React.ElementRef<typeof ScrollAreaPrimitive.Scrollbar>,
28
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Scrollbar>
29
+ >(({ className, orientation = "vertical", ...props }, ref) => (
30
+ <ScrollAreaPrimitive.Scrollbar
31
+ ref={ref}
32
+ orientation={orientation}
33
+ className={cn(
34
+ "flex touch-none select-none transition-colors",
35
+ orientation === "vertical" &&
36
+ "h-full w-2.5 border-l border-l-transparent p-[1px]",
37
+ orientation === "horizontal" &&
38
+ "h-2.5 border-t border-t-transparent p-[1px]",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ <ScrollAreaPrimitive.Thumb className="relative flex-1 rounded-full bg-border" />
44
+ </ScrollAreaPrimitive.Scrollbar>
45
+ ))
46
+ ScrollBar.displayName = ScrollAreaPrimitive.Scrollbar.displayName
47
+
48
+ export { ScrollArea, ScrollBar }
@@ -0,0 +1,140 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SheetPrimitive from "@radix-ui/react-dialog"
5
+ import { type VariantProps, cva } from "class-variance-authority"
6
+ import { X } from "lucide-react"
7
+
8
+ import { cn } from "../../lib/utils"
9
+
10
+ const Sheet = SheetPrimitive.Root
11
+
12
+ const SheetTrigger = SheetPrimitive.Trigger
13
+
14
+ const SheetClose = SheetPrimitive.Close
15
+
16
+ const SheetPortal = SheetPrimitive.Portal
17
+
18
+ const SheetOverlay = React.forwardRef<
19
+ React.ElementRef<typeof SheetPrimitive.Overlay>,
20
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
21
+ >(({ className, ...props }, ref) => (
22
+ <SheetPrimitive.Overlay
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-overlay/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className
26
+ )}
27
+ {...props}
28
+ ref={ref}
29
+ />
30
+ ))
31
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32
+
33
+ const sheetVariants = cva(
34
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35
+ {
36
+ variants: {
37
+ side: {
38
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39
+ bottom:
40
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42
+ right:
43
+ "inset-y-0 right-0 h-full w-[384px] w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ side: "right",
48
+ },
49
+ }
50
+ )
51
+
52
+ interface SheetContentProps
53
+ extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
54
+ VariantProps<typeof sheetVariants> { }
55
+
56
+ const SheetContent = React.forwardRef<
57
+ React.ElementRef<typeof SheetPrimitive.Content>,
58
+ SheetContentProps
59
+ >(({ side = "right", className, children, ...props }, ref) => (
60
+ <SheetPortal>
61
+ <SheetOverlay />
62
+ <SheetPrimitive.Content
63
+ ref={ref}
64
+ className={cn(sheetVariants({ side }), className)}
65
+ {...props}
66
+ >
67
+ <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
68
+ <X className="h-4 w-4" />
69
+ <span className="sr-only">Close</span>
70
+ </SheetPrimitive.Close>
71
+ {children}
72
+ </SheetPrimitive.Content>
73
+ </SheetPortal>
74
+ ))
75
+ SheetContent.displayName = SheetPrimitive.Content.displayName
76
+
77
+ const SheetHeader = ({
78
+ className,
79
+ ...props
80
+ }: React.HTMLAttributes<HTMLDivElement>) => (
81
+ <div
82
+ className={cn(
83
+ "flex flex-col space-y-2 text-center sm:text-left",
84
+ className
85
+ )}
86
+ {...props}
87
+ />
88
+ )
89
+ SheetHeader.displayName = "SheetHeader"
90
+
91
+ const SheetFooter = ({
92
+ className,
93
+ ...props
94
+ }: React.HTMLAttributes<HTMLDivElement>) => (
95
+ <div
96
+ className={cn(
97
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
98
+ className
99
+ )}
100
+ {...props}
101
+ />
102
+ )
103
+ SheetFooter.displayName = "SheetFooter"
104
+
105
+ const SheetTitle = React.forwardRef<
106
+ React.ElementRef<typeof SheetPrimitive.Title>,
107
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
108
+ >(({ className, ...props }, ref) => (
109
+ <SheetPrimitive.Title
110
+ ref={ref}
111
+ className={cn("text-lg font-semibold text-foreground", className)}
112
+ {...props}
113
+ />
114
+ ))
115
+ SheetTitle.displayName = SheetPrimitive.Title.displayName
116
+
117
+ const SheetDescription = React.forwardRef<
118
+ React.ElementRef<typeof SheetPrimitive.Description>,
119
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
120
+ >(({ className, ...props }, ref) => (
121
+ <SheetPrimitive.Description
122
+ ref={ref}
123
+ className={cn("text-sm text-muted-foreground", className)}
124
+ {...props}
125
+ />
126
+ ))
127
+ SheetDescription.displayName = SheetPrimitive.Description.displayName
128
+
129
+ export {
130
+ Sheet,
131
+ SheetPortal,
132
+ SheetOverlay,
133
+ SheetTrigger,
134
+ SheetClose,
135
+ SheetContent,
136
+ SheetHeader,
137
+ SheetFooter,
138
+ SheetTitle,
139
+ SheetDescription,
140
+ }
@@ -0,0 +1,134 @@
1
+ "use client"
2
+
3
+ import React, { memo } from 'react';
4
+ import { Trash2, ChevronRight } from 'lucide-react';
5
+ import { cn } from '../../lib/utils';
6
+ import type { SidebarItemVariantKey } from './generated/variant-keys';
7
+ import { sidebarItemDefaultVariantKey } from "./generated/default-variant-keys";
8
+
9
+ export interface SidebarItemProps {
10
+ icon: React.ReactNode;
11
+ label: string;
12
+ count?: number;
13
+ isActive: boolean;
14
+ onClick: () => void;
15
+ onDrop?: (e: React.DragEvent) => void;
16
+ onDragOver?: (e: React.DragEvent) => void;
17
+ dragOverId?: string | null;
18
+ dragAction?: 'nest' | 'reorder-above' | 'reorder-below' | null;
19
+ id: string;
20
+ level?: number;
21
+ hasChildren?: boolean;
22
+ isExpanded?: boolean;
23
+ onToggleExpand?: (e: React.MouseEvent) => void;
24
+ onDelete?: (e: React.MouseEvent) => void;
25
+ draggable?: boolean;
26
+ onDragStart?: (e: React.DragEvent) => void;
27
+ className?: string;
28
+ }
29
+
30
+ export const SidebarItem = memo(({
31
+ icon,
32
+ label,
33
+ count,
34
+ isActive,
35
+ onClick,
36
+ onDrop,
37
+ onDragOver,
38
+ dragOverId,
39
+ dragAction,
40
+ id,
41
+ level = 0,
42
+ hasChildren = false,
43
+ isExpanded = false,
44
+ onToggleExpand,
45
+ onDelete,
46
+ draggable,
47
+ onDragStart,
48
+ className
49
+ }: SidebarItemProps) => {
50
+ const baseVariant: SidebarItemVariantKey = isActive ? "active" : sidebarItemDefaultVariantKey;
51
+ const variantClasses: Record<SidebarItemVariantKey, string> = {
52
+ active: "bg-secondary text-foreground",
53
+ default: "text-muted-foreground hover:bg-muted hover:text-foreground",
54
+ };
55
+
56
+ const dragNestClass =
57
+ dragOverId === id && dragAction === "nest"
58
+ ? "bg-primary/20 text-primary ring-2 ring-primary shadow-lg shadow-primary/40 scale-[1.02] z-10"
59
+ : null;
60
+
61
+ return (
62
+ <button
63
+ onClick={(e) => {
64
+ onClick();
65
+ if (hasChildren && onToggleExpand) {
66
+ onToggleExpand(e);
67
+ }
68
+ }}
69
+ onDragOver={onDragOver}
70
+ onDrop={onDrop}
71
+ draggable={draggable}
72
+ onDragStart={onDragStart}
73
+ className={cn(
74
+ "w-[320px] w-full h-9 flex flex-col flex-row items-center justify-between px-4 py-1.5 rounded-md text-sm transition-colors group relative",
75
+ dragNestClass ?? variantClasses[baseVariant],
76
+ className
77
+ )}
78
+ style={{ paddingLeft: `${(level * 16) + 8}px` }}
79
+ >
80
+ {dragOverId === id && dragAction === 'reorder-above' && (
81
+ <div className="absolute top-0 left-0 right-0 h-0.5 bg-primary z-20 pointer-events-none" />
82
+ )}
83
+ {dragOverId === id && dragAction === 'reorder-below' && (
84
+ <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary z-20 pointer-events-none" />
85
+ )}
86
+ <div className="flex items-center gap-2 overflow-hidden flex-1">
87
+ {/* Arrow / Spacer - Fixed Width for Alignment */}
88
+ <div
89
+ onClick={(e) => {
90
+ if (hasChildren && onToggleExpand) {
91
+ e.stopPropagation();
92
+ onToggleExpand(e);
93
+ }
94
+ }}
95
+ className={cn(
96
+ "w-5 h-5 flex items-center justify-center cursor-pointer transition-colors",
97
+ hasChildren ? "" : "pointer-events-none"
98
+ )}
99
+ >
100
+ {hasChildren && (
101
+ <ChevronRight size={12} className={cn("transition-transform", isExpanded ? "rotate-90" : "")} />
102
+ )}
103
+ </div>
104
+
105
+ <div className={cn("flex-shrink-0", isActive ? "text-primary" : "text-muted-foreground group-hover:text-foreground")}>
106
+ {icon}
107
+ </div>
108
+ <span className="truncate" title={label}>{label}</span>
109
+ </div>
110
+
111
+ {/* Right Side: Delete + Count */}
112
+ <div className="flex items-center -mr-1">
113
+ {/* Delete Button */}
114
+ <div
115
+ onClick={(e) => onDelete && onDelete(e)}
116
+ className={cn(
117
+ "w-6 h-6 flex items-center justify-center transition-opacity cursor-pointer mr-0.5",
118
+ onDelete
119
+ ? (isActive
120
+ ? "opacity-100 text-destructive"
121
+ : "text-muted-foreground/50 hover:text-destructive opacity-100 md:opacity-0 md:group-hover:opacity-100")
122
+ : "pointer-events-none"
123
+ )}
124
+ >
125
+ {onDelete && <Trash2 size={12} />}
126
+ </div>
127
+
128
+ {count !== undefined && (
129
+ <span className="text-xs opacity-60 w-6 text-center tabular-nums text-muted-foreground translate-y-[0.5px]">{count}</span>
130
+ )}
131
+ </div>
132
+ </button>
133
+ );
134
+ });
@@ -0,0 +1,56 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ArrowDownAZ, ArrowUpAZ, ArrowUpDown, type LucideIcon } from "lucide-react"
5
+ import { Button, ButtonProps } from "../atoms/Button"
6
+ import { cn } from "../../lib/utils"
7
+ import { sortButtonDefaultVariantKey } from "./generated/default-variant-keys"
8
+ import { sortButtonVariantKeys, type SortButtonVariantKey } from "./generated/variant-keys"
9
+
10
+ export interface SortButtonProps extends Omit<ButtonProps, "onChange"> {
11
+ value?: SortButtonVariantKey
12
+ onSortChange?: (value: SortButtonVariantKey) => void
13
+ label?: string
14
+ }
15
+
16
+ const sortIconByValue: Record<SortButtonVariantKey, LucideIcon> = {
17
+ asc: ArrowUpAZ,
18
+ desc: ArrowDownAZ,
19
+ none: ArrowUpDown,
20
+ }
21
+
22
+ const sortButtonStateClasses: Record<SortButtonVariantKey, string> = {
23
+ none: "flex flex-col items-center h-9 py-2 px-3 gap-2 rounded-md",
24
+ asc: "inline-flex items-center h-9 py-2 px-3 gap-2 rounded-md",
25
+ desc: "inline-flex items-center h-9 py-2 px-3 gap-2 rounded-md",
26
+ }
27
+
28
+ const SortButton = React.forwardRef<HTMLButtonElement, SortButtonProps>(
29
+ ({ className, value = sortButtonDefaultVariantKey, onSortChange, label = "Sort", variant = "ghost", size = "sm", ...props }, ref) => {
30
+
31
+ const handleClick = () => {
32
+ const currentIndex = sortButtonVariantKeys.indexOf(value)
33
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % sortButtonVariantKeys.length
34
+ onSortChange?.(sortButtonVariantKeys[nextIndex])
35
+ }
36
+
37
+ const Icon = sortIconByValue[value]
38
+
39
+ return (
40
+ <Button
41
+ ref={ref}
42
+ variant={variant}
43
+ size={size}
44
+ className={cn(sortButtonStateClasses[value], className)}
45
+ onClick={handleClick}
46
+ {...props}
47
+ >
48
+ <Icon className="h-4 w-4" />
49
+ <span>{label}</span>
50
+ </Button>
51
+ )
52
+ }
53
+ )
54
+ SortButton.displayName = "SortButton"
55
+
56
+ export { SortButton }
@@ -0,0 +1,41 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../lib/utils"
5
+
6
+ export interface StatusBarProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ leftNode?: React.ReactNode
8
+ rightNode?: React.ReactNode
9
+ fixed?: boolean
10
+ }
11
+
12
+ const StatusBar = React.forwardRef<HTMLDivElement, StatusBarProps>(
13
+ ({ className, leftNode, rightNode, fixed = true, children, ...props }, ref) => {
14
+ return (
15
+ <div
16
+ ref={ref}
17
+ className={cn(
18
+ "flex flex-col flex-row items-center w-[640px] justify-between px-4 py-1 bg-primary text-primary-foreground text-xs z-50 shadow-md",
19
+ fixed && "fixed bottom-0 left-0 right-0",
20
+ className
21
+ )}
22
+ {...props}
23
+ >
24
+ <div className="flex items-center gap-4">
25
+ {leftNode}
26
+ </div>
27
+
28
+ <div className="flex items-center gap-2">
29
+ {children}
30
+ </div>
31
+
32
+ <div className="flex items-center gap-4">
33
+ {rightNode}
34
+ </div>
35
+ </div>
36
+ )
37
+ }
38
+ )
39
+ StatusBar.displayName = "StatusBar"
40
+
41
+ export { StatusBar }
@@ -0,0 +1,108 @@
1
+ import * as React from "react"
2
+ import { Check } from "lucide-react"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { cn } from "../../lib/utils"
5
+
6
+ const stepperVariants = cva("inline-flex items-start", {
7
+ variants: {
8
+ orientation: {
9
+ horizontal: "flex-row gap-0",
10
+ vertical: "flex-col gap-0",
11
+ },
12
+ },
13
+ defaultVariants: { orientation: "horizontal" },
14
+ })
15
+
16
+ export type StepperState = "completed" | "current" | "upcoming"
17
+
18
+ export interface StepperStep {
19
+ label: string
20
+ state: StepperState
21
+ }
22
+
23
+ export interface StepperProps
24
+ extends React.HTMLAttributes<HTMLDivElement>,
25
+ VariantProps<typeof stepperVariants> {
26
+ steps: StepperStep[]
27
+ }
28
+
29
+ function StepCircle({ state, index }: { state: StepperState; index: number }) {
30
+ const baseClasses =
31
+ "inline-flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold"
32
+ if (state === "completed") {
33
+ return (
34
+ <div className={cn(baseClasses, "bg-foreground text-background")}>
35
+ <Check className="h-4 w-4" />
36
+ </div>
37
+ )
38
+ }
39
+ if (state === "current") {
40
+ return (
41
+ <div
42
+ className={cn(
43
+ baseClasses,
44
+ "border-2 border-foreground bg-background text-foreground"
45
+ )}
46
+ >
47
+ {index + 1}
48
+ </div>
49
+ )
50
+ }
51
+ return (
52
+ <div className={cn(baseClasses, "bg-muted text-muted-foreground")}>
53
+ {index + 1}
54
+ </div>
55
+ )
56
+ }
57
+
58
+ const Stepper = React.forwardRef<HTMLDivElement, StepperProps>(
59
+ ({ className, orientation, steps, ...props }, ref) => {
60
+ const isHorizontal = orientation !== "vertical"
61
+ return (
62
+ <div
63
+ ref={ref}
64
+ className={cn(stepperVariants({ orientation }), "p-4", className)}
65
+ {...props}
66
+ >
67
+ {steps.map((step, idx) => {
68
+ const isLast = idx === steps.length - 1
69
+ return (
70
+ <React.Fragment key={`${step.label}-${idx}`}>
71
+ <div
72
+ className={cn(
73
+ "flex items-center gap-2",
74
+ isHorizontal ? "flex-col" : "flex-row"
75
+ )}
76
+ >
77
+ <StepCircle state={step.state} index={idx} />
78
+ <span
79
+ className={cn(
80
+ "text-xs font-medium",
81
+ step.state === "upcoming"
82
+ ? "text-muted-foreground"
83
+ : "text-foreground"
84
+ )}
85
+ >
86
+ {step.label}
87
+ </span>
88
+ </div>
89
+ {!isLast ? (
90
+ <div
91
+ className={cn(
92
+ "bg-border",
93
+ isHorizontal
94
+ ? "mx-2 mt-4 h-0.5 w-[60px] self-start"
95
+ : "my-2 ml-4 h-[24px] w-0.5 self-start"
96
+ )}
97
+ />
98
+ ) : null}
99
+ </React.Fragment>
100
+ )
101
+ })}
102
+ </div>
103
+ )
104
+ }
105
+ )
106
+ Stepper.displayName = "Stepper"
107
+
108
+ export { Stepper, stepperVariants }