@gunjo/ui 0.0.1-alpha.1 → 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,279 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+
7
+ export interface MentionOption {
8
+ id: string
9
+ label: string
10
+ /** Hint shown next to label (e.g. real name, role). */
11
+ hint?: string
12
+ }
13
+
14
+ export interface MentionProps
15
+ extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "value"> {
16
+ value?: string
17
+ onValueChange?: (value: string) => void
18
+ /** Trigger character. Default "@". */
19
+ trigger?: string
20
+ /** Available people / items to suggest. */
21
+ options: MentionOption[]
22
+ /** Limit shown suggestions. Default 6. */
23
+ maxSuggestions?: number
24
+ }
25
+
26
+ function escapeRegExp(value: string) {
27
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
28
+ }
29
+
30
+ function getClippingRect(root: HTMLElement) {
31
+ let element = root.parentElement
32
+
33
+ while (element && element !== document.documentElement) {
34
+ const style = window.getComputedStyle(element)
35
+ const overflow = `${style.overflow} ${style.overflowX} ${style.overflowY}`
36
+
37
+ if (/(auto|scroll|hidden|clip)/.test(overflow)) {
38
+ return element.getBoundingClientRect()
39
+ }
40
+
41
+ element = element.parentElement
42
+ }
43
+
44
+ return { top: 0, bottom: window.innerHeight }
45
+ }
46
+
47
+ const Mention = React.forwardRef<HTMLTextAreaElement, MentionProps>(
48
+ (
49
+ {
50
+ className,
51
+ value: controlledValue,
52
+ onValueChange,
53
+ trigger = "@",
54
+ options,
55
+ maxSuggestions = 6,
56
+ disabled,
57
+ placeholder,
58
+ rows = 3,
59
+ ...props
60
+ },
61
+ ref
62
+ ) => {
63
+ const [internalValue, setInternalValue] = React.useState("")
64
+ const isControlled = controlledValue !== undefined
65
+ const value = isControlled ? controlledValue : internalValue
66
+
67
+ const setValue = (next: string) => {
68
+ if (!isControlled) setInternalValue(next)
69
+ onValueChange?.(next)
70
+ }
71
+
72
+ const [activeIndex, setActiveIndex] = React.useState(0)
73
+ const [suggestionsDismissed, setSuggestionsDismissed] = React.useState(false)
74
+ const [suggestionSide, setSuggestionSide] = React.useState<"top" | "bottom">("bottom")
75
+ const [suggestionMaxHeight, setSuggestionMaxHeight] = React.useState<number | undefined>(undefined)
76
+ const rootRef = React.useRef<HTMLDivElement | null>(null)
77
+ const internalRef = React.useRef<HTMLTextAreaElement | null>(null)
78
+ const setRef = (node: HTMLTextAreaElement | null) => {
79
+ internalRef.current = node
80
+ if (typeof ref === "function") ref(node)
81
+ else if (ref) ref.current = node
82
+ }
83
+
84
+ const triggerInfo = React.useMemo(() => {
85
+ const el = internalRef.current
86
+ const cursor = el?.selectionStart ?? value.length
87
+ const before = value.slice(0, cursor)
88
+ const triggerIdx = before.lastIndexOf(trigger)
89
+ if (triggerIdx === -1) return null
90
+ // Reject if there's whitespace between trigger and cursor (means we left the mention scope)
91
+ const segment = before.slice(triggerIdx + trigger.length)
92
+ if (/\s/.test(segment)) return null
93
+ return { triggerIdx, query: segment }
94
+ }, [value, trigger])
95
+
96
+ const filtered = React.useMemo(() => {
97
+ if (!triggerInfo) return []
98
+ const q = triggerInfo.query.toLowerCase()
99
+ return options
100
+ .filter((o) => o.label.toLowerCase().includes(q))
101
+ .slice(0, maxSuggestions)
102
+ }, [triggerInfo, options, maxSuggestions])
103
+
104
+ React.useEffect(() => {
105
+ setActiveIndex(0)
106
+ }, [triggerInfo?.query])
107
+
108
+ const showSuggestions = !suggestionsDismissed && !!triggerInfo && filtered.length > 0
109
+ const mentionPattern = React.useMemo(() => {
110
+ const labels = options
111
+ .map((option) => option.label)
112
+ .filter(Boolean)
113
+ .sort((a, b) => b.length - a.length)
114
+ .map(escapeRegExp)
115
+ if (labels.length === 0) return null
116
+ return new RegExp(`${escapeRegExp(trigger)}(${labels.join("|")})(?=\\s|$)`, "g")
117
+ }, [options, trigger])
118
+
119
+ const highlightedValue = React.useMemo(() => {
120
+ if (!mentionPattern || value.length === 0) return value
121
+
122
+ const parts: React.ReactNode[] = []
123
+ let lastIndex = 0
124
+ for (const match of value.matchAll(mentionPattern)) {
125
+ const index = match.index ?? 0
126
+ if (index > lastIndex) {
127
+ parts.push(value.slice(lastIndex, index))
128
+ }
129
+ parts.push(
130
+ <span key={`${index}-${match[0]}`} className="font-medium text-primary">
131
+ {match[0]}
132
+ </span>
133
+ )
134
+ lastIndex = index + match[0].length
135
+ }
136
+ if (lastIndex < value.length) {
137
+ parts.push(value.slice(lastIndex))
138
+ }
139
+ return parts
140
+ }, [mentionPattern, value])
141
+
142
+ React.useLayoutEffect(() => {
143
+ if (!showSuggestions) return
144
+ const root = rootRef.current
145
+ if (!root) return
146
+ const rootRect = root.getBoundingClientRect()
147
+ const contentFitEmbed =
148
+ !!root.closest("[data-embed-fit-height-content='true']") ||
149
+ (typeof window !== "undefined" &&
150
+ new URLSearchParams(window.location.search).get("fitHeight") === "content")
151
+ const estimatedHeight = Math.min(filtered.length, maxSuggestions) * 36 + 8
152
+ if (contentFitEmbed) {
153
+ setSuggestionSide("bottom")
154
+ setSuggestionMaxHeight(Math.min(estimatedHeight, 4 * 36 + 8))
155
+ return
156
+ }
157
+ const clippingRect = getClippingRect(root)
158
+ const bottomSpace = clippingRect.bottom - rootRect.bottom
159
+ const topSpace = rootRect.top - clippingRect.top
160
+ const nextSide = bottomSpace < estimatedHeight && topSpace > bottomSpace ? "top" : "bottom"
161
+ const availableSpace = Math.max(0, nextSide === "top" ? topSpace : bottomSpace)
162
+ setSuggestionSide(nextSide)
163
+ setSuggestionMaxHeight(Math.floor(Math.max(0, availableSpace - 8)))
164
+ }, [showSuggestions, filtered.length])
165
+
166
+ const insertMention = (option: MentionOption) => {
167
+ if (!triggerInfo) return
168
+ const cursor = internalRef.current?.selectionStart ?? value.length
169
+ const before = value.slice(0, triggerInfo.triggerIdx)
170
+ const after = value.slice(cursor)
171
+ const newValue = `${before}${trigger}${option.label} ${after}`
172
+ setSuggestionsDismissed(true)
173
+ setValue(newValue)
174
+ // Move cursor after inserted mention
175
+ window.setTimeout(() => {
176
+ const target = internalRef.current
177
+ if (!target) return
178
+ const pos = before.length + trigger.length + option.label.length + 1
179
+ target.setSelectionRange(pos, pos)
180
+ target.focus()
181
+ }, 0)
182
+ }
183
+
184
+ const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
185
+ const nativeEvent = event.nativeEvent as InputEvent
186
+ const inputData = typeof nativeEvent.data === "string" ? nativeEvent.data : null
187
+ if (inputData === trigger || !suggestionsDismissed) {
188
+ setSuggestionsDismissed(false)
189
+ } else {
190
+ setSuggestionsDismissed(true)
191
+ }
192
+ setValue(event.target.value)
193
+ }
194
+
195
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
196
+ if (!showSuggestions) return
197
+ if (event.key === "ArrowDown") {
198
+ event.preventDefault()
199
+ setActiveIndex((i) => (i + 1) % filtered.length)
200
+ } else if (event.key === "ArrowUp") {
201
+ event.preventDefault()
202
+ setActiveIndex((i) => (i - 1 + filtered.length) % filtered.length)
203
+ } else if (event.key === "Enter" || event.key === "Tab") {
204
+ event.preventDefault()
205
+ insertMention(filtered[activeIndex])
206
+ } else if (event.key === "Escape") {
207
+ event.preventDefault()
208
+ setSuggestionsDismissed(true)
209
+ }
210
+ }
211
+
212
+ return (
213
+ <div ref={rootRef} className={cn("relative w-full", className)} data-slot="mention">
214
+ {value ? (
215
+ <div
216
+ aria-hidden="true"
217
+ className="pointer-events-none absolute inset-0 whitespace-pre-wrap break-words rounded-md border border-transparent px-3 py-2 text-sm text-foreground"
218
+ >
219
+ {highlightedValue}
220
+ </div>
221
+ ) : null}
222
+ <textarea
223
+ ref={setRef}
224
+ rows={rows}
225
+ value={value}
226
+ onKeyDown={handleKeyDown}
227
+ onClick={() => setSuggestionsDismissed(true)}
228
+ disabled={disabled}
229
+ placeholder={placeholder}
230
+ onChange={handleChange}
231
+ className={cn(
232
+ "relative flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm caret-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
233
+ value && "text-transparent selection:bg-primary-subtle"
234
+ )}
235
+ {...props}
236
+ />
237
+ {showSuggestions ? (
238
+ <div
239
+ data-slot="mention-suggestions"
240
+ className={cn(
241
+ "absolute left-0 z-50 w-full max-w-xs rounded-md border bg-popover p-1 shadow-md",
242
+ suggestionSide === "top" ? "bottom-full mb-1" : "top-full mt-1"
243
+ )}
244
+ style={{ maxHeight: suggestionMaxHeight }}
245
+ >
246
+ <ul role="listbox" className="max-h-[inherit] overflow-y-auto">
247
+ {filtered.map((option, idx) => (
248
+ <li key={option.id}>
249
+ <button
250
+ type="button"
251
+ role="option"
252
+ aria-selected={idx === activeIndex}
253
+ onMouseEnter={() => setActiveIndex(idx)}
254
+ onMouseDown={(event) => event.preventDefault()}
255
+ onClick={() => insertMention(option)}
256
+ className={cn(
257
+ "flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-sm",
258
+ idx === activeIndex && "bg-muted"
259
+ )}
260
+ >
261
+ <span className="font-medium">{option.label}</span>
262
+ {option.hint ? (
263
+ <span className="text-xs text-muted-foreground">
264
+ {option.hint}
265
+ </span>
266
+ ) : null}
267
+ </button>
268
+ </li>
269
+ ))}
270
+ </ul>
271
+ </div>
272
+ ) : null}
273
+ </div>
274
+ )
275
+ }
276
+ )
277
+ Mention.displayName = "Mention"
278
+
279
+ export { Mention }
@@ -0,0 +1,109 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconChevronDown as ChevronDown, IconChevronUp as ChevronUp } from "@tabler/icons-react";
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ export interface NumberInputProps
9
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange" | "type"> {
10
+ value?: number
11
+ onValueChange?: (value: number) => void
12
+ min?: number
13
+ max?: number
14
+ step?: number
15
+ incrementLabel?: string
16
+ decrementLabel?: string
17
+ }
18
+
19
+ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
20
+ (
21
+ {
22
+ className,
23
+ value,
24
+ onValueChange,
25
+ min,
26
+ max,
27
+ step = 1,
28
+ incrementLabel = "Increment",
29
+ decrementLabel = "Decrement",
30
+ disabled,
31
+ ...props
32
+ },
33
+ ref
34
+ ) => {
35
+ const clamp = React.useCallback(
36
+ (next: number) => {
37
+ let result = next
38
+ if (min !== undefined) result = Math.max(min, result)
39
+ if (max !== undefined) result = Math.min(max, result)
40
+ return result
41
+ },
42
+ [min, max]
43
+ )
44
+
45
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
46
+ const raw = event.target.value
47
+ if (raw === "" || raw === "-") return
48
+ const parsed = Number(raw)
49
+ if (Number.isNaN(parsed)) return
50
+ onValueChange?.(clamp(parsed))
51
+ }
52
+
53
+ const adjust = (delta: number) => {
54
+ const current = value ?? 0
55
+ onValueChange?.(clamp(current + delta))
56
+ }
57
+
58
+ return (
59
+ <div
60
+ className={cn(
61
+ "inline-flex items-center rounded-md border border-input bg-transparent shadow-sm focus-within:outline-none focus-within:ring-1 focus-within:ring-ring",
62
+ disabled && "opacity-50 pointer-events-none",
63
+ className
64
+ )}
65
+ data-slot="number-input"
66
+ >
67
+ <input
68
+ ref={ref}
69
+ type="number"
70
+ inputMode={step % 1 === 0 ? "numeric" : "decimal"}
71
+ value={value ?? ""}
72
+ onChange={handleChange}
73
+ disabled={disabled}
74
+ min={min}
75
+ max={max}
76
+ step={step}
77
+ className="h-9 w-full rounded-l-md bg-transparent px-3 py-1 text-sm placeholder:text-muted-foreground focus-visible:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
78
+ {...props}
79
+ />
80
+ <div className="flex flex-col border-l border-input">
81
+ <button
82
+ type="button"
83
+ tabIndex={-1}
84
+ onClick={() => adjust(step)}
85
+ disabled={disabled || (max !== undefined && (value ?? 0) >= max)}
86
+ className="flex h-[18px] w-7 items-center justify-center text-muted-foreground hover:bg-muted disabled:opacity-50"
87
+ aria-label={incrementLabel}
88
+ >
89
+ <ChevronUp className="h-3 w-3" />
90
+ </button>
91
+ <div className="h-px bg-input" />
92
+ <button
93
+ type="button"
94
+ tabIndex={-1}
95
+ onClick={() => adjust(-step)}
96
+ disabled={disabled || (min !== undefined && (value ?? 0) <= min)}
97
+ className="flex h-[18px] w-7 items-center justify-center text-muted-foreground hover:bg-muted disabled:opacity-50"
98
+ aria-label={decrementLabel}
99
+ >
100
+ <ChevronDown className="h-3 w-3" />
101
+ </button>
102
+ </div>
103
+ </div>
104
+ )
105
+ }
106
+ )
107
+ NumberInput.displayName = "NumberInput"
108
+
109
+ export { NumberInput }
@@ -0,0 +1,138 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import {
7
+ FormControl,
8
+ FormDescription,
9
+ FormGroup,
10
+ FormLabel,
11
+ FormMessage,
12
+ } from "./Form"
13
+ import { PasswordInput, type PasswordInputProps } from "./PasswordInput"
14
+ import {
15
+ PasswordRequirementList,
16
+ type PasswordRequirement,
17
+ } from "./PasswordRequirementList"
18
+ import { PasswordStrengthMeter } from "./PasswordStrengthMeter"
19
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
20
+
21
+ export interface PasswordGroupProps
22
+ extends Omit<
23
+ React.HTMLAttributes<HTMLDivElement>,
24
+ "defaultValue" | "onChange"
25
+ > {
26
+ id?: string
27
+ label?: React.ReactNode
28
+ description?: React.ReactNode
29
+ value?: string
30
+ defaultValue?: string
31
+ onValueChange?: (value: string) => void
32
+ requirements?: PasswordRequirement[]
33
+ strengthScore?: number
34
+ strengthLabel?: React.ReactNode
35
+ strengthDescription?: React.ReactNode
36
+ strengthValueLabel?: React.ReactNode
37
+ error?: React.ReactNode
38
+ disabled?: boolean
39
+ disabledReason?: React.ReactNode
40
+ passwordInputProps?: Omit<
41
+ PasswordInputProps,
42
+ "id" | "value" | "defaultValue" | "onChange" | "disabled"
43
+ >
44
+ }
45
+
46
+ const PasswordGroup = React.forwardRef<HTMLDivElement, PasswordGroupProps>(
47
+ (
48
+ {
49
+ className,
50
+ id: idProp,
51
+ label = "Password",
52
+ description,
53
+ value,
54
+ defaultValue,
55
+ onValueChange,
56
+ requirements,
57
+ strengthScore,
58
+ strengthLabel,
59
+ strengthDescription,
60
+ strengthValueLabel,
61
+ error,
62
+ disabled,
63
+ disabledReason,
64
+ passwordInputProps,
65
+ ...props
66
+ },
67
+ ref
68
+ ) => {
69
+ const generatedId = React.useId()
70
+ const id = idProp ?? generatedId
71
+ const isControlled = value !== undefined
72
+ const [internalValue, setInternalValue] = React.useState(defaultValue ?? "")
73
+ const resolvedValue = isControlled ? value : internalValue
74
+
75
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
76
+ if (!isControlled) {
77
+ setInternalValue(event.target.value)
78
+ }
79
+ onValueChange?.(event.target.value)
80
+ }
81
+
82
+ const input = (
83
+ <PasswordInput
84
+ {...passwordInputProps}
85
+ id={id}
86
+ value={resolvedValue}
87
+ onChange={handleChange}
88
+ disabled={disabled}
89
+ aria-invalid={error ? true : passwordInputProps?.["aria-invalid"]}
90
+ />
91
+ )
92
+
93
+ return (
94
+ <div
95
+ ref={ref}
96
+ className={cn("w-full", className)}
97
+ data-slot="password-group"
98
+ {...props}
99
+ >
100
+ <FormGroup>
101
+ {label ? <FormLabel htmlFor={id}>{label}</FormLabel> : null}
102
+ <FormControl>
103
+ {disabled && disabledReason ? (
104
+ <Tooltip>
105
+ <TooltipTrigger asChild>
106
+ <span className="block w-full" tabIndex={0}>
107
+ {input}
108
+ </span>
109
+ </TooltipTrigger>
110
+ <TooltipContent>{disabledReason}</TooltipContent>
111
+ </Tooltip>
112
+ ) : (
113
+ input
114
+ )}
115
+ </FormControl>
116
+ {description ? (
117
+ <FormDescription>{description}</FormDescription>
118
+ ) : null}
119
+ {strengthScore !== undefined ? (
120
+ <PasswordStrengthMeter
121
+ score={strengthScore}
122
+ label={strengthLabel}
123
+ description={strengthDescription}
124
+ valueLabel={strengthValueLabel}
125
+ />
126
+ ) : null}
127
+ {requirements && requirements.length > 0 ? (
128
+ <PasswordRequirementList requirements={requirements} />
129
+ ) : null}
130
+ {error ? <FormMessage>{error}</FormMessage> : null}
131
+ </FormGroup>
132
+ </div>
133
+ )
134
+ }
135
+ )
136
+ PasswordGroup.displayName = "PasswordGroup"
137
+
138
+ export { PasswordGroup }
@@ -0,0 +1,74 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconEye as Eye, IconEyeOff as EyeOff } from "@tabler/icons-react";
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ export interface PasswordInputProps
9
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
10
+ /** If provided, controls the show/hide state externally. */
11
+ show?: boolean
12
+ onShowChange?: (show: boolean) => void
13
+ showLabel?: string
14
+ hideLabel?: string
15
+ }
16
+
17
+ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
18
+ (
19
+ {
20
+ className,
21
+ show: showProp,
22
+ onShowChange,
23
+ showLabel = "Show password",
24
+ hideLabel = "Hide password",
25
+ disabled,
26
+ ...props
27
+ },
28
+ ref
29
+ ) => {
30
+ const [internalShow, setInternalShow] = React.useState(false)
31
+ const isControlled = showProp !== undefined
32
+ const show = isControlled ? showProp : internalShow
33
+
34
+ const toggle = () => {
35
+ const next = !show
36
+ if (!isControlled) setInternalShow(next)
37
+ onShowChange?.(next)
38
+ }
39
+
40
+ const Icon = show ? EyeOff : Eye
41
+
42
+ return (
43
+ <div
44
+ className={cn(
45
+ "relative inline-flex items-center w-full",
46
+ disabled && "opacity-50 pointer-events-none",
47
+ className
48
+ )}
49
+ data-slot="password-input"
50
+ >
51
+ <input
52
+ ref={ref}
53
+ type={show ? "text" : "password"}
54
+ disabled={disabled}
55
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 pr-10 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
56
+ {...props}
57
+ />
58
+ <button
59
+ type="button"
60
+ tabIndex={-1}
61
+ onClick={toggle}
62
+ disabled={disabled}
63
+ className="absolute right-2 top-1/2 -translate-y-1/2 flex h-6 w-6 items-center justify-center rounded text-muted-foreground hover:text-foreground"
64
+ aria-label={show ? hideLabel : showLabel}
65
+ >
66
+ <Icon className="h-4 w-4" />
67
+ </button>
68
+ </div>
69
+ )
70
+ }
71
+ )
72
+ PasswordInput.displayName = "PasswordInput"
73
+
74
+ export { PasswordInput }