@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
@@ -1,12 +1,15 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
- import { ArrowDownAZ, ArrowUpAZ, ArrowUpDown, type LucideIcon } from "lucide-react"
5
- import { Button, ButtonProps } from "../atoms/Button"
4
+ import { IconSortDescendingLetters as ArrowDownAZ, IconSortAscendingLetters as ArrowUpAZ, IconArrowsSort as ArrowUpDown } from "@tabler/icons-react";
5
+ import type { Icon as LucideIcon } from "@tabler/icons-react";
6
+ import { Button, ButtonProps } from "../inputs/Button"
6
7
  import { cn } from "../../lib/utils"
7
8
  import { sortButtonDefaultVariantKey } from "./generated/default-variant-keys"
8
9
  import { sortButtonVariantKeys, type SortButtonVariantKey } from "./generated/variant-keys"
9
10
 
11
+ export type { SortButtonVariantKey }
12
+
10
13
  export interface SortButtonProps extends Omit<ButtonProps, "onChange"> {
11
14
  value?: SortButtonVariantKey
12
15
  onSortChange?: (value: SortButtonVariantKey) => void
@@ -16,15 +16,26 @@ export interface SwitchProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
16
16
  }
17
17
 
18
18
  const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
19
- ({ className, checked, onCheckedChange, ...props }, ref) => {
20
- const switchState: SwitchVariantKey = checked ? "checked" : switchDefaultVariantKey
19
+ ({ className, checked, defaultChecked, onCheckedChange, onClick, ...props }, ref) => {
20
+ const [uncontrolledChecked, setUncontrolledChecked] = React.useState(Boolean(defaultChecked))
21
+ const isControlled = checked !== undefined
22
+ const currentChecked = isControlled ? checked : uncontrolledChecked
23
+ const switchState: SwitchVariantKey = currentChecked ? "checked" : switchDefaultVariantKey
24
+
25
+ const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
26
+ onClick?.(event)
27
+ if (event.defaultPrevented) return
28
+ const nextChecked = !currentChecked
29
+ if (!isControlled) setUncontrolledChecked(nextChecked)
30
+ onCheckedChange?.(nextChecked)
31
+ }
21
32
 
22
33
  return (
23
34
  <button
24
35
  type="button"
25
36
  role="switch"
26
- aria-checked={checked}
27
- onClick={() => onCheckedChange?.(!checked)}
37
+ aria-checked={currentChecked}
38
+ onClick={handleClick}
28
39
  className={cn(
29
40
  "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border border-transparent p-0.5 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
30
41
  switchStateClasses[switchState],
@@ -0,0 +1,114 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../../lib/utils"
6
+ import { Tag } from "../display/Tag"
7
+
8
+ export interface TagInputProps {
9
+ id?: string
10
+ value?: string[]
11
+ onValueChange?: (value: string[]) => void
12
+ placeholder?: string
13
+ /** Keys that commit the current input value as a tag. Default ["Enter", ","]. */
14
+ commitKeys?: string[]
15
+ /** Reject duplicates (case-insensitive). Default true. */
16
+ dedupe?: boolean
17
+ maxTags?: number
18
+ removeLabel?: string
19
+ className?: string
20
+ disabled?: boolean
21
+ }
22
+
23
+ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
24
+ (
25
+ {
26
+ value,
27
+ id,
28
+ onValueChange,
29
+ placeholder = "Add tag...",
30
+ commitKeys = ["Enter", ","],
31
+ dedupe = true,
32
+ maxTags,
33
+ removeLabel = "Remove tag",
34
+ className,
35
+ disabled,
36
+ },
37
+ ref
38
+ ) => {
39
+ const [draft, setDraft] = React.useState("")
40
+ const isComposingRef = React.useRef(false)
41
+ const tags = value ?? []
42
+
43
+ const commit = React.useCallback(() => {
44
+ const trimmed = draft.trim()
45
+ if (!trimmed) return
46
+ if (maxTags !== undefined && tags.length >= maxTags) return
47
+ if (dedupe && tags.some((t) => t.toLowerCase() === trimmed.toLowerCase()))
48
+ return
49
+ onValueChange?.([...tags, trimmed])
50
+ setDraft("")
51
+ }, [draft, tags, dedupe, maxTags, onValueChange])
52
+
53
+ const remove = (target: string) => {
54
+ onValueChange?.(tags.filter((t) => t !== target))
55
+ }
56
+
57
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
58
+ const nativeEvent = event.nativeEvent as KeyboardEvent & { keyCode?: number }
59
+ if (nativeEvent.isComposing || isComposingRef.current || nativeEvent.keyCode === 229) {
60
+ return
61
+ }
62
+ if (commitKeys.includes(event.key)) {
63
+ event.preventDefault()
64
+ commit()
65
+ } else if (event.key === "Backspace" && !draft && tags.length > 0) {
66
+ remove(tags[tags.length - 1])
67
+ }
68
+ }
69
+
70
+ return (
71
+ <div
72
+ className={cn(
73
+ "flex flex-wrap items-center gap-1 rounded-md border border-input bg-transparent px-2 py-1 text-sm shadow-sm focus-within:outline-none focus-within:ring-1 focus-within:ring-ring",
74
+ disabled && "opacity-50 pointer-events-none",
75
+ className
76
+ )}
77
+ data-slot="tag-input"
78
+ >
79
+ {tags.map((tag) => (
80
+ <Tag
81
+ key={tag}
82
+ size="sm"
83
+ onRemove={disabled ? undefined : () => remove(tag)}
84
+ removeLabel={removeLabel}
85
+ >
86
+ {tag}
87
+ </Tag>
88
+ ))}
89
+ <input
90
+ id={id}
91
+ ref={ref}
92
+ type="text"
93
+ value={draft}
94
+ onChange={(e) => setDraft(e.target.value)}
95
+ onCompositionStart={() => {
96
+ isComposingRef.current = true
97
+ }}
98
+ onCompositionEnd={(event) => {
99
+ isComposingRef.current = false
100
+ setDraft(event.currentTarget.value)
101
+ }}
102
+ onKeyDown={handleKeyDown}
103
+ onBlur={commit}
104
+ placeholder={tags.length === 0 ? placeholder : ""}
105
+ disabled={disabled}
106
+ className="min-w-[80px] flex-1 bg-transparent px-1 py-0.5 outline-none placeholder:text-muted-foreground"
107
+ />
108
+ </div>
109
+ )
110
+ }
111
+ )
112
+ TagInput.displayName = "TagInput"
113
+
114
+ export { TagInput }
@@ -25,6 +25,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
25
25
  ref={ref}
26
26
  disabled={disabled}
27
27
  {...props}
28
+ data-slot="textarea"
28
29
  />
29
30
  )
30
31
  }
@@ -0,0 +1,150 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { IconClock as Clock } from "@tabler/icons-react";
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ export interface TimePickerProps {
9
+ /** Time value in 24-hour format "HH:mm" (e.g. "14:30"). Empty string = unset. */
10
+ value?: string
11
+ onValueChange?: (value: string) => void
12
+ /** 12-hour with AM/PM, or 24-hour. Default 24. */
13
+ hour12?: boolean
14
+ /** Granularity of minute select. Default 1. */
15
+ minuteStep?: 1 | 5 | 10 | 15 | 30
16
+ className?: string
17
+ disabled?: boolean
18
+ hourLabel?: string
19
+ minuteLabel?: string
20
+ periodLabel?: string
21
+ }
22
+
23
+ function parse(value: string | undefined): {
24
+ hour: number | null
25
+ minute: number | null
26
+ } {
27
+ if (!value) return { hour: null, minute: null }
28
+ const match = /^(\d{1,2}):(\d{2})$/.exec(value)
29
+ if (!match) return { hour: null, minute: null }
30
+ return { hour: Number(match[1]), minute: Number(match[2]) }
31
+ }
32
+
33
+ function format2(n: number) {
34
+ return n.toString().padStart(2, "0")
35
+ }
36
+
37
+ const TimePicker = React.forwardRef<HTMLDivElement, TimePickerProps>(
38
+ (
39
+ {
40
+ value,
41
+ onValueChange,
42
+ hour12 = false,
43
+ minuteStep = 1,
44
+ className,
45
+ disabled,
46
+ hourLabel = "Hour",
47
+ minuteLabel = "Minute",
48
+ periodLabel = "AM/PM",
49
+ },
50
+ ref
51
+ ) => {
52
+ const { hour, minute } = parse(value)
53
+ const displayHour = hour ?? 0
54
+ const displayMinute = minute ?? 0
55
+
56
+ const hourOptions = hour12
57
+ ? Array.from({ length: 12 }, (_, i) => i + 1)
58
+ : Array.from({ length: 24 }, (_, i) => i)
59
+
60
+ const minuteOptions = Array.from(
61
+ { length: 60 / minuteStep },
62
+ (_, i) => i * minuteStep
63
+ )
64
+
65
+ const ampm = displayHour >= 12 ? "PM" : "AM"
66
+ const display12 =
67
+ displayHour === 0 ? 12 : displayHour > 12 ? displayHour - 12 : displayHour
68
+
69
+ const setHour = (h: number) => {
70
+ const newHour = hour12
71
+ ? ampm === "PM"
72
+ ? h === 12
73
+ ? 12
74
+ : h + 12
75
+ : h === 12
76
+ ? 0
77
+ : h
78
+ : h
79
+ onValueChange?.(`${format2(newHour)}:${format2(displayMinute)}`)
80
+ }
81
+
82
+ const setMinute = (m: number) => {
83
+ onValueChange?.(`${format2(displayHour)}:${format2(m)}`)
84
+ }
85
+
86
+ const setAmPm = (next: "AM" | "PM") => {
87
+ let newHour = displayHour
88
+ if (next === "AM" && displayHour >= 12) newHour = displayHour - 12
89
+ else if (next === "PM" && displayHour < 12) newHour = displayHour + 12
90
+ onValueChange?.(`${format2(newHour)}:${format2(displayMinute)}`)
91
+ }
92
+
93
+ const selectClass =
94
+ "h-9 rounded-md border border-input bg-transparent px-2 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring disabled:opacity-50"
95
+
96
+ return (
97
+ <div
98
+ ref={ref}
99
+ className={cn("inline-flex items-center gap-2", className)}
100
+ data-slot="time-picker"
101
+ >
102
+ <Clock className="h-4 w-4 text-muted-foreground" />
103
+ <select
104
+ aria-label={hourLabel}
105
+ disabled={disabled}
106
+ value={hour12 ? display12 : displayHour}
107
+ onChange={(e) => setHour(Number(e.target.value))}
108
+ className={selectClass}
109
+ >
110
+ {hourOptions.map((h) => (
111
+ <option key={h} value={h}>
112
+ {format2(h)}
113
+ </option>
114
+ ))}
115
+ </select>
116
+ <span aria-hidden className="text-muted-foreground">
117
+ :
118
+ </span>
119
+ <select
120
+ aria-label={minuteLabel}
121
+ disabled={disabled}
122
+ value={displayMinute}
123
+ onChange={(e) => setMinute(Number(e.target.value))}
124
+ className={selectClass}
125
+ >
126
+ {minuteOptions.map((m) => (
127
+ <option key={m} value={m}>
128
+ {format2(m)}
129
+ </option>
130
+ ))}
131
+ </select>
132
+ {hour12 ? (
133
+ <select
134
+ aria-label={periodLabel}
135
+ disabled={disabled}
136
+ value={ampm}
137
+ onChange={(e) => setAmPm(e.target.value as "AM" | "PM")}
138
+ className={selectClass}
139
+ >
140
+ <option value="AM">AM</option>
141
+ <option value="PM">PM</option>
142
+ </select>
143
+ ) : null}
144
+ </div>
145
+ )
146
+ }
147
+ )
148
+ TimePicker.displayName = "TimePicker"
149
+
150
+ export { TimePicker }
@@ -0,0 +1,48 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TogglePrimitive from "@radix-ui/react-toggle"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+
7
+ import { cn } from "../../lib/utils"
8
+
9
+ export const toggleVariants = cva(
10
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:border disabled:border-input disabled:bg-muted/50 disabled:text-muted-foreground disabled:opacity-100 data-[state=on]:!bg-primary data-[state=on]:!text-primary-foreground disabled:data-[state=on]:!bg-primary-subtle disabled:data-[state=on]:!text-primary-subtle-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: "bg-transparent",
15
+ outline:
16
+ "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
17
+ },
18
+ size: {
19
+ default: "h-9 px-2.5 min-w-9",
20
+ sm: "h-8 px-2 min-w-8",
21
+ lg: "h-10 px-3 min-w-10",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ size: "default",
27
+ },
28
+ }
29
+ )
30
+
31
+ export interface ToggleProps
32
+ extends React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root>,
33
+ VariantProps<typeof toggleVariants> {}
34
+
35
+ const Toggle = React.forwardRef<
36
+ React.ElementRef<typeof TogglePrimitive.Root>,
37
+ ToggleProps
38
+ >(({ className, variant, size, ...props }, ref) => (
39
+ <TogglePrimitive.Root
40
+ ref={ref}
41
+ className={cn(toggleVariants({ variant, size }), className)}
42
+ {...props}
43
+ />
44
+ ))
45
+
46
+ Toggle.displayName = TogglePrimitive.Root.displayName
47
+
48
+ export { Toggle }
@@ -8,7 +8,7 @@ import type { ToggleGroupVariantKey } from "./generated/variant-keys"
8
8
  import { toggleGroupDefaultVariantKey } from "./generated/default-variant-keys"
9
9
 
10
10
  const toggleGroupVariantClasses: Record<ToggleGroupVariantKey, string> = {
11
- default: "bg-secondary text-foreground hover:bg-secondary/80",
11
+ default: "bg-secondary text-foreground hover:bg-muted hover:text-muted-foreground",
12
12
  outline: "border border-input bg-transparent text-foreground hover:bg-muted",
13
13
  }
14
14
 
@@ -40,7 +40,7 @@ const ToggleGroupItem = React.forwardRef<
40
40
  <ToggleGroupPrimitive.Item
41
41
  ref={ref}
42
42
  className={cn(
43
- "inline-flex w-fit items-center justify-center rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-foreground data-[state=on]:text-primary-foreground",
43
+ "inline-flex w-fit items-center justify-center rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:border disabled:border-input disabled:bg-muted/50 disabled:text-muted-foreground disabled:opacity-100 data-[state=on]:bg-foreground data-[state=on]:text-background data-[state=on]:!bg-primary data-[state=on]:!text-primary-foreground disabled:data-[state=on]:!bg-primary-subtle disabled:data-[state=on]:!text-primary-subtle-foreground",
44
44
  // Size variants
45
45
  size === "default" && "h-9 py-0 px-3",
46
46
  size === "sm" && "h-8 px-2",
@@ -0,0 +1,148 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Button, type ButtonProps } from "./Button"
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
6
+ import { cn } from "../../lib/utils"
7
+
8
+ type TooltipContentProps = React.ComponentPropsWithoutRef<typeof TooltipContent>
9
+
10
+ export interface TooltipButtonProps extends ButtonProps {
11
+ tooltip: React.ReactNode
12
+ tooltipSide?: TooltipContentProps["side"]
13
+ tooltipAlign?: TooltipContentProps["align"]
14
+ tooltipSideOffset?: TooltipContentProps["sideOffset"]
15
+ tooltipContentClassName?: string
16
+ tooltipPortalContainer?: TooltipContentProps["portalContainer"]
17
+ tooltipOpenOnClick?: boolean
18
+ tooltipCloseOnPress?: boolean
19
+ tooltipClickDuration?: number
20
+ }
21
+
22
+ const TooltipButton = React.forwardRef<HTMLButtonElement, TooltipButtonProps>(
23
+ (
24
+ {
25
+ tooltip,
26
+ tooltipSide,
27
+ tooltipAlign,
28
+ tooltipSideOffset,
29
+ tooltipContentClassName,
30
+ tooltipPortalContainer,
31
+ tooltipOpenOnClick = false,
32
+ tooltipCloseOnPress = false,
33
+ tooltipClickDuration = 1600,
34
+ className,
35
+ onClick,
36
+ onBlur,
37
+ onPointerDown,
38
+ onPointerLeave,
39
+ ...props
40
+ },
41
+ ref
42
+ ) => {
43
+ const [open, setOpen] = React.useState(false)
44
+ const closeTimerRef = React.useRef<number | null>(null)
45
+ const suppressHoverRef = React.useRef(false)
46
+
47
+ React.useEffect(() => {
48
+ return () => {
49
+ if (closeTimerRef.current !== null) {
50
+ window.clearTimeout(closeTimerRef.current)
51
+ }
52
+ }
53
+ }, [])
54
+
55
+ const scheduleClose = React.useCallback(() => {
56
+ if (closeTimerRef.current !== null) {
57
+ window.clearTimeout(closeTimerRef.current)
58
+ }
59
+ closeTimerRef.current = window.setTimeout(() => {
60
+ setOpen(false)
61
+ closeTimerRef.current = null
62
+ }, tooltipClickDuration)
63
+ }, [tooltipClickDuration])
64
+
65
+ const handleClick = React.useCallback(
66
+ (event: React.MouseEvent<HTMLButtonElement>) => {
67
+ onClick?.(event)
68
+ if (tooltipCloseOnPress && !tooltipOpenOnClick) {
69
+ setOpen(false)
70
+ }
71
+ if (!tooltipOpenOnClick || event.defaultPrevented) return
72
+ setOpen(true)
73
+ scheduleClose()
74
+ },
75
+ [onClick, scheduleClose, tooltipCloseOnPress, tooltipOpenOnClick]
76
+ )
77
+
78
+ const handlePointerDown = React.useCallback(
79
+ (event: React.PointerEvent<HTMLButtonElement>) => {
80
+ onPointerDown?.(event)
81
+ if (!tooltipCloseOnPress || event.defaultPrevented) return
82
+ suppressHoverRef.current = true
83
+ setOpen(false)
84
+ },
85
+ [onPointerDown, tooltipCloseOnPress]
86
+ )
87
+
88
+ const handlePointerLeave = React.useCallback(
89
+ (event: React.PointerEvent<HTMLButtonElement>) => {
90
+ onPointerLeave?.(event)
91
+ if (tooltipCloseOnPress) {
92
+ suppressHoverRef.current = false
93
+ }
94
+ },
95
+ [onPointerLeave, tooltipCloseOnPress]
96
+ )
97
+
98
+ const handleBlur = React.useCallback(
99
+ (event: React.FocusEvent<HTMLButtonElement>) => {
100
+ onBlur?.(event)
101
+ if (tooltipOpenOnClick) {
102
+ setOpen(false)
103
+ }
104
+ },
105
+ [onBlur, tooltipOpenOnClick]
106
+ )
107
+
108
+ const handleOpenChange = React.useCallback(
109
+ (nextOpen: boolean) => {
110
+ if (tooltipCloseOnPress && suppressHoverRef.current && nextOpen) return
111
+ setOpen(nextOpen)
112
+ },
113
+ [tooltipCloseOnPress]
114
+ )
115
+
116
+ const tooltipRootProps = tooltipOpenOnClick || tooltipCloseOnPress
117
+ ? { open, onOpenChange: handleOpenChange }
118
+ : undefined
119
+
120
+ return (
121
+ <Tooltip {...tooltipRootProps} openOnPress={!tooltipCloseOnPress}>
122
+ <TooltipTrigger asChild>
123
+ <Button
124
+ ref={ref}
125
+ className={cn(className)}
126
+ onClick={handleClick}
127
+ onBlur={handleBlur}
128
+ onPointerDown={handlePointerDown}
129
+ onPointerLeave={handlePointerLeave}
130
+ {...props}
131
+ />
132
+ </TooltipTrigger>
133
+ <TooltipContent
134
+ side={tooltipSide}
135
+ align={tooltipAlign}
136
+ sideOffset={tooltipSideOffset}
137
+ portalContainer={tooltipPortalContainer}
138
+ className={tooltipContentClassName}
139
+ >
140
+ {tooltip}
141
+ </TooltipContent>
142
+ </Tooltip>
143
+ )
144
+ }
145
+ )
146
+ TooltipButton.displayName = "TooltipButton"
147
+
148
+ export { TooltipButton }