@gunjo/ui 0.0.1-alpha.0 → 0.0.1-alpha.2

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 (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +90 -0
  3. package/README.md +52 -91
  4. package/package.json +47 -6
  5. package/src/components/display/Accordion.tsx +185 -0
  6. package/src/components/display/AccordionGroup.tsx +155 -0
  7. package/src/components/display/ActionDataTable.tsx +413 -0
  8. package/src/components/display/ActivityTimelineCard.tsx +483 -0
  9. package/src/components/display/AnalyticsCard.tsx +167 -0
  10. package/src/components/display/AssetCard.tsx +242 -0
  11. package/src/components/display/AssetGrid.tsx +164 -0
  12. package/src/components/display/Avatar.tsx +127 -0
  13. package/src/components/display/AvatarGroup.tsx +131 -0
  14. package/src/components/{atoms → display}/Badge.tsx +3 -3
  15. package/src/components/display/BarChart.tsx +247 -0
  16. package/src/components/{molecules → display}/Card.tsx +1 -1
  17. package/src/components/display/Carousel.tsx +593 -0
  18. package/src/components/display/ChartLegend.tsx +124 -0
  19. package/src/components/display/ChatMessage.tsx +382 -0
  20. package/src/components/display/ChoroplethMap.tsx +613 -0
  21. package/src/components/display/Code.tsx +42 -0
  22. package/src/components/display/CodeBlock.tsx +338 -0
  23. package/src/components/display/ColorSwatch.tsx +71 -0
  24. package/src/components/display/ConcentricProgressCard.tsx +545 -0
  25. package/src/components/display/DataTable.tsx +522 -0
  26. package/src/components/display/DistributionBar.tsx +102 -0
  27. package/src/components/display/DocNote.tsx +36 -0
  28. package/src/components/display/DonutChart.tsx +257 -0
  29. package/src/components/display/EmptyState.tsx +44 -0
  30. package/src/components/display/FileTree.tsx +180 -0
  31. package/src/components/display/GaugeChart.tsx +219 -0
  32. package/src/components/display/HeatmapChart.tsx +266 -0
  33. package/src/components/display/Icon.tsx +66 -0
  34. package/src/components/display/ImagePreview.tsx +140 -0
  35. package/src/components/{atoms → display}/Img.tsx +46 -12
  36. package/src/components/display/LabeledDonutCard.tsx +475 -0
  37. package/src/components/display/LineChart.tsx +464 -0
  38. package/src/components/{molecules → display}/List.tsx +20 -13
  39. package/src/components/display/MarkdownRenderer.tsx +157 -0
  40. package/src/components/display/MetadataList.tsx +81 -0
  41. package/src/components/display/MiniDistributionBarCard.tsx +314 -0
  42. package/src/components/display/PieChart.tsx +234 -0
  43. package/src/components/display/QuadrantMatrix.tsx +330 -0
  44. package/src/components/display/RadarChart.tsx +335 -0
  45. package/src/components/display/RadialBarChart.tsx +264 -0
  46. package/src/components/display/RetentionCohortCard.tsx +350 -0
  47. package/src/components/display/RibbonChart.tsx +618 -0
  48. package/src/components/display/SearchableAccordion.tsx +270 -0
  49. package/src/components/display/SegmentTimelineCard.tsx +452 -0
  50. package/src/components/display/SegmentedGaugeCard.tsx +607 -0
  51. package/src/components/display/Spacer.tsx +51 -0
  52. package/src/components/display/SparklineChart.tsx +394 -0
  53. package/src/components/display/StackedBarChart.tsx +393 -0
  54. package/src/components/display/Statistic.tsx +70 -0
  55. package/src/components/{molecules → display}/Table.tsx +22 -7
  56. package/src/components/display/Tag.tsx +80 -0
  57. package/src/components/display/TagEditor.tsx +141 -0
  58. package/src/components/display/Timeline.tsx +121 -0
  59. package/src/components/{atoms → display}/ToolPill.tsx +42 -18
  60. package/src/components/display/TreeView.tsx +226 -0
  61. package/src/components/display/chart-tooltip.tsx +423 -0
  62. package/src/components/display/chart-utils.ts +71 -0
  63. package/src/components/display/circular-chart-utils.ts +147 -0
  64. package/src/components/display/generated/default-variant-keys.ts +90 -0
  65. package/src/components/display/generated/variant-keys.ts +169 -0
  66. package/src/components/{atoms → feedback}/Alert.tsx +12 -5
  67. package/src/components/feedback/Banner.tsx +90 -0
  68. package/src/components/{molecules → feedback}/NotificationCenter.tsx +64 -31
  69. package/src/components/feedback/ProgressWidget.tsx +44 -0
  70. package/src/components/{atoms → feedback}/Spinner.tsx +2 -2
  71. package/src/components/{molecules → feedback}/StatusBar.tsx +4 -4
  72. package/src/components/feedback/StatusScreen.tsx +148 -0
  73. package/src/components/{molecules → feedback}/Stepper.tsx +10 -5
  74. package/src/components/feedback/Toast.tsx +108 -0
  75. package/src/components/feedback/ToastProvider.tsx +78 -0
  76. package/src/components/feedback/generated/default-variant-keys.ts +16 -0
  77. package/src/components/feedback/generated/variant-keys.ts +21 -0
  78. package/src/components/generated/component-manifest.ts +1568 -454
  79. package/src/components/generated/component-style-hints.ts +1958 -718
  80. package/src/components/{atoms → inputs}/ButtonVariants.ts +13 -3
  81. package/src/components/inputs/Calendar.tsx +212 -0
  82. package/src/components/inputs/ChatComposer.tsx +75 -0
  83. package/src/components/inputs/ChatInput.tsx +528 -0
  84. package/src/components/{atoms → inputs}/Checkbox.tsx +2 -2
  85. package/src/components/inputs/Combobox.tsx +175 -0
  86. package/src/components/inputs/CopyButton.tsx +187 -0
  87. package/src/components/inputs/DatePicker.tsx +519 -0
  88. package/src/components/inputs/DateRangePicker.tsx +878 -0
  89. package/src/components/inputs/EditableField.tsx +182 -0
  90. package/src/components/{organisms → inputs}/FileUploader.tsx +24 -9
  91. package/src/components/inputs/FilterButton.tsx +163 -0
  92. package/src/components/{molecules → inputs}/Form.tsx +20 -3
  93. package/src/components/{atoms → inputs}/Input.tsx +2 -0
  94. package/src/components/inputs/InputOTP.tsx +75 -0
  95. package/src/components/inputs/Mention.tsx +279 -0
  96. package/src/components/inputs/NumberInput.tsx +109 -0
  97. package/src/components/inputs/PasswordGroup.tsx +138 -0
  98. package/src/components/inputs/PasswordInput.tsx +74 -0
  99. package/src/components/inputs/PasswordRequirementList.tsx +96 -0
  100. package/src/components/inputs/PasswordStrengthMeter.tsx +93 -0
  101. package/src/components/inputs/PhoneInput.tsx +99 -0
  102. package/src/components/inputs/PostalCodeInput.tsx +98 -0
  103. package/src/components/inputs/RangeSlider.tsx +129 -0
  104. package/src/components/inputs/SearchInput.tsx +76 -0
  105. package/src/components/inputs/Select.tsx +39 -0
  106. package/src/components/{atoms → inputs}/Slider.tsx +18 -5
  107. package/src/components/{molecules → inputs}/SortButton.tsx +5 -2
  108. package/src/components/{atoms → inputs}/Switch.tsx +15 -4
  109. package/src/components/inputs/TagInput.tsx +114 -0
  110. package/src/components/{atoms → inputs}/Textarea.tsx +1 -0
  111. package/src/components/inputs/TimePicker.tsx +150 -0
  112. package/src/components/inputs/Toggle.tsx +48 -0
  113. package/src/components/{atoms → inputs}/ToggleGroup.tsx +2 -2
  114. package/src/components/inputs/TooltipButton.tsx +148 -0
  115. package/src/components/inputs/VoiceInputButton.tsx +317 -0
  116. package/src/components/inputs/calendar-holidays.ts +56 -0
  117. package/src/components/inputs/generated/default-variant-keys.ts +32 -0
  118. package/src/components/{atoms → inputs}/generated/variant-keys.ts +19 -27
  119. package/src/components/layout/AspectRatio.tsx +12 -0
  120. package/src/components/layout/AssetInspectorPanel.tsx +416 -0
  121. package/src/components/layout/Cluster.tsx +56 -0
  122. package/src/components/layout/CollapsiblePanelToggle.tsx +94 -0
  123. package/src/components/layout/Container.tsx +43 -0
  124. package/src/components/layout/DeviceFrame.tsx +227 -0
  125. package/src/components/layout/Grid.tsx +65 -0
  126. package/src/components/layout/HStack.tsx +73 -0
  127. package/src/components/{organisms → layout}/InspectorPanel.tsx +6 -5
  128. package/src/components/layout/MarqueeFrame.tsx +158 -0
  129. package/src/components/layout/Resizable.tsx +94 -0
  130. package/src/components/layout/ScrollArea.tsx +71 -0
  131. package/src/components/{organisms → layout}/SpatialCanvas.tsx +12 -7
  132. package/src/components/layout/VStack.tsx +69 -0
  133. package/src/components/layout/generated/default-variant-keys.ts +16 -0
  134. package/src/components/layout/generated/variant-keys.ts +21 -0
  135. package/src/components/{molecules → navigation}/Breadcrumb.tsx +5 -4
  136. package/src/components/navigation/Command.tsx +266 -0
  137. package/src/components/navigation/CommandPalette.tsx +83 -0
  138. package/src/components/navigation/DocumentPager.tsx +171 -0
  139. package/src/components/navigation/Footer.tsx +88 -0
  140. package/src/components/navigation/Header.tsx +80 -0
  141. package/src/components/{molecules → navigation}/Menubar.tsx +45 -12
  142. package/src/components/navigation/NavigationMenu.tsx +128 -0
  143. package/src/components/navigation/PageAside.tsx +84 -0
  144. package/src/components/{molecules → navigation}/Pagination.tsx +60 -7
  145. package/src/components/{organisms → navigation}/RightRail.tsx +1 -1
  146. package/src/components/navigation/Sidebar.tsx +223 -0
  147. package/src/components/navigation/SidebarItem.tsx +160 -0
  148. package/src/components/{molecules → navigation}/Tabs.tsx +2 -2
  149. package/src/components/navigation/TextLink.tsx +71 -0
  150. package/src/components/navigation/generated/default-variant-keys.ts +12 -0
  151. package/src/components/navigation/generated/variant-keys.ts +13 -0
  152. package/src/components/overlay/AIChatInput.tsx +5 -0
  153. package/src/components/overlay/AIChatMessage.tsx +6 -0
  154. package/src/components/overlay/AlertDialog.tsx +145 -0
  155. package/src/components/overlay/ChatPanel.tsx +180 -0
  156. package/src/components/{molecules → overlay}/ContextMenu.tsx +65 -29
  157. package/src/components/{molecules → overlay}/Dialog.tsx +21 -13
  158. package/src/components/overlay/Drawer.tsx +131 -0
  159. package/src/components/{molecules → overlay}/DropdownMenu.tsx +52 -17
  160. package/src/components/overlay/FloatingPanel.tsx +90 -0
  161. package/src/components/overlay/HoverCard.tsx +36 -0
  162. package/src/components/overlay/MediaLightbox.tsx +403 -0
  163. package/src/components/overlay/MediaPickerDialog.tsx +198 -0
  164. package/src/components/overlay/Modal.tsx +103 -0
  165. package/src/components/overlay/OnboardingFlow.tsx +172 -0
  166. package/src/components/overlay/Popover.tsx +36 -0
  167. package/src/components/overlay/ShareModal.tsx +324 -0
  168. package/src/components/{molecules → overlay}/Sheet.tsx +76 -19
  169. package/src/components/overlay/Tooltip.tsx +130 -0
  170. package/src/components/overlay/generated/default-variant-keys.ts +14 -0
  171. package/src/components/overlay/generated/variant-keys.ts +17 -0
  172. package/src/components/patterns/BlogTemplate.tsx +46 -0
  173. package/src/components/{templates → patterns}/DashboardTemplate.tsx +2 -2
  174. package/src/components/patterns/DocsTemplate.tsx +41 -0
  175. package/src/components/{templates → patterns}/MediaLibraryTemplate.tsx +1 -1
  176. package/src/components/patterns/OnboardingTemplate.tsx +32 -0
  177. package/src/components/patterns/PricingTemplate.tsx +106 -0
  178. package/src/globals.css +173 -22
  179. package/src/index.ts +177 -76
  180. package/tailwind-theme-extend.cjs +48 -3
  181. package/design/atoms-metadata.json +0 -82
  182. package/design/molecules-metadata.json +0 -130
  183. package/design/organisms-metadata.json +0 -38
  184. package/design/templates-metadata.json +0 -38
  185. package/src/components/atoms/Avatar.tsx +0 -57
  186. package/src/components/atoms/Select.tsx +0 -28
  187. package/src/components/atoms/generated/default-variant-keys.ts +0 -36
  188. package/src/components/molecules/AIChatInput.tsx +0 -140
  189. package/src/components/molecules/AIChatMessage.tsx +0 -109
  190. package/src/components/molecules/Accordion.tsx +0 -99
  191. package/src/components/molecules/Calendar.tsx +0 -60
  192. package/src/components/molecules/Carousel.tsx +0 -261
  193. package/src/components/molecules/Command.tsx +0 -152
  194. package/src/components/molecules/FilterButton.tsx +0 -133
  195. package/src/components/molecules/HoverCard.tsx +0 -29
  196. package/src/components/molecules/Modal.tsx +0 -66
  197. package/src/components/molecules/Popover.tsx +0 -31
  198. package/src/components/molecules/ProgressWidget.tsx +0 -40
  199. package/src/components/molecules/Resizable.tsx +0 -47
  200. package/src/components/molecules/ScrollArea.tsx +0 -48
  201. package/src/components/molecules/SidebarItem.tsx +0 -134
  202. package/src/components/molecules/Toast.tsx +0 -57
  203. package/src/components/molecules/Tooltip.tsx +0 -30
  204. package/src/components/molecules/generated/default-variant-keys.ts +0 -22
  205. package/src/components/molecules/generated/variant-keys.ts +0 -33
  206. package/src/components/organisms/CommandPalette.tsx +0 -58
  207. package/src/components/organisms/FloatingPanel.tsx +0 -46
  208. package/src/components/organisms/ShareModal.tsx +0 -182
  209. package/src/components/organisms/ToastProvider.tsx +0 -49
  210. /package/src/components/{atoms → display}/Kbd.tsx +0 -0
  211. /package/src/components/{atoms → display}/Separator.tsx +0 -0
  212. /package/src/components/{atoms → display}/Skeleton.tsx +0 -0
  213. /package/src/components/{atoms → feedback}/Progress.tsx +0 -0
  214. /package/src/components/{atoms → inputs}/Button.tsx +0 -0
  215. /package/src/components/{atoms → inputs}/Label.tsx +0 -0
  216. /package/src/components/{atoms → inputs}/RadioGroup.tsx +0 -0
  217. /package/src/components/{organisms → navigation}/AppRail.tsx +0 -0
  218. /package/src/components/{templates → patterns}/AuthTemplate.tsx +0 -0
  219. /package/src/components/{templates → patterns}/BannalyzeTemplate.tsx +0 -0
  220. /package/src/components/{templates → patterns}/ChatTemplate.tsx +0 -0
  221. /package/src/components/{templates → patterns}/EditorTemplate.tsx +0 -0
  222. /package/src/components/{templates → patterns}/KanbanTemplate.tsx +0 -0
  223. /package/src/components/{templates → patterns}/LandingTemplate.tsx +0 -0
  224. /package/src/components/{templates → patterns}/SettingsTemplate.tsx +0 -0
@@ -2,9 +2,10 @@
2
2
 
3
3
  import * as React from "react"
4
4
  import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
- import { Check, ChevronRight, Circle } from "lucide-react"
5
+ import { IconCheck as Check, IconChevronRight as ChevronRight, IconCircle as Circle } from "@tabler/icons-react";
6
6
 
7
7
  import { cn } from "../../lib/utils"
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./Tooltip"
8
9
 
9
10
  const DropdownMenu = DropdownMenuPrimitive.Root
10
11
 
@@ -47,7 +48,7 @@ const DropdownMenuSubContent = React.forwardRef<
47
48
  <DropdownMenuPrimitive.SubContent
48
49
  ref={ref}
49
50
  className={cn(
50
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
+ "z-50 min-w-[8rem] overflow-hidden rounded-md rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
52
  className
52
53
  )}
53
54
  {...props}
@@ -58,14 +59,16 @@ DropdownMenuSubContent.displayName =
58
59
 
59
60
  const DropdownMenuContent = React.forwardRef<
60
61
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
61
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
62
- >(({ className, sideOffset = 4, ...props }, ref) => (
63
- <DropdownMenuPrimitive.Portal>
62
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
63
+ portalContainer?: HTMLElement | null
64
+ }
65
+ >(({ className, sideOffset = 4, portalContainer, ...props }, ref) => (
66
+ <DropdownMenuPrimitive.Portal container={portalContainer ?? undefined}>
64
67
  <DropdownMenuPrimitive.Content
65
68
  ref={ref}
66
69
  sideOffset={sideOffset}
67
70
  className={cn(
68
- "z-50 flex flex-col w-[180px] min-w-[8rem] overflow-hidden rounded-md rounded-lg border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
71
+ "z-50 flex w-[180px] min-w-[8rem] flex-col overflow-hidden rounded-md rounded-lg border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
69
72
  className
70
73
  )}
71
74
  {...props}
@@ -78,18 +81,50 @@ const DropdownMenuItem = React.forwardRef<
78
81
  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
79
82
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
80
83
  inset?: boolean
84
+ disabledReason?: React.ReactNode
85
+ disabledReasonLabel?: string
86
+ disabledReasonPortalContainer?: HTMLElement | null
81
87
  }
82
- >(({ className, inset, ...props }, ref) => (
83
- <DropdownMenuPrimitive.Item
84
- ref={ref}
85
- className={cn(
86
- "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
87
- inset && "pl-8",
88
- className
89
- )}
90
- {...props}
91
- />
92
- ))
88
+ >(({ className, inset, disabledReason, disabledReasonLabel, disabledReasonPortalContainer, disabled, onSelect, ...props }, ref) => {
89
+ const item = (
90
+ <DropdownMenuPrimitive.Item
91
+ ref={ref}
92
+ disabled={disabled}
93
+ onSelect={(event) => {
94
+ if (disabled) {
95
+ event.preventDefault()
96
+ return
97
+ }
98
+ onSelect?.(event)
99
+ }}
100
+ className={cn(
101
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
102
+ inset && "pl-8",
103
+ className
104
+ )}
105
+ {...props}
106
+ />
107
+ )
108
+
109
+ if (!disabled || !disabledReason) return item
110
+
111
+ return (
112
+ <Tooltip>
113
+ <TooltipTrigger asChild>
114
+ <span
115
+ className="block w-full"
116
+ tabIndex={0}
117
+ aria-label={disabledReasonLabel ?? (typeof disabledReason === "string" ? disabledReason : undefined)}
118
+ >
119
+ {item}
120
+ </span>
121
+ </TooltipTrigger>
122
+ <TooltipContent portalContainer={disabledReasonPortalContainer} className="max-w-64 text-left">
123
+ {disabledReason}
124
+ </TooltipContent>
125
+ </Tooltip>
126
+ )
127
+ })
93
128
  DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94
129
 
95
130
  const DropdownMenuCheckboxItem = React.forwardRef<
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ import React from 'react';
4
+ import { cn } from '../../lib/utils';
5
+ import { motion, HTMLMotionProps, useDragControls } from 'framer-motion';
6
+
7
+ interface FloatingPanelProps extends HTMLMotionProps<"div"> {
8
+ children: React.ReactNode;
9
+ className?: string;
10
+ variant?: 'glass' | 'solid' | 'ghost';
11
+ title?: string;
12
+ contentClassName?: string;
13
+ dragEnabled?: boolean;
14
+ resizable?: boolean;
15
+ minWidth?: number;
16
+ minHeight?: number;
17
+ dragHandleClassName?: string;
18
+ }
19
+
20
+ export const FloatingPanel: React.FC<FloatingPanelProps> = ({
21
+ children,
22
+ className,
23
+ variant = 'glass',
24
+ title,
25
+ contentClassName,
26
+ dragEnabled = false,
27
+ resizable = false,
28
+ minWidth = 220,
29
+ minHeight = 140,
30
+ dragHandleClassName,
31
+ drag,
32
+ dragListener,
33
+ dragMomentum,
34
+ style,
35
+ ...props
36
+ }) => {
37
+ const dragControls = useDragControls();
38
+ const hasHeaderDragHandle = dragEnabled && Boolean(title);
39
+ const resolvedDrag = drag ?? (dragEnabled ? true : false);
40
+
41
+ return (
42
+ <motion.div
43
+ initial={{ opacity: 0, y: 10, scale: 0.98 }}
44
+ animate={{ opacity: 1, y: 0, scale: 1 }}
45
+ exit={{ opacity: 0, y: 10, scale: 0.98 }}
46
+ transition={{ type: 'spring', stiffness: 300, damping: 30 }}
47
+ drag={resolvedDrag}
48
+ dragControls={hasHeaderDragHandle ? dragControls : undefined}
49
+ dragListener={hasHeaderDragHandle ? false : dragListener}
50
+ dragMomentum={dragMomentum ?? false}
51
+ style={{
52
+ minWidth: resizable ? minWidth : undefined,
53
+ minHeight: resizable ? minHeight : undefined,
54
+ ...style,
55
+ }}
56
+ className={cn(
57
+ "relative w-[360px] rounded-2xl border flex flex-col overflow-hidden transition-[box-shadow,border-color,background-color] duration-200 hover:shadow-lg", // Avoid transitioning transform so drag follows the pointer.
58
+ variant === 'glass' && "bg-background/80 backdrop-blur-md border-border/70",
59
+ variant === 'solid' && "bg-background border-border",
60
+ variant === 'ghost' && "bg-transparent border-transparent shadow-none hover:shadow-none",
61
+ dragEnabled && !hasHeaderDragHandle && "cursor-grab active:cursor-grabbing",
62
+ resizable && "resize",
63
+ className
64
+ )}
65
+ {...props}
66
+ >
67
+ {title && (
68
+ <div
69
+ className={cn(
70
+ "px-4 py-3 border-b border-border/60 flex items-center justify-between shrink-0",
71
+ hasHeaderDragHandle && "cursor-grab touch-none select-none active:cursor-grabbing",
72
+ dragHandleClassName
73
+ )}
74
+ onPointerDown={hasHeaderDragHandle ? (event) => dragControls.start(event) : undefined}
75
+ >
76
+ <h3 className="font-semibold text-sm text-foreground">{title}</h3>
77
+ </div>
78
+ )}
79
+ <div className={cn("flex-1 overflow-auto", contentClassName)}>
80
+ {children}
81
+ </div>
82
+ {resizable && (
83
+ <div
84
+ aria-hidden
85
+ className="pointer-events-none absolute bottom-1 right-1 h-3 w-3 rounded-br-[10px] border-b border-r border-muted-foreground/40"
86
+ />
87
+ )}
88
+ </motion.div>
89
+ );
90
+ };
@@ -0,0 +1,36 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ const HoverCard = HoverCardPrimitive.Root
9
+
10
+ const HoverCardTrigger = HoverCardPrimitive.Trigger
11
+
12
+ interface HoverCardContentProps
13
+ extends React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> {
14
+ portalContainer?: HTMLElement | null
15
+ }
16
+
17
+ const HoverCardContent = React.forwardRef<
18
+ React.ElementRef<typeof HoverCardPrimitive.Content>,
19
+ HoverCardContentProps
20
+ >(({ className, align = "center", sideOffset = 4, portalContainer, ...props }, ref) => (
21
+ <HoverCardPrimitive.Portal container={portalContainer}>
22
+ <HoverCardPrimitive.Content
23
+ ref={ref}
24
+ align={align}
25
+ sideOffset={sideOffset}
26
+ className={cn(
27
+ "z-50 flex w-64 w-[256px] flex-col items-center items-start gap-2 rounded-md border bg-popover p-4 text-left text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
28
+ className
29
+ )}
30
+ {...props}
31
+ />
32
+ </HoverCardPrimitive.Portal>
33
+ ))
34
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
35
+
36
+ export { HoverCard, HoverCardTrigger, HoverCardContent }
@@ -0,0 +1,403 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconChevronLeft as ChevronLeft, IconChevronRight as ChevronRight, IconHeart as Heart, IconInfoCircle as Info, IconMaximize as Maximize2, IconPencil as Pencil, IconRotate as RotateCcw, IconRotateClockwise as RotateCw, IconShare2 as Share2, IconX as X, IconZoomIn as ZoomIn, IconZoomOut as ZoomOut } from "@tabler/icons-react";
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import type { AssetCardAsset } from "../display/AssetCard"
8
+ import { Button } from "../inputs/Button"
9
+ import { Slider } from "../inputs/Slider"
10
+ import { TooltipButton } from "../inputs/TooltipButton"
11
+ import {
12
+ Dialog,
13
+ DialogContent,
14
+ DialogDescription,
15
+ DialogTitle,
16
+ } from "./Dialog"
17
+ import { Popover, PopoverContent, PopoverTrigger } from "./Popover"
18
+ import { mediaLightboxDefaultVariantKey } from "./generated/default-variant-keys"
19
+ import type { MediaLightboxVariantKey } from "./generated/variant-keys"
20
+
21
+ export interface MediaLightboxLabels {
22
+ close?: string
23
+ previous?: string
24
+ next?: string
25
+ fitWidth?: string
26
+ zoomIn?: string
27
+ zoomOut?: string
28
+ reset?: string
29
+ rotate?: string
30
+ save?: string
31
+ share?: string
32
+ favorite?: string
33
+ edit?: string
34
+ details?: string
35
+ metadata?: string
36
+ dimensions?: string
37
+ type?: string
38
+ size?: string
39
+ created?: string
40
+ rating?: string
41
+ }
42
+
43
+ export interface MediaLightboxProps {
44
+ open: boolean
45
+ onOpenChange: (open: boolean) => void
46
+ asset?: AssetCardAsset | null
47
+ portalContainer?: HTMLElement | null
48
+ variant?: MediaLightboxVariantKey
49
+ labels?: MediaLightboxLabels
50
+ hasPrevious?: boolean
51
+ hasNext?: boolean
52
+ onPrevious?: () => void
53
+ onNext?: () => void
54
+ onShare?: (asset: AssetCardAsset) => void
55
+ onFavorite?: (asset: AssetCardAsset) => void
56
+ onDetails?: (asset: AssetCardAsset) => void
57
+ }
58
+
59
+ const variantClasses: Record<MediaLightboxVariantKey, { chrome: string }> = {
60
+ default: {
61
+ chrome: "p-2 sm:p-4",
62
+ },
63
+ compact: {
64
+ chrome: "p-2 sm:p-3",
65
+ },
66
+ }
67
+
68
+ function metadataRows(asset: AssetCardAsset, labels?: MediaLightboxLabels) {
69
+ return [
70
+ asset.width && asset.height
71
+ ? { label: labels?.dimensions ?? "Dimensions", value: `${asset.width} x ${asset.height}` }
72
+ : null,
73
+ asset.type ? { label: labels?.type ?? "Type", value: asset.type } : null,
74
+ asset.size ? { label: labels?.size ?? "Size", value: asset.size } : null,
75
+ asset.createdAt ? { label: labels?.created ?? "Created", value: asset.createdAt } : null,
76
+ asset.rating !== undefined ? { label: labels?.rating ?? "Rating", value: `${asset.rating.toFixed(1)}/5` } : null,
77
+ ].filter(Boolean) as Array<{ label: string; value: React.ReactNode }>
78
+ }
79
+
80
+ const MediaLightbox = React.forwardRef<HTMLDivElement, MediaLightboxProps>(
81
+ (
82
+ {
83
+ open,
84
+ onOpenChange,
85
+ asset,
86
+ portalContainer,
87
+ variant = mediaLightboxDefaultVariantKey,
88
+ labels,
89
+ hasPrevious,
90
+ hasNext,
91
+ onPrevious,
92
+ onNext,
93
+ onShare,
94
+ onFavorite,
95
+ onDetails,
96
+ },
97
+ ref
98
+ ) => {
99
+ const [scale, setScale] = React.useState(1)
100
+ const [fitWidth, setFitWidth] = React.useState(false)
101
+ const [rotation, setRotation] = React.useState(0)
102
+ const [editing, setEditing] = React.useState(false)
103
+ const classes = variantClasses[variant]
104
+
105
+ React.useEffect(() => {
106
+ setScale(1)
107
+ setFitWidth(false)
108
+ setRotation(0)
109
+ setEditing(false)
110
+ }, [asset?.id, open])
111
+
112
+ const changeScale = (next: number) => {
113
+ setFitWidth(false)
114
+ setScale(Math.min(3.5, Math.max(0.5, next)))
115
+ }
116
+ const resetView = () => {
117
+ setScale(1)
118
+ setFitWidth(false)
119
+ setRotation(0)
120
+ setEditing(false)
121
+ }
122
+ const zoomPercent = Math.round(scale * 100)
123
+ const rows = asset ? metadataRows(asset, labels) : []
124
+
125
+ return (
126
+ <Dialog open={open} onOpenChange={onOpenChange}>
127
+ <DialogContent
128
+ ref={ref}
129
+ portalContainer={portalContainer}
130
+ overlayClassName={portalContainer ? "!absolute" : undefined}
131
+ showCloseButton={false}
132
+ className={cn(
133
+ "h-[92vh] w-[96vw] max-w-none overflow-hidden border-border bg-foreground p-0 text-background sm:rounded-lg",
134
+ portalContainer && "!absolute !inset-0 !left-0 !top-0 !h-full !w-full !translate-x-0 !translate-y-0 !transform-none !rounded-none !border-0 data-[state=open]:!animate-none data-[state=closed]:!animate-none sm:!rounded-none"
135
+ )}
136
+ >
137
+ <DialogTitle className="sr-only">{asset?.title ?? "Media preview"}</DialogTitle>
138
+ <DialogDescription className="sr-only">
139
+ {asset ? `${asset.title} preview` : "Media preview"}
140
+ </DialogDescription>
141
+ <div className="relative flex h-full w-full flex-col overflow-hidden bg-foreground p-0">
142
+ <div className="pointer-events-none absolute inset-x-0 top-0 z-20 flex items-start justify-end gap-2 p-4">
143
+ {editing ? (
144
+ <div className="pointer-events-auto absolute left-1/2 top-4 flex -translate-x-1/2 items-center gap-2 rounded-full border border-background/10 bg-background/10 p-2 shadow-lg backdrop-blur">
145
+ <TooltipButton
146
+ type="button"
147
+ variant="ghost"
148
+ size="icon"
149
+ className="h-9 w-9 rounded-full text-background hover:bg-background/15 hover:text-background"
150
+ aria-label={labels?.rotate ?? "Rotate"}
151
+ tooltip={labels?.rotate ?? "Rotate"}
152
+ tooltipSide="bottom"
153
+ tooltipPortalContainer={portalContainer}
154
+ tooltipCloseOnPress
155
+ onClick={() => setRotation((value) => value + 90)}
156
+ >
157
+ <RotateCw className="h-4 w-4" />
158
+ </TooltipButton>
159
+ <span className="h-6 w-px bg-background/20" aria-hidden="true" />
160
+ <Button type="button" className="h-9 rounded-full px-5" onClick={() => setEditing(false)}>
161
+ {labels?.save ?? "Save"}
162
+ </Button>
163
+ </div>
164
+ ) : null}
165
+ {asset && onShare ? (
166
+ <TooltipButton
167
+ type="button"
168
+ variant="ghost"
169
+ size="icon"
170
+ className="pointer-events-auto h-10 w-10 rounded-full bg-background/10 text-background hover:bg-background/20 hover:text-background"
171
+ aria-label={labels?.share ?? "Share"}
172
+ tooltip={labels?.share ?? "Share"}
173
+ tooltipSide="bottom"
174
+ tooltipPortalContainer={portalContainer}
175
+ tooltipCloseOnPress
176
+ onClick={() => onShare(asset)}
177
+ >
178
+ <Share2 className="h-5 w-5" />
179
+ </TooltipButton>
180
+ ) : null}
181
+ {asset && onFavorite ? (
182
+ <TooltipButton
183
+ type="button"
184
+ variant="ghost"
185
+ size="icon"
186
+ className={cn(
187
+ "pointer-events-auto h-10 w-10 rounded-full bg-background/10 text-background hover:bg-background/20 hover:text-background",
188
+ asset.isFavorite && "text-primary hover:text-primary"
189
+ )}
190
+ aria-label={labels?.favorite ?? "Favorite"}
191
+ tooltip={labels?.favorite ?? "Favorite"}
192
+ tooltipSide="bottom"
193
+ tooltipPortalContainer={portalContainer}
194
+ tooltipCloseOnPress
195
+ aria-pressed={asset.isFavorite}
196
+ onClick={() => onFavorite(asset)}
197
+ >
198
+ <Heart className={cn("h-5 w-5", asset.isFavorite && "fill-current")} />
199
+ </TooltipButton>
200
+ ) : null}
201
+ <TooltipButton
202
+ type="button"
203
+ variant="ghost"
204
+ size="icon"
205
+ className={cn("pointer-events-auto h-10 w-10 rounded-full bg-background/10 text-background hover:bg-background/20 hover:text-background", editing && "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground")}
206
+ aria-label={labels?.edit ?? "Edit"}
207
+ tooltip={labels?.edit ?? "Edit"}
208
+ tooltipSide="bottom"
209
+ tooltipPortalContainer={portalContainer}
210
+ tooltipCloseOnPress
211
+ aria-pressed={editing}
212
+ onClick={() => setEditing((value) => !value)}
213
+ >
214
+ <Pencil className="h-5 w-5" />
215
+ </TooltipButton>
216
+ {asset ? (
217
+ <Popover>
218
+ <PopoverTrigger asChild>
219
+ <TooltipButton
220
+ type="button"
221
+ variant="ghost"
222
+ size="icon"
223
+ className="pointer-events-auto h-10 w-10 rounded-full bg-background/10 text-background hover:bg-background/20 hover:text-background"
224
+ aria-label={labels?.details ?? "Info"}
225
+ tooltip={labels?.details ?? "Info"}
226
+ tooltipSide="bottom"
227
+ tooltipPortalContainer={portalContainer}
228
+ tooltipCloseOnPress
229
+ >
230
+ <Info className="h-5 w-5" />
231
+ </TooltipButton>
232
+ </PopoverTrigger>
233
+ <PopoverContent portalContainer={portalContainer} align="end" side="bottom" className="w-72 border-background/10 bg-background text-foreground">
234
+ <div className="space-y-4">
235
+ <div className="min-w-0">
236
+ <p className="truncate text-sm font-semibold">{asset.title}</p>
237
+ <p className="mt-1 text-xs text-muted-foreground">{labels?.metadata ?? "Metadata"}</p>
238
+ </div>
239
+ <dl className="space-y-2">
240
+ {rows.map((row) => (
241
+ <div key={row.label} className="flex items-center justify-between gap-4 text-sm">
242
+ <dt className="text-muted-foreground">{row.label}</dt>
243
+ <dd className="truncate text-right font-medium">{row.value}</dd>
244
+ </div>
245
+ ))}
246
+ </dl>
247
+ {onDetails ? (
248
+ <Button type="button" variant="secondary" className="w-full" onClick={() => onDetails(asset)}>
249
+ {labels?.details ?? "Details"}
250
+ </Button>
251
+ ) : null}
252
+ </div>
253
+ </PopoverContent>
254
+ </Popover>
255
+ ) : null}
256
+ <TooltipButton
257
+ type="button"
258
+ variant="ghost"
259
+ size="icon"
260
+ className="pointer-events-auto h-10 w-10 rounded-full bg-background/10 text-background hover:bg-background/20 hover:text-background"
261
+ aria-label={labels?.close ?? "Close"}
262
+ tooltip={labels?.close ?? "Close"}
263
+ tooltipSide="bottom"
264
+ tooltipPortalContainer={portalContainer}
265
+ tooltipCloseOnPress
266
+ onClick={() => onOpenChange(false)}
267
+ >
268
+ <X className="h-5 w-5" />
269
+ </TooltipButton>
270
+ </div>
271
+
272
+ <div className="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-foreground p-0">
273
+ {hasPrevious ? (
274
+ <TooltipButton
275
+ type="button"
276
+ variant="ghost"
277
+ size="icon"
278
+ className="absolute left-4 top-1/2 z-10 h-12 w-12 -translate-y-1/2 rounded-full bg-background/10 text-background hover:bg-background/20 hover:text-background"
279
+ aria-label={labels?.previous ?? "Previous"}
280
+ tooltip={labels?.previous ?? "Previous"}
281
+ tooltipSide="right"
282
+ tooltipPortalContainer={portalContainer}
283
+ tooltipCloseOnPress
284
+ onClick={onPrevious}
285
+ >
286
+ <ChevronLeft className="h-6 w-6" />
287
+ </TooltipButton>
288
+ ) : null}
289
+ {asset?.src ? (
290
+ <div className="flex h-full w-full items-center justify-center overflow-hidden px-20 pb-32 pt-24 sm:px-24">
291
+ <img
292
+ src={asset.src}
293
+ alt={asset.alt ?? asset.title}
294
+ className={cn(
295
+ "select-none object-contain transition-transform",
296
+ fitWidth ? "w-full max-w-none" : "max-h-full max-w-full"
297
+ )}
298
+ style={{ transform: `scale(${scale}) rotate(${rotation}deg)` }}
299
+ draggable={false}
300
+ />
301
+ </div>
302
+ ) : (
303
+ <div className="text-sm text-background/70">No preview</div>
304
+ )}
305
+ {hasNext ? (
306
+ <TooltipButton
307
+ type="button"
308
+ variant="ghost"
309
+ size="icon"
310
+ className="absolute right-4 top-1/2 z-10 h-12 w-12 -translate-y-1/2 rounded-full bg-background/10 text-background hover:bg-background/20 hover:text-background"
311
+ aria-label={labels?.next ?? "Next"}
312
+ tooltip={labels?.next ?? "Next"}
313
+ tooltipSide="left"
314
+ tooltipPortalContainer={portalContainer}
315
+ tooltipCloseOnPress
316
+ onClick={onNext}
317
+ >
318
+ <ChevronRight className="h-6 w-6" />
319
+ </TooltipButton>
320
+ ) : null}
321
+ </div>
322
+
323
+ {asset ? (
324
+ <div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 flex justify-center p-2 sm:p-4">
325
+ <div className={cn("pointer-events-auto grid w-[calc(100%-1rem)] max-w-lg grid-cols-[auto_auto_auto_minmax(0,1fr)_auto_auto] items-center gap-2 rounded-2xl border border-background/10 bg-background/10 shadow-lg backdrop-blur sm:rounded-full", classes.chrome)}>
326
+ <TooltipButton
327
+ type="button"
328
+ variant="ghost"
329
+ size="icon"
330
+ className="h-8 w-8 shrink-0 rounded-full text-background hover:bg-background/15 hover:text-background sm:h-9 sm:w-9"
331
+ aria-label={labels?.fitWidth ?? "Fit width"}
332
+ tooltip={labels?.fitWidth ?? "Fit width"}
333
+ tooltipSide="top"
334
+ tooltipPortalContainer={portalContainer}
335
+ tooltipCloseOnPress
336
+ onClick={() => { setFitWidth(true); setScale(1); }}
337
+ >
338
+ <Maximize2 className="h-4 w-4" />
339
+ </TooltipButton>
340
+ <TooltipButton
341
+ type="button"
342
+ variant="ghost"
343
+ size="icon"
344
+ className="h-8 w-8 shrink-0 rounded-full text-background hover:bg-background/15 hover:text-background sm:h-9 sm:w-9"
345
+ aria-label={labels?.reset ?? "Reset"}
346
+ tooltip={labels?.reset ?? "Reset"}
347
+ tooltipSide="top"
348
+ tooltipPortalContainer={portalContainer}
349
+ tooltipCloseOnPress
350
+ onClick={resetView}
351
+ >
352
+ <RotateCcw className="h-4 w-4" />
353
+ </TooltipButton>
354
+ <TooltipButton
355
+ type="button"
356
+ variant="ghost"
357
+ size="icon"
358
+ className="h-8 w-8 shrink-0 rounded-full text-background hover:bg-background/15 hover:text-background sm:h-9 sm:w-9"
359
+ aria-label={labels?.zoomOut ?? "Zoom out"}
360
+ tooltip={labels?.zoomOut ?? "Zoom out"}
361
+ tooltipSide="top"
362
+ tooltipPortalContainer={portalContainer}
363
+ tooltipCloseOnPress
364
+ onClick={() => changeScale(scale - 0.1)}
365
+ >
366
+ <ZoomOut className="h-4 w-4" />
367
+ </TooltipButton>
368
+ <Slider
369
+ value={String(scale)}
370
+ min={0.5}
371
+ max={3.5}
372
+ step={0.1}
373
+ onChange={(event) => changeScale(Number(event.currentTarget.value))}
374
+ className="!w-full min-w-0"
375
+ aria-label="Zoom"
376
+ />
377
+ <TooltipButton
378
+ type="button"
379
+ variant="ghost"
380
+ size="icon"
381
+ className="h-8 w-8 shrink-0 rounded-full text-background hover:bg-background/15 hover:text-background sm:h-9 sm:w-9"
382
+ aria-label={labels?.zoomIn ?? "Zoom in"}
383
+ tooltip={labels?.zoomIn ?? "Zoom in"}
384
+ tooltipSide="top"
385
+ tooltipPortalContainer={portalContainer}
386
+ tooltipCloseOnPress
387
+ onClick={() => changeScale(scale + 0.1)}
388
+ >
389
+ <ZoomIn className="h-4 w-4" />
390
+ </TooltipButton>
391
+ <span className="w-11 shrink-0 text-right text-xs tabular-nums text-background/70 sm:w-12">{zoomPercent}%</span>
392
+ </div>
393
+ </div>
394
+ ) : null}
395
+ </div>
396
+ </DialogContent>
397
+ </Dialog>
398
+ )
399
+ }
400
+ )
401
+ MediaLightbox.displayName = "MediaLightbox"
402
+
403
+ export { MediaLightbox }