@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
@@ -0,0 +1,198 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconSearch as Search } from "@tabler/icons-react";
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { AssetGrid } from "../display/AssetGrid"
8
+ import { AssetCard } from "../display/AssetCard"
9
+ import type { AssetCardAsset } from "../display/AssetCard"
10
+ import { Button } from "../inputs/Button"
11
+ import { Input } from "../inputs/Input"
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogFooter,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ } from "./Dialog"
20
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./Tooltip"
21
+ import { mediaPickerDialogDefaultVariantKey } from "./generated/default-variant-keys"
22
+ import type { MediaPickerDialogVariantKey } from "./generated/variant-keys"
23
+
24
+ export interface MediaPickerDialogLabels {
25
+ title?: React.ReactNode
26
+ description?: React.ReactNode
27
+ searchPlaceholder?: string
28
+ cancel?: React.ReactNode
29
+ confirm?: React.ReactNode | ((selectedCount: number) => React.ReactNode)
30
+ confirmDisabledReason?: React.ReactNode
31
+ emptyTitle?: React.ReactNode
32
+ emptyDescription?: React.ReactNode
33
+ close?: string
34
+ }
35
+
36
+ export interface MediaPickerDialogProps {
37
+ open: boolean
38
+ onOpenChange: (open: boolean) => void
39
+ items: AssetCardAsset[]
40
+ portalContainer?: HTMLElement | null
41
+ variant?: MediaPickerDialogVariantKey
42
+ selectedIds?: string[]
43
+ multiSelect?: boolean
44
+ labels?: MediaPickerDialogLabels
45
+ onConfirm: (items: AssetCardAsset[]) => void
46
+ }
47
+
48
+ const variantClasses: Record<MediaPickerDialogVariantKey, { content: string; grid: string }> = {
49
+ default: {
50
+ content: "max-w-5xl",
51
+ grid: "",
52
+ },
53
+ compact: {
54
+ content: "max-w-3xl",
55
+ grid: "",
56
+ },
57
+ }
58
+
59
+ const MediaPickerDialog = React.forwardRef<HTMLDivElement, MediaPickerDialogProps>(
60
+ (
61
+ {
62
+ open,
63
+ onOpenChange,
64
+ items,
65
+ portalContainer,
66
+ variant = mediaPickerDialogDefaultVariantKey,
67
+ selectedIds,
68
+ multiSelect,
69
+ labels,
70
+ onConfirm,
71
+ },
72
+ ref
73
+ ) => {
74
+ const [query, setQuery] = React.useState("")
75
+ const [internalSelectedIds, setInternalSelectedIds] = React.useState<string[]>(selectedIds ?? [])
76
+ const classes = variantClasses[variant]
77
+
78
+ React.useEffect(() => {
79
+ if (open) setInternalSelectedIds(selectedIds ?? [])
80
+ }, [open, selectedIds])
81
+
82
+ const selectedSet = React.useMemo(() => new Set(internalSelectedIds), [internalSelectedIds])
83
+ const filteredItems = React.useMemo(() => {
84
+ const normalized = query.trim().toLowerCase()
85
+ if (!normalized) return items
86
+ return items.filter((item) => item.title.toLowerCase().includes(normalized) || item.type?.toLowerCase().includes(normalized))
87
+ }, [items, query])
88
+
89
+ const handleSelect = (asset: AssetCardAsset) => {
90
+ if (!multiSelect) {
91
+ setInternalSelectedIds([asset.id])
92
+ onConfirm([asset])
93
+ onOpenChange(false)
94
+ return
95
+ }
96
+ setInternalSelectedIds((current) =>
97
+ current.includes(asset.id)
98
+ ? current.filter((id) => id !== asset.id)
99
+ : [...current, asset.id]
100
+ )
101
+ }
102
+
103
+ const handleConfirm = () => {
104
+ onConfirm(items.filter((item) => selectedSet.has(item.id)))
105
+ onOpenChange(false)
106
+ }
107
+ const confirmLabel =
108
+ typeof labels?.confirm === "function"
109
+ ? labels.confirm(internalSelectedIds.length)
110
+ : labels?.confirm ?? `${internalSelectedIds.length} selected`
111
+ const confirmDisabledReason = labels?.confirmDisabledReason ?? "Select at least one asset before confirming."
112
+
113
+ return (
114
+ <Dialog open={open} onOpenChange={onOpenChange}>
115
+ <DialogContent
116
+ ref={ref}
117
+ portalContainer={portalContainer}
118
+ closeLabel={labels?.close}
119
+ className={cn(
120
+ "grid h-[calc(100%-2rem)] max-h-[calc(100%-2rem)] w-[calc(100%-2rem)] grid-rows-[auto_minmax(0,1fr)_auto] gap-0 overflow-hidden p-0",
121
+ classes.content
122
+ )}
123
+ >
124
+ <DialogHeader className="border-b p-6 pb-4">
125
+ <DialogTitle>{labels?.title ?? "Choose media"}</DialogTitle>
126
+ <DialogDescription>
127
+ {labels?.description ?? "Select assets from the library."}
128
+ </DialogDescription>
129
+ </DialogHeader>
130
+
131
+ <div className={cn("grid min-h-0 grid-rows-[auto_minmax(0,1fr)] gap-4 overflow-hidden p-6", !multiSelect && "pb-8")}>
132
+ <div className="relative">
133
+ <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" aria-hidden="true" />
134
+ <Input
135
+ value={query}
136
+ onChange={(event) => setQuery(event.target.value)}
137
+ placeholder={labels?.searchPlaceholder ?? "Search assets..."}
138
+ className="w-full pl-9"
139
+ />
140
+ </div>
141
+ <div className="min-h-0 overflow-y-auto pb-4 pr-1">
142
+ <AssetGrid
143
+ items={filteredItems}
144
+ selectedIds={internalSelectedIds}
145
+ selectionMode={multiSelect ? "multiple" : "single"}
146
+ onSelect={handleSelect}
147
+ emptyTitle={labels?.emptyTitle ?? "No assets"}
148
+ emptyDescription={labels?.emptyDescription ?? "Try a different search query."}
149
+ minColumnWidth={150}
150
+ className={classes.grid}
151
+ renderItem={(asset, state) => (
152
+ <AssetCard
153
+ asset={asset}
154
+ selected={state.selected}
155
+ variant={variant === "compact" ? "compact" : "default"}
156
+ selectionMode={multiSelect ? "multiple" : "single"}
157
+ imageFit="contain"
158
+ portalContainer={portalContainer}
159
+ onSelect={handleSelect}
160
+ />
161
+ )}
162
+ />
163
+ </div>
164
+ </div>
165
+
166
+ {multiSelect ? (
167
+ <DialogFooter className="border-t p-4">
168
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
169
+ {labels?.cancel ?? "Cancel"}
170
+ </Button>
171
+ {internalSelectedIds.length === 0 ? (
172
+ <Tooltip>
173
+ <TooltipTrigger asChild>
174
+ <span className="inline-flex">
175
+ <Button type="button" disabled>
176
+ {confirmLabel}
177
+ </Button>
178
+ </span>
179
+ </TooltipTrigger>
180
+ <TooltipContent portalContainer={portalContainer}>
181
+ {confirmDisabledReason}
182
+ </TooltipContent>
183
+ </Tooltip>
184
+ ) : (
185
+ <Button type="button" onClick={handleConfirm}>
186
+ {confirmLabel}
187
+ </Button>
188
+ )}
189
+ </DialogFooter>
190
+ ) : null}
191
+ </DialogContent>
192
+ </Dialog>
193
+ )
194
+ }
195
+ )
196
+ MediaPickerDialog.displayName = "MediaPickerDialog"
197
+
198
+ export { MediaPickerDialog }
@@ -0,0 +1,103 @@
1
+ "use client"
2
+ import React, { useEffect, useRef } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { IconX as X } from "@tabler/icons-react";
5
+ import { cn } from '../../lib/utils';
6
+
7
+ export interface ModalProps {
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ title: string;
11
+ children: React.ReactNode;
12
+ footer?: React.ReactNode;
13
+ portalContainer?: HTMLElement | null;
14
+ closeLabel?: string;
15
+ overlayClassName?: string;
16
+ className?: string;
17
+ }
18
+
19
+ export function Modal({
20
+ isOpen,
21
+ onClose,
22
+ title,
23
+ children,
24
+ footer,
25
+ portalContainer,
26
+ closeLabel = "Close",
27
+ overlayClassName,
28
+ className,
29
+ }: ModalProps) {
30
+ const modalRef = useRef<HTMLDivElement>(null);
31
+
32
+ useEffect(() => {
33
+ const handleEscape = (e: KeyboardEvent) => {
34
+ if (e.key === 'Escape') onClose();
35
+ };
36
+
37
+ if (isOpen && !portalContainer) {
38
+ document.addEventListener('keydown', handleEscape);
39
+ document.body.style.overflow = 'hidden';
40
+ }
41
+
42
+ return () => {
43
+ document.removeEventListener('keydown', handleEscape);
44
+ if (!portalContainer) {
45
+ document.body.style.overflow = 'unset';
46
+ }
47
+ };
48
+ }, [isOpen, onClose, portalContainer]);
49
+
50
+ if (!isOpen) return null;
51
+
52
+ const content = (
53
+ <div
54
+ data-prevent-deselect="true"
55
+ className={cn(
56
+ "fixed inset-0 z-[9999] flex items-center justify-center bg-overlay/60 backdrop-blur-sm animate-in fade-in duration-200",
57
+ portalContainer && "absolute",
58
+ overlayClassName
59
+ )}
60
+ onClick={(e) => {
61
+ e.stopPropagation();
62
+ onClose();
63
+ }}
64
+ >
65
+ <div
66
+ ref={modalRef}
67
+ role="dialog"
68
+ aria-modal="true"
69
+ aria-label={title}
70
+ onClick={(e) => e.stopPropagation()}
71
+ className={cn(
72
+ "mx-4 flex w-[448px] max-w-md max-w-[calc(100%-2rem)] flex-col overflow-hidden rounded-lg border border-border bg-card shadow-xl animate-in zoom-in-95 duration-200",
73
+ className
74
+ )}
75
+ >
76
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border">
77
+ <h3 className="min-w-0 pr-3 font-semibold text-foreground">{title}</h3>
78
+ <button
79
+ type="button"
80
+ onClick={onClose}
81
+ className="rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
82
+ aria-label={closeLabel}
83
+ >
84
+ <X size={18} />
85
+ </button>
86
+ </div>
87
+ <div className="p-4">
88
+ {children}
89
+ </div>
90
+ {footer && (
91
+ <div className="px-4 py-3 bg-muted/50 border-t border-border flex justify-end gap-2">
92
+ {footer}
93
+ </div>
94
+ )}
95
+ </div>
96
+ </div>
97
+ );
98
+
99
+ if (typeof document !== 'undefined') {
100
+ return createPortal(content, portalContainer ?? document.body);
101
+ }
102
+ return content;
103
+ }
@@ -0,0 +1,172 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconCheck as Check, IconChevronLeft as ChevronLeft, IconChevronRight as ChevronRight } from "@tabler/icons-react";
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { Button } from "../inputs/Button"
8
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip"
9
+
10
+ export interface OnboardingStep {
11
+ id: string
12
+ title: React.ReactNode
13
+ description?: React.ReactNode
14
+ content: React.ReactNode
15
+ }
16
+
17
+ export interface OnboardingFlowProps {
18
+ steps: OnboardingStep[]
19
+ /** Controlled current step index (0-based). */
20
+ currentIndex?: number
21
+ onCurrentIndexChange?: (index: number) => void
22
+ /** Called when the user clicks Continue on the final step. */
23
+ onComplete?: () => void
24
+ /** Optional override for the back button label. */
25
+ backLabel?: string
26
+ /** Optional override for the next button label (non-final step). */
27
+ nextLabel?: string
28
+ /** Optional override for the final step button label. */
29
+ completeLabel?: string
30
+ /** Accessible label for the step indicator. */
31
+ progressLabel?: string
32
+ /** Text shown above the active step title. */
33
+ stepLabel?: (current: number, total: number) => React.ReactNode
34
+ /** Disabled reason shown on the back button when the first step is active. */
35
+ backDisabledReason?: string
36
+ /** Minimum height for the step title, description, and content area. Keeps footer actions stable across steps. */
37
+ bodyMinHeight?: React.CSSProperties["minHeight"]
38
+ className?: string
39
+ }
40
+
41
+ const OnboardingFlow = React.forwardRef<HTMLDivElement, OnboardingFlowProps>(
42
+ (
43
+ {
44
+ steps,
45
+ currentIndex: controlledIndex,
46
+ onCurrentIndexChange,
47
+ onComplete,
48
+ backLabel = "Back",
49
+ nextLabel = "Continue",
50
+ completeLabel = "Finish",
51
+ progressLabel = "Onboarding progress",
52
+ stepLabel = (current, total) => `Step ${current} of ${total}`,
53
+ backDisabledReason = "This is the first step.",
54
+ bodyMinHeight,
55
+ className,
56
+ },
57
+ ref
58
+ ) => {
59
+ const [internalIndex, setInternalIndex] = React.useState(0)
60
+ const isControlled = controlledIndex !== undefined
61
+ const index = isControlled ? controlledIndex : internalIndex
62
+
63
+ const setIndex = (next: number) => {
64
+ const clamped = Math.max(0, Math.min(steps.length - 1, next))
65
+ if (!isControlled) setInternalIndex(clamped)
66
+ onCurrentIndexChange?.(clamped)
67
+ }
68
+
69
+ const isFirst = index === 0
70
+ const isLast = index === steps.length - 1
71
+ const step = steps[index]
72
+
73
+ if (!step) return null
74
+
75
+ return (
76
+ <div
77
+ ref={ref}
78
+ className={cn(
79
+ "flex w-full flex-col gap-6 rounded-lg border border-border bg-card p-6 text-card-foreground shadow-sm",
80
+ className
81
+ )}
82
+ >
83
+ {/* Step indicator */}
84
+ <ol className="flex items-center gap-2" aria-label={progressLabel}>
85
+ {steps.map((s, idx) => {
86
+ const isComplete = idx < index
87
+ const isActive = idx === index
88
+ return (
89
+ <React.Fragment key={s.id}>
90
+ <li
91
+ aria-current={isActive ? "step" : undefined}
92
+ className={cn(
93
+ "flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold",
94
+ isComplete
95
+ ? "bg-foreground text-background"
96
+ : isActive
97
+ ? "bg-foreground text-background"
98
+ : "border border-border bg-background text-muted-foreground"
99
+ )}
100
+ >
101
+ {isComplete ? (
102
+ <Check className="h-3 w-3" aria-hidden />
103
+ ) : (
104
+ idx + 1
105
+ )}
106
+ </li>
107
+ {idx < steps.length - 1 ? (
108
+ <span
109
+ aria-hidden
110
+ className={cn(
111
+ "h-0.5 flex-1",
112
+ idx < index ? "bg-foreground" : "bg-border"
113
+ )}
114
+ />
115
+ ) : null}
116
+ </React.Fragment>
117
+ )
118
+ })}
119
+ </ol>
120
+
121
+ {/* Content */}
122
+ <div className="flex flex-col gap-6" style={bodyMinHeight ? { minHeight: bodyMinHeight } : undefined}>
123
+ <div className="flex flex-col gap-2">
124
+ <p className="text-xs font-medium text-muted-foreground">
125
+ {stepLabel(index + 1, steps.length)}
126
+ </p>
127
+ <h3 className="text-lg font-semibold tracking-tight">
128
+ {step.title}
129
+ </h3>
130
+ {step.description ? (
131
+ <p className="text-sm text-muted-foreground">{step.description}</p>
132
+ ) : null}
133
+ </div>
134
+ <div>{step.content}</div>
135
+ </div>
136
+
137
+ {/* Footer actions */}
138
+ <div className="flex items-center justify-between">
139
+ <TooltipProvider>
140
+ <Tooltip>
141
+ <TooltipTrigger asChild>
142
+ <span className={cn("inline-flex", isFirst ? "cursor-not-allowed" : "")}>
143
+ <Button
144
+ variant="outline"
145
+ onClick={() => setIndex(index - 1)}
146
+ disabled={isFirst}
147
+ >
148
+ <ChevronLeft className="mr-1 h-4 w-4" />
149
+ {backLabel}
150
+ </Button>
151
+ </span>
152
+ </TooltipTrigger>
153
+ {isFirst ? <TooltipContent>{backDisabledReason}</TooltipContent> : null}
154
+ </Tooltip>
155
+ </TooltipProvider>
156
+ <Button
157
+ onClick={() => {
158
+ if (isLast) onComplete?.()
159
+ else setIndex(index + 1)
160
+ }}
161
+ >
162
+ {isLast ? completeLabel : nextLabel}
163
+ {!isLast ? <ChevronRight className="ml-1 h-4 w-4" /> : null}
164
+ </Button>
165
+ </div>
166
+ </div>
167
+ )
168
+ }
169
+ )
170
+ OnboardingFlow.displayName = "OnboardingFlow"
171
+
172
+ export { OnboardingFlow }
@@ -0,0 +1,36 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ const Popover = PopoverPrimitive.Root
9
+
10
+ const PopoverTrigger = PopoverPrimitive.Trigger
11
+
12
+ const PopoverAnchor = PopoverPrimitive.Anchor
13
+
14
+ const PopoverContent = React.forwardRef<
15
+ React.ElementRef<typeof PopoverPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
17
+ portalContainer?: HTMLElement | null
18
+ }
19
+ >(({ className, align = "center", sideOffset = 4, collisionPadding = 16, portalContainer, ...props }, ref) => (
20
+ <PopoverPrimitive.Portal container={portalContainer ?? undefined}>
21
+ <PopoverPrimitive.Content
22
+ ref={ref}
23
+ align={align}
24
+ sideOffset={sideOffset}
25
+ collisionPadding={collisionPadding}
26
+ className={cn(
27
+ "z-50 flex max-h-[min(var(--radix-popper-available-height),calc(100vh-2rem))] w-72 w-[288px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-md rounded-lg border bg-popover p-4 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
+ </PopoverPrimitive.Portal>
33
+ ))
34
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName
35
+
36
+ export { Popover, PopoverAnchor, PopoverTrigger, PopoverContent }