@catalystsoftware/ui 1.0.2 → 1.0.5
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.
- package/data/tailwind.config.js +261 -3821
- package/dist/components/catalyst-ui/buttons/burger.tsx +207 -0
- package/dist/components/catalyst-ui/core/data-display/timeline.tsx +210 -0
- package/dist/components/catalyst-ui/core/feedback/alert.tsx +491 -0
- package/dist/components/catalyst-ui/core/feedback/spinner-1.tsx +65 -0
- package/dist/components/catalyst-ui/core/feedback/toast.tsx +1857 -0
- package/dist/components/catalyst-ui/core/navigation/menu.tsx +164 -0
- package/dist/components/catalyst-ui/forms/toggle-class.tsx +176 -0
- package/dist/components/catalyst-ui/hooks/use-copy-to-clipboard.tsx +419 -0
- package/dist/components/catalyst-ui/hooks/use-counter.tsx +13 -0
- package/dist/components/catalyst-ui/hooks/use-event-listener.tsx +23 -0
- package/dist/components/catalyst-ui/hooks/use-export-markdown.tsx +47 -0
- package/dist/components/catalyst-ui/hooks/use-focus.tsx +17 -0
- package/dist/components/catalyst-ui/hooks/use-interval.tsx +23 -0
- package/dist/components/catalyst-ui/hooks/use-is-client.tsx +16 -0
- package/dist/components/catalyst-ui/hooks/use-media-query.tsx +19 -0
- package/dist/components/catalyst-ui/hooks/use-mobile.tsx +19 -0
- package/dist/components/catalyst-ui/hooks/use-resize-observer.tsx +81 -0
- package/dist/components/catalyst-ui/hooks/use-timeout.tsx +21 -0
- package/dist/components/catalyst-ui/hooks/use-timer.tsx +209 -0
- package/dist/components/catalyst-ui/hooks/use-toggle.tsx +12 -0
- package/dist/components/catalyst-ui/media/image.tsx +13 -0
- package/dist/components/catalyst-ui/overlays/dual-sidebar.tsx +4142 -0
- package/dist/components/catalyst-ui/overlays/sidebar-original.tsx +726 -0
- package/dist/components/catalyst-ui/primitives/accordion.tsx +250 -0
- package/dist/components/catalyst-ui/primitives/alert-dialog.tsx +126 -0
- package/dist/components/catalyst-ui/primitives/aspect-ratio.tsx +9 -0
- package/dist/components/catalyst-ui/primitives/avatar.tsx +296 -0
- package/dist/components/catalyst-ui/primitives/badge.tsx +57 -0
- package/dist/components/catalyst-ui/primitives/breadcrumb.tsx +101 -0
- package/dist/components/catalyst-ui/primitives/button.tsx +265 -0
- package/dist/components/catalyst-ui/primitives/calendar-v4.tsx +208 -0
- package/dist/components/catalyst-ui/primitives/calendar.tsx +295 -0
- package/dist/components/catalyst-ui/primitives/card.tsx +618 -0
- package/dist/components/catalyst-ui/primitives/carousel.tsx +238 -0
- package/dist/components/catalyst-ui/primitives/chart.tsx +347 -0
- package/dist/components/catalyst-ui/primitives/checkbox.tsx +225 -0
- package/dist/components/catalyst-ui/primitives/collapsible.tsx +212 -0
- package/dist/components/catalyst-ui/primitives/command.tsx +393 -0
- package/dist/components/catalyst-ui/primitives/context-menu.tsx +236 -0
- package/dist/components/catalyst-ui/primitives/dialog.tsx +471 -0
- package/dist/components/catalyst-ui/primitives/drawer.tsx +761 -0
- package/dist/components/catalyst-ui/primitives/dropdown-menu.tsx +290 -0
- package/dist/components/catalyst-ui/primitives/empty.tsx +104 -0
- package/dist/components/catalyst-ui/primitives/field.tsx +244 -0
- package/dist/components/catalyst-ui/primitives/hover-card.tsx +124 -0
- package/dist/components/catalyst-ui/primitives/input-otp.tsx +76 -0
- package/dist/components/catalyst-ui/primitives/input.tsx +64 -0
- package/dist/components/catalyst-ui/primitives/item.tsx +196 -0
- package/dist/components/catalyst-ui/primitives/kbd.tsx +75 -0
- package/dist/components/catalyst-ui/primitives/label.tsx +24 -0
- package/dist/components/catalyst-ui/primitives/navigation-menu.tsx +150 -0
- package/dist/components/catalyst-ui/primitives/pagination.tsx +198 -0
- package/dist/components/catalyst-ui/primitives/popover.tsx +232 -0
- package/dist/components/catalyst-ui/primitives/progress.tsx +34 -0
- package/dist/components/catalyst-ui/primitives/radio-group.tsx +43 -0
- package/dist/components/catalyst-ui/primitives/resizable.tsx +56 -0
- package/dist/components/catalyst-ui/primitives/select.tsx +155 -0
- package/dist/components/catalyst-ui/primitives/separator.tsx +74 -0
- package/dist/components/catalyst-ui/primitives/sheet.tsx +126 -0
- package/dist/components/catalyst-ui/primitives/skeleton.tsx +15 -0
- package/dist/components/catalyst-ui/primitives/slider.tsx +27 -0
- package/dist/components/catalyst-ui/primitives/switch.tsx +187 -0
- package/dist/components/catalyst-ui/primitives/tabs.tsx +335 -0
- package/dist/components/catalyst-ui/primitives/textarea.tsx +24 -0
- package/dist/components/catalyst-ui/primitives/toggle-group.tsx +55 -0
- package/dist/components/catalyst-ui/primitives/toggle.tsx +42 -0
- package/dist/components/catalyst-ui/primitives/tooltip.tsx +116 -0
- package/dist/components/catalyst-ui/utils/basic-auth.tsx +40 -0
- package/dist/components/catalyst-ui/utils/context-storage.tsx +19 -0
- package/dist/components/catalyst-ui/utils/cors-middleware.tsx +71 -0
- package/dist/components/catalyst-ui/utils/deferred-content.tsx +595 -0
- package/dist/components/catalyst-ui/utils/honeypot-middleware.tsx +38 -0
- package/dist/components/catalyst-ui/utils/incId.tsx +75 -0
- package/dist/components/catalyst-ui/utils/jwk-auth.tsx +36 -0
- package/dist/components/catalyst-ui/utils/request-id.tsx +14 -0
- package/dist/components/catalyst-ui/utils/secure-headers.tsx +37 -0
- package/dist/components/catalyst-ui/utils/server-timing.tsx +23 -0
- package/dist/components/catalyst-ui/utils/utils.ts +43 -0
- package/dist/components/catalyst-ui/utils/with-cookie.tsx +43 -0
- package/dist/components/catalyst-ui/x/accordian-x.tsx +428 -0
- package/dist/components/catalyst-ui/x/alert-x.tsx +413 -0
- package/dist/components/catalyst-ui/x/animated-text-x.tsx +2242 -0
- package/dist/components/catalyst-ui/x/avatar-x.tsx +515 -0
- package/dist/components/catalyst-ui/x/badge-x.tsx +670 -0
- package/dist/components/catalyst-ui/x/button-X.tsx +2857 -0
- package/dist/components/catalyst-ui/x/button-group-x.tsx +847 -0
- package/dist/components/catalyst-ui/x/calendar-x.tsx +1910 -0
- package/dist/components/catalyst-ui/x/card-x.tsx +2597 -0
- package/dist/components/catalyst-ui/x/checkbox-x.tsx +656 -0
- package/dist/components/catalyst-ui/x/collapsible-x.tsx +1360 -0
- package/dist/components/catalyst-ui/x/combobox-x.tsx +911 -0
- package/dist/components/catalyst-ui/x/data-table-x.tsx +1753 -0
- package/dist/components/catalyst-ui/x/date-picker-x.tsx +648 -0
- package/dist/components/catalyst-ui/x/dialog-x.tsx +659 -0
- package/dist/components/catalyst-ui/x/dropdown-menu-x.tsx +612 -0
- package/dist/components/catalyst-ui/x/hover-card-x.tsx +375 -0
- package/dist/components/catalyst-ui/x/icon-x.tsx +840 -0
- package/dist/components/catalyst-ui/x/input-mask-x.tsx +981 -0
- package/dist/components/catalyst-ui/x/input-otp-x.tsx +659 -0
- package/dist/components/catalyst-ui/x/loader-x.tsx +1757 -0
- package/dist/components/catalyst-ui/x/pagination-x.tsx +622 -0
- package/dist/components/catalyst-ui/x/popover-x.tsx +744 -0
- package/dist/components/catalyst-ui/x/radio-group-x.tsx +499 -0
- package/dist/components/catalyst-ui/x/select-x.tsx +1127 -0
- package/dist/components/catalyst-ui/x/sheet-x.tsx +668 -0
- package/dist/components/catalyst-ui/x/switch-x.tsx +681 -0
- package/dist/components/catalyst-ui/x/table-x.tsx +574 -0
- package/dist/components/catalyst-ui/x/tabs-x.tsx +839 -0
- package/dist/components/catalyst-ui/x/textarea-x.tsx +1263 -0
- package/dist/components/catalyst-ui/x/tooltip-x.tsx +396 -0
- package/dist/components/catalyst-ui/x/tracker-x.tsx +560 -0
- package/dist/data/bg-data.tsx +901 -0
- package/dist/data/buttons-data.tsx +2327 -0
- package/dist/data/charts-data.tsx +102 -0
- package/dist/data/chat-data.tsx +83 -0
- package/dist/data/code-data.tsx +1040 -0
- package/dist/data/comboboxes-data.tsx +1843 -0
- package/dist/data/command-data.tsx +1381 -0
- package/dist/data/core-data.tsx +15953 -0
- package/dist/data/crm-data.tsx +47 -0
- package/dist/data/data.tsx +159 -0
- package/dist/data/date-and-time-data.tsx +554 -0
- package/dist/data/dependencies.tsx +7 -0
- package/dist/data/ecommerce-data.tsx +1387 -0
- package/dist/data/forms-data.tsx +7890 -0
- package/dist/data/hooks-data.tsx +5487 -0
- package/dist/data/index.ts +34 -0
- package/dist/data/inputs-data.tsx +557 -0
- package/dist/data/interactive-data.tsx +5394 -0
- package/dist/data/lofi-data.tsx +18295 -0
- package/dist/data/marketing-data.tsx +2546 -0
- package/dist/data/media-data.tsx +1510 -0
- package/dist/data/motion-data.tsx +5801 -0
- package/dist/data/overlay-data.tsx +4136 -0
- package/dist/data/pdf-data.tsx +124 -0
- package/dist/data/pos-data.tsx +213 -0
- package/dist/data/postcss.config.js +6 -0
- package/dist/data/primitive-data.tsx +5170 -0
- package/dist/data/prompt-data.tsx +1226 -0
- package/dist/data/requiredLibs.ts +4 -0
- package/dist/data/sandbox-data.tsx +1 -0
- package/dist/data/sidebars-data.tsx +5421 -0
- package/dist/data/stacks-data.tsx +32 -0
- package/dist/data/table-data.tsx +706 -0
- package/dist/data/tailwind.config.js +270 -0
- package/dist/data/tailwind.config.ngin.js +3830 -0
- package/dist/data/tailwind.css +431 -0
- package/dist/data/tools-data.tsx +6910 -0
- package/dist/data/typography-data.tsx +2050 -0
- package/dist/data/utils-data.tsx +6500 -0
- package/dist/data/x-data.tsx +1171 -0
- package/dist/data.tsx +159 -0
- package/package.json +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -362
|
@@ -0,0 +1,2857 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback, Children, cloneElement, ComponentPropsWithoutRef, createContext, CSSProperties, ReactElement, ReactNode, useContext, useRef } from 'react'
|
|
2
|
+
import { AlertCircle, ArchiveX, ArrowRight, Ban, CheckIcon, ChevronDown, Command, CopyIcon, Download, LucideProps, Magnet, MousePointerClick, StarIcon, Trash2, XCircle } from 'lucide-react'
|
|
3
|
+
import { Instagram, Linkedin,Twitter,} from '@catalystsoftware/icons'
|
|
4
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
5
|
+
import { attractButtonVariants, BurgerButton, buttonVariants, cn, commandButtonVariants, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, holdButtonVariants, LoaderProps, particleButtonVariants, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, Transition, useCopyToClipboard, useExport } from '~/components/catalyst-ui'
|
|
6
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
7
|
+
import { Link, useNavigation, useResolvedPath } from '@remix-run/react';
|
|
8
|
+
import { SpinnerV4 } from '~/components/catalyst-ui'
|
|
9
|
+
import { useControllableState } from '@radix-ui/react-use-controllable-state'
|
|
10
|
+
import { motion, AnimatePresence, Variant, HTMLMotionProps } from 'framer-motion'
|
|
11
|
+
|
|
12
|
+
// @devsearch app/components/catalyst-ui/data/primitive-data.tsx:buttonX
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
============================ BUTTONX =====================================================================
|
|
16
|
+
* **variant**: default | destructive | outline | secondary | ghost | link | elevated | filled | tonal | outlined | text | styled | reverted | dashed | gradient | soft | glass | success | warning | info | shine | pulse | violet | blue | green
|
|
17
|
+
* **size**: default | sm | lg | icon
|
|
18
|
+
* **align**: left | middle | right
|
|
19
|
+
* **effect**: expandIcon | ringHover | shine | shineHover | gooeyRight | gooeyLeft | underline | hoverUnderline | gradientSlideShow
|
|
20
|
+
* **prefetch**: none | intent | render | viewport
|
|
21
|
+
* **nav**: null | navlink | anchor
|
|
22
|
+
* **to**: string (navigation path)
|
|
23
|
+
* **iconPlacement**: left | right
|
|
24
|
+
* **icon**: React.ReactNode
|
|
25
|
+
* **newTab**: boolean
|
|
26
|
+
* **onClick**: function
|
|
27
|
+
* **className**: string
|
|
28
|
+
* **...props**: standard button props
|
|
29
|
+
*
|
|
30
|
+
============================ Attract =====================================================================
|
|
31
|
+
* **particleCount**: number (default: 12)
|
|
32
|
+
* **attractRadius**: number (default: 50)
|
|
33
|
+
* **attractVariant**: default | violet | blue | green
|
|
34
|
+
* **icon**: React.ReactNode
|
|
35
|
+
* **activeText**: string (default: "Attracting")
|
|
36
|
+
* **inactiveText**: string (default: "Hover me")
|
|
37
|
+
* **className**: string
|
|
38
|
+
* **...props**: standard button props
|
|
39
|
+
*
|
|
40
|
+
============================ DEFAULT (Button) ============================================================
|
|
41
|
+
* **className**: string
|
|
42
|
+
* **variant**: default | destructive | outline | secondary | ghost | link | elevated | filled | tonal | outlined | text | styled | reverted | dashed | gradient | soft | glass | success | warning | info | shine | pulse | violet | blue | green
|
|
43
|
+
* **size**: default | sm | lg | icon
|
|
44
|
+
* **align**: left | middle | right
|
|
45
|
+
* **effect**: expandIcon | ringHover | shine | shineHover | gooeyRight | gooeyLeft | underline | hoverUnderline | gradientSlideShow
|
|
46
|
+
* **asChild**: boolean (default: false)
|
|
47
|
+
* **type**: string (default: null)
|
|
48
|
+
* **children**: React.ReactNode
|
|
49
|
+
* **...props**: standard button props
|
|
50
|
+
*
|
|
51
|
+
============================ Burger ======================================================================
|
|
52
|
+
* **className**: string
|
|
53
|
+
* **...props**: standard button props
|
|
54
|
+
* *(Uses BurgerButton component from catalyst-ui)*
|
|
55
|
+
*
|
|
56
|
+
============================ Command =====================================================================
|
|
57
|
+
* **className**: string
|
|
58
|
+
* **variant**: default | primary | secondary
|
|
59
|
+
* **size**: sm | default | lg
|
|
60
|
+
* **children**: React.ReactNode (default: "CMD + K")
|
|
61
|
+
* **...props**: standard button props
|
|
62
|
+
*
|
|
63
|
+
============================ Copy ========================================================================
|
|
64
|
+
* **content**: string (text to copy)
|
|
65
|
+
* **className**: string
|
|
66
|
+
* **size**: default | sm | lg | icon
|
|
67
|
+
* **variant**: default | destructive | outline | secondary | ghost | link
|
|
68
|
+
* **delay**: number (default: 3000) - time in ms before icon resets
|
|
69
|
+
* **onClick**: function
|
|
70
|
+
* **onCopy**: function(content: string) - callback when copy succeeds
|
|
71
|
+
* **isCopied**: boolean - controlled copied state
|
|
72
|
+
* **onCopyChange**: function(isCopied: boolean) - controlled copied state handler
|
|
73
|
+
* **...props**: standard button props
|
|
74
|
+
*
|
|
75
|
+
============================ CopyText ====================================================================
|
|
76
|
+
* **code**: string (text to copy)
|
|
77
|
+
* **className**: string
|
|
78
|
+
* **ttText**: string (default: 'Copy') - tooltip text
|
|
79
|
+
* **icon**: React.Component (default: CopyIcon)
|
|
80
|
+
* **size**: default | sm | lg | icon (default: 'icon')
|
|
81
|
+
* **variant**: default | destructive | outline | secondary | ghost | link (default: 'ghost')
|
|
82
|
+
* **...props**: standard button props
|
|
83
|
+
*
|
|
84
|
+
============================ Export ======================================================================
|
|
85
|
+
* **code**: string (default: null) - content to export
|
|
86
|
+
* **filename**: string (default: 'MyComponent.tsx') - filename for download
|
|
87
|
+
* **className**: string
|
|
88
|
+
*
|
|
89
|
+
============================ Flip ========================================================================
|
|
90
|
+
* **frontText**: string (required) - text displayed on front
|
|
91
|
+
* **backText**: string (required) - text displayed on back
|
|
92
|
+
* **transition**: object (default: { type: 'spring', stiffness: 280, damping: 20 })
|
|
93
|
+
* **className**: string
|
|
94
|
+
* **frontClassName**: string - styling for front face
|
|
95
|
+
* **backClassName**: string - styling for back face
|
|
96
|
+
* **from**: 'top' | 'bottom' | 'left' | 'right' (default: 'top') - flip direction
|
|
97
|
+
* **...props**: standard motion button props
|
|
98
|
+
*
|
|
99
|
+
============================ Form ========================================================================
|
|
100
|
+
* **primary**: true | false | "dashed" (default: true)
|
|
101
|
+
* **variant**: default | destructive | outline | secondary | ghost | link (default: "default")
|
|
102
|
+
* **align**: left | middle | right (default: "default")
|
|
103
|
+
* **size**: default | sm | lg | icon (default: 'sm')
|
|
104
|
+
* **loadingText**: string (default: "Submitting...")
|
|
105
|
+
* **className**: string
|
|
106
|
+
* **children**: React.ReactNode
|
|
107
|
+
* **...props**: standard button props
|
|
108
|
+
*
|
|
109
|
+
============================ Float =======================================================================
|
|
110
|
+
* **icon**: React.ReactNode
|
|
111
|
+
* **description**: string - text description below icon
|
|
112
|
+
* **tooltip**: string - tooltip text on hover
|
|
113
|
+
* **type**: "default" | "primary" (default: "default")
|
|
114
|
+
* **shape**: "circle" | "square" (default: "circle")
|
|
115
|
+
* **size**: "small" | "default" | "large" (default: "default")
|
|
116
|
+
* **className**: string
|
|
117
|
+
* **onClick**: function
|
|
118
|
+
* **children**: React.ReactNode
|
|
119
|
+
*
|
|
120
|
+
============================ Hold ========================================================================
|
|
121
|
+
* **holdDuration**: number (default: 3000) - time in ms to hold
|
|
122
|
+
* **onComplete**: function - callback when hold completes
|
|
123
|
+
* **icon**: React.ReactNode
|
|
124
|
+
* **holdText**: string (default: "Hold me")
|
|
125
|
+
* **releaseText**: string (default: "Release")
|
|
126
|
+
* **holdVariant**: "red" | "green" | "blue" | "orange" | "grey" (default: "red")
|
|
127
|
+
* **className**: string
|
|
128
|
+
* **disabled**: boolean
|
|
129
|
+
* **...props**: standard button props
|
|
130
|
+
*
|
|
131
|
+
============================ Input =======================================================================
|
|
132
|
+
* **placeholder**: string (default: "Enter text") - for InputButtonInput
|
|
133
|
+
* **submitText**: string (default: "Submit") - for InputButtonSubmit
|
|
134
|
+
* **className**: string
|
|
135
|
+
* **transition**: object (default: { type: 'spring', stiffness: 300, damping: 20 })
|
|
136
|
+
* **showInput**: boolean - controlled input visibility
|
|
137
|
+
* **setShowInput**: function - controlled input visibility handler
|
|
138
|
+
* **id**: string - unique identifier
|
|
139
|
+
* **children**: React.ReactNode (or default structure)
|
|
140
|
+
* **...props**: standard div props
|
|
141
|
+
*
|
|
142
|
+
============================ Liquid ======================================================================
|
|
143
|
+
* **className**: string
|
|
144
|
+
* **variant**: default | destructive | outline | secondary | ghost | link
|
|
145
|
+
* **size**: default | sm | lg | icon
|
|
146
|
+
* **children**: React.ReactNode
|
|
147
|
+
* **...props**: standard motion button props
|
|
148
|
+
*
|
|
149
|
+
============================ Particle ====================================================================
|
|
150
|
+
* **children**: React.ReactNode
|
|
151
|
+
* **onClick**: function
|
|
152
|
+
* **onSuccess**: function - callback on click
|
|
153
|
+
* **successDuration**: number (default: 1000) - particle animation duration
|
|
154
|
+
* **particleCount**: number | "low" | "high" (default: 6) - number of particles (low=4, high=10)
|
|
155
|
+
* **showIcon**: boolean (default: true)
|
|
156
|
+
* **icon**: React.ReactNode
|
|
157
|
+
* **variant**: default | destructive | outline | secondary | ghost | link
|
|
158
|
+
* **size**: default | sm | lg | icon
|
|
159
|
+
* **align**: left | middle | right
|
|
160
|
+
* **className**: string
|
|
161
|
+
* **disabled**: boolean
|
|
162
|
+
* **...props**: standard button props
|
|
163
|
+
*
|
|
164
|
+
============================ Rate ========================================================================
|
|
165
|
+
* **defaultValue**: number (default: 0) - initial rating value
|
|
166
|
+
* **value**: number - controlled rating value
|
|
167
|
+
* **onChange**: function(event, value) - callback on rating change
|
|
168
|
+
* **onValueChange**: function(value) - callback on rating value change
|
|
169
|
+
* **readOnly**: boolean (default: false)
|
|
170
|
+
* **className**: string
|
|
171
|
+
* **starSize**: number (default: 20) - size of star icons
|
|
172
|
+
* **icon**: React.ReactNode (default: <StarIcon />)
|
|
173
|
+
* **children**: React.ReactNode (or auto-generated 5 stars)
|
|
174
|
+
*
|
|
175
|
+
============================ Rate1 =======================================================================
|
|
176
|
+
* **defaultValue**: number (default: 3) - initial rating value
|
|
177
|
+
* **value**: number - controlled rating value
|
|
178
|
+
* **onChange**: function(event, value) - callback on rating change
|
|
179
|
+
* **onValueChange**: function(value) - callback on rating value change
|
|
180
|
+
* **readOnly**: boolean (default: false)
|
|
181
|
+
* **className**: string
|
|
182
|
+
* **showText**: boolean (default: true) - show rating text below
|
|
183
|
+
* **text**: string - custom text to display
|
|
184
|
+
* **children**: React.ReactNode (or auto-generated 5 stars)
|
|
185
|
+
* **...props**: standard div props
|
|
186
|
+
*
|
|
187
|
+
============================ Ripple ======================================================================
|
|
188
|
+
* **ref**: React.Ref<HTMLButtonElement>
|
|
189
|
+
* **children**: React.ReactNode
|
|
190
|
+
* **onClick**: function
|
|
191
|
+
* **className**: string
|
|
192
|
+
* **rippleClassName**: string - styling for ripple effect
|
|
193
|
+
* **variant**: default | destructive | outline | secondary | ghost | link
|
|
194
|
+
* **size**: default | sm | lg | icon
|
|
195
|
+
* **scale**: number (default: 10) - ripple scale multiplier
|
|
196
|
+
* **transition**: object (default: { duration: 0.6, ease: 'easeOut' })
|
|
197
|
+
* **...props**: standard motion button props
|
|
198
|
+
*
|
|
199
|
+
============================ Social ======================================================================
|
|
200
|
+
* **shareButtons**: array of { icon: Component, label: string, onClick?: function } (default: Twitter, Instagram, LinkedIn, Link)
|
|
201
|
+
* **label**: string (default: "Hover me")
|
|
202
|
+
* **icon**: React.ReactNode
|
|
203
|
+
* **variant**: "light" | "dark" | "outline"
|
|
204
|
+
* **className**: string
|
|
205
|
+
* **...props**: standard button props
|
|
206
|
+
*
|
|
207
|
+
============================ Split =======================================================================
|
|
208
|
+
* **label**: string | null (default: null) - dropdown label or ChevronDown icon
|
|
209
|
+
* **icon**: React.ReactNode
|
|
210
|
+
* **items**: array of { label: string, icon?: ReactNode, command?: function, separator?: boolean } (required)
|
|
211
|
+
* **onClick**: function - main button click handler
|
|
212
|
+
* **className**: string
|
|
213
|
+
* **containerClassName**: string - wrapper styling
|
|
214
|
+
* **variant**: "default" | "destructive" | "outline" | "secondary" | "ghost" (default: "default")
|
|
215
|
+
* **align**: left | middle | right (default: "default")
|
|
216
|
+
* **size**: default | sm | lg | icon (default: "sm")
|
|
217
|
+
* **children**: React.ReactNode
|
|
218
|
+
* **...props**: standard button props
|
|
219
|
+
*
|
|
220
|
+
============================ Switch ======================================================================
|
|
221
|
+
* **checked**: boolean (default: false) - switch state
|
|
222
|
+
* **onCheckedChange**: function(checked: boolean) - callback on state change
|
|
223
|
+
* **className**: string
|
|
224
|
+
* **variant**: 'default' | 'primary' | 'secondary' | 'destructive' (default: 'default')
|
|
225
|
+
* **size**: 'sm' | 'md' | 'lg' (default: 'md')
|
|
226
|
+
* **disabled**: boolean (default: false)
|
|
227
|
+
* **labels**: { checked: string, unchecked: string } - optional labels for states
|
|
228
|
+
*
|
|
229
|
+
============================ Toggle ======================================================================
|
|
230
|
+
* **checked**: boolean (default: false) - toggle state
|
|
231
|
+
* **onChange**: function(checked: boolean) - callback on state change
|
|
232
|
+
* **disabled**: boolean (default: false)
|
|
233
|
+
* **className**: string
|
|
234
|
+
* **variant**: "default" | "outline" | "ghost" (default: "default")
|
|
235
|
+
* **size**: "sm" | "md" | "lg" | "icon" (default: "md")
|
|
236
|
+
* **children**: React.ReactNode (default: "Toggle")
|
|
237
|
+
* **...props**: standard button props
|
|
238
|
+
*
|
|
239
|
+
============================ Slide =======================================================================
|
|
240
|
+
* **className**: string
|
|
241
|
+
* **variant**: string (slideTextButtonVariants)
|
|
242
|
+
* **size**: "sm" | "default" | "lg" (default: "default")
|
|
243
|
+
* **text**: string (default: "Browse Components") - default text
|
|
244
|
+
* **hoverText**: string - text on hover (defaults to text if not provided)
|
|
245
|
+
* **to**: string (default: "/") - navigation path
|
|
246
|
+
* **prefetch**: "none" | "intent" | "render" | "viewport" (default: "intent")
|
|
247
|
+
* **...props**: Link component props
|
|
248
|
+
*
|
|
249
|
+
============================ TextReveal ==================================================================
|
|
250
|
+
* **text**: string (default: "shadcn.io") - text to reveal
|
|
251
|
+
* **revealColor**: string (default: "#37FF8B") - revealed text color
|
|
252
|
+
* **strokeColor**: string (default: "rgba(100, 100, 100, 0.7)") - text stroke color
|
|
253
|
+
* **className**: string
|
|
254
|
+
* **style**: React.CSSProperties
|
|
255
|
+
* **ref**: React.Ref<HTMLButtonElement>
|
|
256
|
+
* **...props**: standard button props
|
|
257
|
+
*
|
|
258
|
+
============================ Shimmer =====================================================================
|
|
259
|
+
* **shimmerColor**: string (default: "#ffffff") - shimmer effect color
|
|
260
|
+
* **shimmerSize**: string (default: "0.05em") - shimmer width
|
|
261
|
+
* **shimmerDuration**: string (default: "3s") - animation duration
|
|
262
|
+
* **borderRadius**: string (default: "100px") - button border radius
|
|
263
|
+
* **background**: string (default: "rgba(0, 0, 0, 1)") - button background
|
|
264
|
+
* **className**: string
|
|
265
|
+
* **children**: React.ReactNode
|
|
266
|
+
* **ref**: React.Ref<HTMLButtonElement>
|
|
267
|
+
* **...props**: standard button props
|
|
268
|
+
*
|
|
269
|
+
============================ TooltipButton ===============================================================
|
|
270
|
+
* **icon**: React.ReactNode (required) - icon to display
|
|
271
|
+
* **content**: string (required) - tooltip content
|
|
272
|
+
* **onClick**: function (default: null) - click handler
|
|
273
|
+
* **size**: "default" | "sm" | "lg" | "icon" (default: 'icon')
|
|
274
|
+
* **variant**: default | destructive | outline | secondary | ghost | link (default: 'ghost')
|
|
275
|
+
* **className**: string
|
|
276
|
+
*
|
|
277
|
+
*/
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
function ButtonX({ button = 'default', children, ...props }: any) {
|
|
281
|
+
switch (button) {
|
|
282
|
+
case 'Attract':
|
|
283
|
+
return <AttractButton {...props}>{children}</AttractButton>;
|
|
284
|
+
case 'Burger':
|
|
285
|
+
return <BurgerButton {...props}>{children}</BurgerButton>;
|
|
286
|
+
case 'Command':
|
|
287
|
+
return <CommandButton {...props}>{children}</CommandButton>;
|
|
288
|
+
case 'Copy':
|
|
289
|
+
return <CopyButton {...props}>{children}</CopyButton>;
|
|
290
|
+
case 'CopyText':
|
|
291
|
+
return <CopyText {...props}>{children}</CopyText>;
|
|
292
|
+
case 'Export':
|
|
293
|
+
return <ExportFile {...props} />;
|
|
294
|
+
case 'Flip':
|
|
295
|
+
return <FlipButton {...props} />;
|
|
296
|
+
case 'Form':
|
|
297
|
+
return <FormButton {...props}>{children}</FormButton>;
|
|
298
|
+
case 'Float':
|
|
299
|
+
return <FloatButton {...props}>{children}</FloatButton>;
|
|
300
|
+
case 'Hold':
|
|
301
|
+
return <HoldButton {...props}>{children}</HoldButton>;
|
|
302
|
+
case 'Input':
|
|
303
|
+
return (
|
|
304
|
+
<InputButtonProvider {...props}>
|
|
305
|
+
<InputButton>
|
|
306
|
+
<InputButtonAction>{children || "Click me"}</InputButtonAction>
|
|
307
|
+
<InputButtonInput placeholder={props.placeholder || "Enter text"} />
|
|
308
|
+
<InputButtonSubmit>{props.submitText || "Submit"}</InputButtonSubmit>
|
|
309
|
+
</InputButton>
|
|
310
|
+
</InputButtonProvider>
|
|
311
|
+
);
|
|
312
|
+
case 'Liquid':
|
|
313
|
+
return <LiquidButton {...props}>{children}</LiquidButton>;
|
|
314
|
+
case 'Particle':
|
|
315
|
+
return <ParticleButton {...props}>{children}</ParticleButton>;
|
|
316
|
+
case 'Rate':
|
|
317
|
+
return (
|
|
318
|
+
<Rating
|
|
319
|
+
defaultValue={props.defaultValue || 0}
|
|
320
|
+
value={props.value}
|
|
321
|
+
onChange={props.onChange}
|
|
322
|
+
onValueChange={props.onValueChange}
|
|
323
|
+
readOnly={props.readOnly || false}
|
|
324
|
+
className={props.className}
|
|
325
|
+
>
|
|
326
|
+
{children || Array.from({ length: 5 }).map((_, index) => (
|
|
327
|
+
<RatingButton
|
|
328
|
+
key={index}
|
|
329
|
+
index={index}
|
|
330
|
+
size={props.starSize || 20}
|
|
331
|
+
icon={props.icon || <StarIcon />}
|
|
332
|
+
/>
|
|
333
|
+
))}
|
|
334
|
+
</Rating>
|
|
335
|
+
);
|
|
336
|
+
case 'Rate1':
|
|
337
|
+
return (
|
|
338
|
+
<div>
|
|
339
|
+
<Rate1 defaultValue={props.defaultValue || 3} {...props}>
|
|
340
|
+
{children || Array.from({ length: 5 }).map((_, index) => (
|
|
341
|
+
<RatingButton1 key={index} />
|
|
342
|
+
))}
|
|
343
|
+
</Rate1>
|
|
344
|
+
{props.showText !== false && (
|
|
345
|
+
<p className="text-sm text-muted-foreground mt-2">
|
|
346
|
+
{props.text || `Rating: ${props.defaultValue || 3} stars`}
|
|
347
|
+
</p>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
case 'Ripple':
|
|
352
|
+
return <RippleButton {...props}>{children}</RippleButton>;
|
|
353
|
+
case 'Social':
|
|
354
|
+
return <SocialButton {...props}>{children}</SocialButton>;
|
|
355
|
+
case 'Split':
|
|
356
|
+
return <SplitButton {...props}>{children}</SplitButton>;
|
|
357
|
+
case 'Switch':
|
|
358
|
+
return <SwitchButton {...props}>{children}</SwitchButton>;
|
|
359
|
+
case 'Toggle':
|
|
360
|
+
return <ToggleButton {...props}>{children || "Toggle"}</ToggleButton>;
|
|
361
|
+
case 'Slide':
|
|
362
|
+
return <SlideButton {...props}>{children}</SlideButton>;
|
|
363
|
+
case 'TextReveal':
|
|
364
|
+
return <TextRevealButton {...props}>{children}</TextRevealButton>;
|
|
365
|
+
case 'Shimmer':
|
|
366
|
+
return <ShimmerButton {...props}>{children}</ShimmerButton>;
|
|
367
|
+
case 'TooltipButton':
|
|
368
|
+
return <TooltipButton {...props} />;
|
|
369
|
+
case 'default':
|
|
370
|
+
default:
|
|
371
|
+
return <Button {...props}>{children}</Button>;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
ButtonX.displayName = "ButtonX"
|
|
376
|
+
|
|
377
|
+
export { ButtonX }
|
|
378
|
+
|
|
379
|
+
function Button({
|
|
380
|
+
className,
|
|
381
|
+
variant,
|
|
382
|
+
size,
|
|
383
|
+
align,
|
|
384
|
+
effect,
|
|
385
|
+
icon: Icon,
|
|
386
|
+
iconPlacement = "left",
|
|
387
|
+
prefetch = "intent",
|
|
388
|
+
to = "/",
|
|
389
|
+
nav = null,
|
|
390
|
+
newTab = true,
|
|
391
|
+
asChild = false,
|
|
392
|
+
type = null,
|
|
393
|
+
children,
|
|
394
|
+
...props
|
|
395
|
+
}: React.ComponentProps<"button"> &
|
|
396
|
+
VariantProps<typeof buttonVariants> & {
|
|
397
|
+
asChild?: boolean
|
|
398
|
+
}) {
|
|
399
|
+
const Comp = asChild ? Slot : "button"
|
|
400
|
+
const n = useNavigation()
|
|
401
|
+
const resolvedTo = useResolvedPath(to);
|
|
402
|
+
const isSubmitting = n.state === 'submitting';
|
|
403
|
+
const isNavigating = n.state === 'loading' && n.location?.pathname === resolvedTo.pathname;
|
|
404
|
+
const isDisabled = isSubmitting && isNavigating;
|
|
405
|
+
let loadingText = 'Navigating...'
|
|
406
|
+
if (nav === 'navlink') {
|
|
407
|
+
return (
|
|
408
|
+
<Link
|
|
409
|
+
to={to}
|
|
410
|
+
prefetch={prefetch}
|
|
411
|
+
preventScrollReset
|
|
412
|
+
disabled={isDisabled}
|
|
413
|
+
{...props}>
|
|
414
|
+
<Comp
|
|
415
|
+
data-slot="button"
|
|
416
|
+
type={type || 'button'}
|
|
417
|
+
className={cn(buttonVariants({ variant, effect, size, align, className }))}
|
|
418
|
+
disabled={isDisabled}
|
|
419
|
+
>
|
|
420
|
+
{isNavigating ? (
|
|
421
|
+
<div className='flex items-center gap-3'>
|
|
422
|
+
<SpinnerV4 title={loadingText} subtitle='' size='icon' />
|
|
423
|
+
</div>
|
|
424
|
+
) : (
|
|
425
|
+
children
|
|
426
|
+
)}
|
|
427
|
+
</Comp>
|
|
428
|
+
</Link>
|
|
429
|
+
)
|
|
430
|
+
} else if (nav === 'anchor') {
|
|
431
|
+
const anchorProps = newTab ? {
|
|
432
|
+
target: "_blank",
|
|
433
|
+
rel: "noopener noreferrer"
|
|
434
|
+
} : {};
|
|
435
|
+
return (
|
|
436
|
+
<a
|
|
437
|
+
href={to}
|
|
438
|
+
preventScrollReset
|
|
439
|
+
disabled={isDisabled}
|
|
440
|
+
{...anchorProps}
|
|
441
|
+
>
|
|
442
|
+
<Comp
|
|
443
|
+
data-slot="button"
|
|
444
|
+
type={type || 'button'}
|
|
445
|
+
className={cn(buttonVariants({ variant, effect, size, align, className }))}
|
|
446
|
+
disabled={isDisabled}
|
|
447
|
+
{...props}
|
|
448
|
+
>
|
|
449
|
+
{isNavigating ? (
|
|
450
|
+
<div className='flex items-center gap-3'>
|
|
451
|
+
<SpinnerV4 title={loadingText} subtitle='' size='icon' />
|
|
452
|
+
</div>
|
|
453
|
+
) : (
|
|
454
|
+
children
|
|
455
|
+
)}
|
|
456
|
+
</Comp>
|
|
457
|
+
</a>
|
|
458
|
+
)
|
|
459
|
+
} else if (effect) {
|
|
460
|
+
let loadingText = 'Submitting...'
|
|
461
|
+
return (
|
|
462
|
+
<Comp
|
|
463
|
+
data-slot="button"
|
|
464
|
+
type={type || 'submit'}
|
|
465
|
+
className={cn(buttonVariants({ variant, effect, size, align, className }))}
|
|
466
|
+
{...props}
|
|
467
|
+
>
|
|
468
|
+
{Icon &&
|
|
469
|
+
iconPlacement === 'left' &&
|
|
470
|
+
(effect === 'expandIcon' ? (
|
|
471
|
+
<div className="w-0 translate-x-[0%] pr-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-100 group-hover:pr-2 group-hover:opacity-100">
|
|
472
|
+
<Icon />
|
|
473
|
+
</div>
|
|
474
|
+
) : (
|
|
475
|
+
<Icon />
|
|
476
|
+
))}
|
|
477
|
+
|
|
478
|
+
{isNavigating ? (
|
|
479
|
+
<div className='flex items-center gap-3'>
|
|
480
|
+
<SpinnerV4 title={loadingText} subtitle='' size='icon' />
|
|
481
|
+
</div>
|
|
482
|
+
) : (
|
|
483
|
+
children
|
|
484
|
+
)}
|
|
485
|
+
{Icon &&
|
|
486
|
+
iconPlacement === 'right' &&
|
|
487
|
+
(effect === 'expandIcon' ? (
|
|
488
|
+
<div className="w-0 translate-x-[100%] pl-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-0 group-hover:pl-2 group-hover:opacity-100">
|
|
489
|
+
<Icon />
|
|
490
|
+
</div>
|
|
491
|
+
) : (
|
|
492
|
+
<Icon />
|
|
493
|
+
))}
|
|
494
|
+
</Comp>
|
|
495
|
+
)
|
|
496
|
+
} else {
|
|
497
|
+
let loadingText = 'Submitting...'
|
|
498
|
+
return (
|
|
499
|
+
<Comp
|
|
500
|
+
data-slot="button"
|
|
501
|
+
type={type || 'submit'}
|
|
502
|
+
className={cn(buttonVariants({ variant, effect, size, align, className }))}
|
|
503
|
+
{...props}
|
|
504
|
+
>
|
|
505
|
+
{isNavigating ? (
|
|
506
|
+
<div className='flex items-center gap-3'>
|
|
507
|
+
<SpinnerV4 title={loadingText} subtitle='' size='icon' />
|
|
508
|
+
</div>
|
|
509
|
+
) : (
|
|
510
|
+
children
|
|
511
|
+
)}
|
|
512
|
+
</Comp>
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
}
|
|
517
|
+
function AttractButton({
|
|
518
|
+
particleCount = 12,
|
|
519
|
+
attractRadius = 50,
|
|
520
|
+
attractVariant,
|
|
521
|
+
icon,
|
|
522
|
+
activeText = "Attracting",
|
|
523
|
+
inactiveText = "Hover me",
|
|
524
|
+
className,
|
|
525
|
+
...props
|
|
526
|
+
}: AttractButtonProps) {
|
|
527
|
+
const [isAttracting, setIsAttracting] = useState(false)
|
|
528
|
+
const [particles, setParticles] = useState<Particle[]>([])
|
|
529
|
+
const [particlePositions, setParticlePositions] = useState<{ [key: number]: { x: number; y: number } }>({})
|
|
530
|
+
|
|
531
|
+
useEffect(() => {
|
|
532
|
+
const newParticles = Array.from({ length: particleCount }, (_, i) => ({
|
|
533
|
+
id: i,
|
|
534
|
+
x: Math.random() * 360 - 180,
|
|
535
|
+
y: Math.random() * 360 - 180,
|
|
536
|
+
}))
|
|
537
|
+
setParticles(newParticles)
|
|
538
|
+
|
|
539
|
+
const initialPositions: { [key: number]: { x: number; y: number } } = {}
|
|
540
|
+
newParticles.forEach(p => {
|
|
541
|
+
initialPositions[p.id] = { x: p.x, y: p.y }
|
|
542
|
+
})
|
|
543
|
+
setParticlePositions(initialPositions)
|
|
544
|
+
}, [particleCount])
|
|
545
|
+
|
|
546
|
+
const handleInteractionStart = useCallback(() => {
|
|
547
|
+
setIsAttracting(true)
|
|
548
|
+
const attracted: { [key: number]: { x: number; y: number } } = {}
|
|
549
|
+
particles.forEach(p => {
|
|
550
|
+
attracted[p.id] = { x: 0, y: 0 }
|
|
551
|
+
})
|
|
552
|
+
setParticlePositions(attracted)
|
|
553
|
+
}, [particles])
|
|
554
|
+
|
|
555
|
+
const handleInteractionEnd = useCallback(() => {
|
|
556
|
+
setIsAttracting(false)
|
|
557
|
+
const scattered: { [key: number]: { x: number; y: number } } = {}
|
|
558
|
+
particles.forEach(p => {
|
|
559
|
+
scattered[p.id] = { x: p.x, y: p.y }
|
|
560
|
+
})
|
|
561
|
+
setParticlePositions(scattered)
|
|
562
|
+
}, [particles])
|
|
563
|
+
|
|
564
|
+
const particleColor = attractVariant === "violet"
|
|
565
|
+
? "bg-violet-400 dark:bg-violet-300"
|
|
566
|
+
: attractVariant === "blue"
|
|
567
|
+
? "bg-blue-400 dark:bg-blue-300"
|
|
568
|
+
: attractVariant === "green"
|
|
569
|
+
? "bg-green-400 dark:bg-green-300"
|
|
570
|
+
: "bg-primary"
|
|
571
|
+
|
|
572
|
+
return (
|
|
573
|
+
<Button
|
|
574
|
+
className={cn(attractButtonVariants({ attractVariant }), className)}
|
|
575
|
+
onMouseEnter={handleInteractionStart}
|
|
576
|
+
onMouseLeave={handleInteractionEnd}
|
|
577
|
+
onTouchStart={handleInteractionStart}
|
|
578
|
+
onTouchEnd={handleInteractionEnd}
|
|
579
|
+
asChild={false}
|
|
580
|
+
{...props}
|
|
581
|
+
>
|
|
582
|
+
<>
|
|
583
|
+
{particles.map((particle) => (
|
|
584
|
+
<div
|
|
585
|
+
key={particle.id}
|
|
586
|
+
className={cn(
|
|
587
|
+
"absolute w-1.5 h-1.5 rounded-full transition-all duration-300",
|
|
588
|
+
particleColor,
|
|
589
|
+
isAttracting ? "opacity-100" : "opacity-40"
|
|
590
|
+
)}
|
|
591
|
+
style={{
|
|
592
|
+
transform: `translate(${particlePositions[particle.id]?.x || particle.x}px, ${particlePositions[particle.id]?.y || particle.y}px)`,
|
|
593
|
+
}}
|
|
594
|
+
/>
|
|
595
|
+
))}
|
|
596
|
+
<span className="relative w-full flex items-center justify-center gap-2">
|
|
597
|
+
{icon || (
|
|
598
|
+
<Magnet
|
|
599
|
+
className={cn(
|
|
600
|
+
"w-4 h-4 transition-transform duration-300",
|
|
601
|
+
isAttracting && "scale-110"
|
|
602
|
+
)}
|
|
603
|
+
/>
|
|
604
|
+
)}
|
|
605
|
+
{isAttracting ? activeText : inactiveText}
|
|
606
|
+
</span>
|
|
607
|
+
</>
|
|
608
|
+
</Button>
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
function CommandButton({
|
|
612
|
+
className,
|
|
613
|
+
variant,
|
|
614
|
+
size,
|
|
615
|
+
children,
|
|
616
|
+
...props
|
|
617
|
+
}: React.ComponentProps<"button"> &
|
|
618
|
+
VariantProps<typeof commandButtonVariants> & {
|
|
619
|
+
children?: React.ReactNode
|
|
620
|
+
}) {
|
|
621
|
+
return (
|
|
622
|
+
<button
|
|
623
|
+
data-slot="command-button"
|
|
624
|
+
className={cn(commandButtonVariants({ variant, size, className }))}
|
|
625
|
+
{...props}
|
|
626
|
+
>
|
|
627
|
+
<Command
|
|
628
|
+
className={cn(
|
|
629
|
+
"text-zinc-600 dark:text-zinc-400 transition-all duration-300 group-hover:scale-110 group-hover:rotate-[-4deg] group-active:scale-95",
|
|
630
|
+
size === "sm" && "size-3",
|
|
631
|
+
size === "default" && "size-4",
|
|
632
|
+
size === "lg" && "size-5"
|
|
633
|
+
)}
|
|
634
|
+
/>
|
|
635
|
+
<span className="text-zinc-600 dark:text-zinc-400">
|
|
636
|
+
{children || "CMD + K"}
|
|
637
|
+
</span>
|
|
638
|
+
<span
|
|
639
|
+
className={cn(
|
|
640
|
+
"absolute inset-0 bg-gradient-to-r from-indigo-500/0 via-indigo-500/10 to-indigo-500/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-500 ease-out"
|
|
641
|
+
)}
|
|
642
|
+
/>
|
|
643
|
+
</button>
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
function CopyButton({
|
|
647
|
+
content,
|
|
648
|
+
className,
|
|
649
|
+
size,
|
|
650
|
+
variant,
|
|
651
|
+
delay = 3000,
|
|
652
|
+
onClick,
|
|
653
|
+
onCopy,
|
|
654
|
+
isCopied,
|
|
655
|
+
onCopyChange,
|
|
656
|
+
...props
|
|
657
|
+
}: CopyButtonProps) {
|
|
658
|
+
const [localIsCopied, setLocalIsCopied] = React.useState(isCopied ?? false);
|
|
659
|
+
const Icon = localIsCopied ? CheckIcon : CopyIcon;
|
|
660
|
+
|
|
661
|
+
React.useEffect(() => {
|
|
662
|
+
setLocalIsCopied(isCopied ?? false);
|
|
663
|
+
}, [isCopied]);
|
|
664
|
+
|
|
665
|
+
const handleIsCopied = React.useCallback(
|
|
666
|
+
(isCopied: boolean) => {
|
|
667
|
+
setLocalIsCopied(isCopied);
|
|
668
|
+
onCopyChange?.(isCopied);
|
|
669
|
+
},
|
|
670
|
+
[onCopyChange],
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
const handleCopy = React.useCallback(
|
|
674
|
+
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
675
|
+
if (isCopied) return;
|
|
676
|
+
if (content) {
|
|
677
|
+
navigator.clipboard
|
|
678
|
+
.writeText(content)
|
|
679
|
+
.then(() => {
|
|
680
|
+
handleIsCopied(true);
|
|
681
|
+
setTimeout(() => handleIsCopied(false), delay);
|
|
682
|
+
onCopy?.(content);
|
|
683
|
+
})
|
|
684
|
+
.catch((error) => {
|
|
685
|
+
console.error('Error copying command', error);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
onClick?.(e);
|
|
689
|
+
},
|
|
690
|
+
[isCopied, content, delay, onClick, onCopy, handleIsCopied],
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
return (
|
|
694
|
+
<motion.button
|
|
695
|
+
data-slot="copy-button"
|
|
696
|
+
whileHover={{ scale: 1.05 }}
|
|
697
|
+
whileTap={{ scale: 0.95 }}
|
|
698
|
+
className={cn(buttonVariants({ variant, size }), className)}
|
|
699
|
+
onClick={handleCopy}
|
|
700
|
+
{...props}
|
|
701
|
+
>
|
|
702
|
+
<AnimatePresence mode="wait">
|
|
703
|
+
<motion.span
|
|
704
|
+
key={localIsCopied ? 'check' : 'copy'}
|
|
705
|
+
data-slot="copy-button-icon"
|
|
706
|
+
initial={{ scale: 0 }}
|
|
707
|
+
animate={{ scale: 1 }}
|
|
708
|
+
exit={{ scale: 0 }}
|
|
709
|
+
transition={{ duration: 0.15 }}
|
|
710
|
+
>
|
|
711
|
+
<Icon />
|
|
712
|
+
</motion.span>
|
|
713
|
+
</AnimatePresence>
|
|
714
|
+
</motion.button>
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
function CopyText({
|
|
718
|
+
code,
|
|
719
|
+
className,
|
|
720
|
+
ttText = 'Copy',
|
|
721
|
+
icon = CopyIcon,
|
|
722
|
+
size = 'icon',
|
|
723
|
+
variant = 'ghost',
|
|
724
|
+
...props
|
|
725
|
+
}: CopyButtonProps) {
|
|
726
|
+
const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
727
|
+
|
|
728
|
+
const Icon = isCopied ? CheckIcon : icon;
|
|
729
|
+
|
|
730
|
+
return (
|
|
731
|
+
<Tooltip>
|
|
732
|
+
<TooltipTrigger>
|
|
733
|
+
<motion.button
|
|
734
|
+
data-slot="copy-button"
|
|
735
|
+
whileHover={{ scale: 1.05 }}
|
|
736
|
+
whileTap={{ scale: 0.95 }}
|
|
737
|
+
className={cn(buttonVariants({ variant, size }), className)}
|
|
738
|
+
onClick={() => copyToClipboard(code)}
|
|
739
|
+
{...props}
|
|
740
|
+
>
|
|
741
|
+
<AnimatePresence mode="wait">
|
|
742
|
+
<motion.span
|
|
743
|
+
key={isCopied ? 'check' : 'copy'}
|
|
744
|
+
data-slot="copy-button-icon"
|
|
745
|
+
initial={{ scale: 0 }}
|
|
746
|
+
animate={{ scale: 1 }}
|
|
747
|
+
exit={{ scale: 0 }}
|
|
748
|
+
transition={{ duration: 0.15 }}
|
|
749
|
+
>
|
|
750
|
+
<Icon />
|
|
751
|
+
</motion.span>
|
|
752
|
+
</AnimatePresence>
|
|
753
|
+
</motion.button>
|
|
754
|
+
</TooltipTrigger>
|
|
755
|
+
<TooltipContent>
|
|
756
|
+
<p>{ttText}</p>
|
|
757
|
+
</TooltipContent>
|
|
758
|
+
</Tooltip>
|
|
759
|
+
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
function ExportFile({ code = null, filename = 'MyComponent.tsx', className }) {
|
|
763
|
+
const exportFile = useExport();
|
|
764
|
+
|
|
765
|
+
const handleClick = () => {
|
|
766
|
+
if (!code || !filename) return null;
|
|
767
|
+
exportFile(code, filename);
|
|
768
|
+
};
|
|
769
|
+
return (
|
|
770
|
+
<TooltipButton icon={<Download />} onClick={handleClick} content="Export File" className={cn("h-8 w-8 cursor-pointer hover:bg-background/80", className)} />
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
const DEFAULT_SPAN_CLASS_NAME = 'absolute inset-0 flex items-center justify-center rounded-lg';
|
|
774
|
+
function FlipButton({
|
|
775
|
+
frontText,
|
|
776
|
+
backText,
|
|
777
|
+
transition = { type: 'spring', stiffness: 280, damping: 20 },
|
|
778
|
+
className,
|
|
779
|
+
frontClassName,
|
|
780
|
+
backClassName,
|
|
781
|
+
from = 'top',
|
|
782
|
+
...props
|
|
783
|
+
}: FlipButtonProps) {
|
|
784
|
+
const isVertical = from === 'top' || from === 'bottom';
|
|
785
|
+
const rotateAxis = isVertical ? 'rotateX' : 'rotateY';
|
|
786
|
+
|
|
787
|
+
const frontOffset = from === 'top' || from === 'left' ? '50%' : '-50%';
|
|
788
|
+
const backOffset = from === 'top' || from === 'left' ? '-50%' : '50%';
|
|
789
|
+
|
|
790
|
+
const buildVariant = (
|
|
791
|
+
opacity: number,
|
|
792
|
+
rotation: number,
|
|
793
|
+
offset: string | null = null,
|
|
794
|
+
): Variant => ({
|
|
795
|
+
opacity,
|
|
796
|
+
[rotateAxis]: rotation,
|
|
797
|
+
...(isVertical && offset !== null ? { y: offset } : {}),
|
|
798
|
+
...(!isVertical && offset !== null ? { x: offset } : {}),
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
const frontVariants = {
|
|
802
|
+
initial: buildVariant(1, 0, '0%'),
|
|
803
|
+
hover: buildVariant(0, 90, frontOffset),
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const backVariants = {
|
|
807
|
+
initial: buildVariant(0, 90, backOffset),
|
|
808
|
+
hover: buildVariant(1, 0, '0%'),
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
return (
|
|
812
|
+
<motion.button
|
|
813
|
+
data-slot="flip-button"
|
|
814
|
+
initial="initial"
|
|
815
|
+
whileHover="hover"
|
|
816
|
+
whileTap={{ scale: 0.95 }}
|
|
817
|
+
className={cn(
|
|
818
|
+
'relative inline-block h-10 px-4 py-2 text-sm font-medium cursor-pointer perspective-[1000px] focus:outline-none',
|
|
819
|
+
className,
|
|
820
|
+
)}
|
|
821
|
+
{...props}
|
|
822
|
+
>
|
|
823
|
+
<motion.span
|
|
824
|
+
data-slot="flip-button-front"
|
|
825
|
+
variants={frontVariants}
|
|
826
|
+
transition={transition}
|
|
827
|
+
className={cn(
|
|
828
|
+
DEFAULT_SPAN_CLASS_NAME,
|
|
829
|
+
'bg-muted text-black dark:text-white',
|
|
830
|
+
frontClassName,
|
|
831
|
+
)}
|
|
832
|
+
>
|
|
833
|
+
{frontText}
|
|
834
|
+
</motion.span>
|
|
835
|
+
<motion.span
|
|
836
|
+
data-slot="flip-button-back"
|
|
837
|
+
variants={backVariants}
|
|
838
|
+
transition={transition}
|
|
839
|
+
className={cn(
|
|
840
|
+
DEFAULT_SPAN_CLASS_NAME,
|
|
841
|
+
'bg-primary text-primary-foreground',
|
|
842
|
+
backClassName,
|
|
843
|
+
)}
|
|
844
|
+
>
|
|
845
|
+
{backText}
|
|
846
|
+
</motion.span>
|
|
847
|
+
<span className="invisible">{frontText}</span>
|
|
848
|
+
</motion.button>
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
function FormButton({
|
|
852
|
+
primary = true, // true | false | dashed
|
|
853
|
+
variant = "default",
|
|
854
|
+
align = "default",
|
|
855
|
+
size = 'sm',
|
|
856
|
+
loadingText = "Submitting...",
|
|
857
|
+
className,
|
|
858
|
+
children,
|
|
859
|
+
...props
|
|
860
|
+
}: ButtonNavLinkProps) {
|
|
861
|
+
const nav = useNavigation()
|
|
862
|
+
|
|
863
|
+
const isSubmitting = nav.state === 'submitting';
|
|
864
|
+
|
|
865
|
+
const baseClasses = cn(
|
|
866
|
+
"transition duration-300 border-2 border-border w-[95%] text-center mx-auto text-foreground px-6 py-3 rounded-none",
|
|
867
|
+
buttonVariants({ variant, size, className, align }),
|
|
868
|
+
className
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const primaryClasses =
|
|
872
|
+
primary === true
|
|
873
|
+
? "bg-[#2563eb] hover:bg-[#16171a]"
|
|
874
|
+
: primary === false
|
|
875
|
+
? "bg-[#121212] hover:bg-[#2563eb]"
|
|
876
|
+
: primary === "dashed"
|
|
877
|
+
? "h-8 border-dashed"
|
|
878
|
+
: "";
|
|
879
|
+
return (
|
|
880
|
+
<Button
|
|
881
|
+
type="submit"
|
|
882
|
+
className={cn(baseClasses, primaryClasses)}
|
|
883
|
+
disabled={isSubmitting}
|
|
884
|
+
{...props}
|
|
885
|
+
>
|
|
886
|
+
{isSubmitting ? (
|
|
887
|
+
<div className='flex items-center gap-3'>
|
|
888
|
+
<div className="flex items-center justify-center h-full">
|
|
889
|
+
<div className='animate-spin rounded-full h-6 w-6 border-t-4 border-primary border-solid'></div>
|
|
890
|
+
</div>
|
|
891
|
+
<p className='ml-3'>{loadingText}</p>
|
|
892
|
+
</div>
|
|
893
|
+
) : (
|
|
894
|
+
children
|
|
895
|
+
)}
|
|
896
|
+
</Button>
|
|
897
|
+
);
|
|
898
|
+
};
|
|
899
|
+
function FloatButton({
|
|
900
|
+
icon,
|
|
901
|
+
description,
|
|
902
|
+
tooltip,
|
|
903
|
+
type = "default",
|
|
904
|
+
shape = "circle",
|
|
905
|
+
size = "default",
|
|
906
|
+
className,
|
|
907
|
+
onClick,
|
|
908
|
+
children
|
|
909
|
+
}: FloatButtonProps) {
|
|
910
|
+
const sizeClasses = {
|
|
911
|
+
small: "h-10 w-10 text-sm",
|
|
912
|
+
default: "h-12 w-12 text-base",
|
|
913
|
+
large: "h-14 w-14 text-lg"
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const shapeClasses = {
|
|
917
|
+
circle: "rounded-full",
|
|
918
|
+
square: "rounded-lg"
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const typeClasses = {
|
|
922
|
+
default: "bg-background border-border hover:bg-accent hover:text-accent-foreground",
|
|
923
|
+
primary: "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const buttonContent = (
|
|
927
|
+
<Button
|
|
928
|
+
variant={type === "primary" ? "default" : "outline"}
|
|
929
|
+
size="icon"
|
|
930
|
+
className={cn(
|
|
931
|
+
"fixed bottom-6 right-6 shadow-lg border transition-all duration-200 hover:shadow-xl",
|
|
932
|
+
sizeClasses[size],
|
|
933
|
+
shapeClasses[shape],
|
|
934
|
+
type === "default" && typeClasses.default,
|
|
935
|
+
type === "primary" && typeClasses.primary,
|
|
936
|
+
className
|
|
937
|
+
)}
|
|
938
|
+
onClick={onClick}
|
|
939
|
+
>
|
|
940
|
+
<div className="flex flex-col items-center justify-center gap-1">
|
|
941
|
+
{icon && <span className="flex items-center justify-center">{icon}</span>}
|
|
942
|
+
{description && (
|
|
943
|
+
<span className="text-xs font-medium truncate max-w-full">{description}</span>
|
|
944
|
+
)}
|
|
945
|
+
</div>
|
|
946
|
+
{children}
|
|
947
|
+
</Button>
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
if (tooltip) {
|
|
951
|
+
return (
|
|
952
|
+
<TooltipProvider>
|
|
953
|
+
<Tooltip>
|
|
954
|
+
<TooltipTrigger asChild>{buttonContent}</TooltipTrigger>
|
|
955
|
+
<TooltipContent side="left">
|
|
956
|
+
<p>{tooltip}</p>
|
|
957
|
+
</TooltipContent>
|
|
958
|
+
</Tooltip>
|
|
959
|
+
</TooltipProvider>
|
|
960
|
+
)
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return buttonContent
|
|
964
|
+
}
|
|
965
|
+
function HoldButton({
|
|
966
|
+
holdDuration = 3000,
|
|
967
|
+
onComplete,
|
|
968
|
+
icon,
|
|
969
|
+
holdText = "Hold me",
|
|
970
|
+
releaseText = "Release",
|
|
971
|
+
holdVariant = "red",
|
|
972
|
+
className,
|
|
973
|
+
disabled,
|
|
974
|
+
...props
|
|
975
|
+
}: HoldButtonProps) {
|
|
976
|
+
const [isHolding, setIsHolding] = useState(false)
|
|
977
|
+
const [progress, setProgress] = useState(0)
|
|
978
|
+
const startTimeRef = useRef<number | null>(null)
|
|
979
|
+
const animationFrameRef = useRef<number | null>(null)
|
|
980
|
+
|
|
981
|
+
const progressColor = {
|
|
982
|
+
red: "bg-red-200/30 dark:bg-red-300/30",
|
|
983
|
+
green: "bg-green-200/30 dark:bg-green-300/30",
|
|
984
|
+
blue: "bg-blue-200/30 dark:bg-blue-300/30",
|
|
985
|
+
orange: "bg-orange-200/30 dark:bg-orange-300/30",
|
|
986
|
+
grey: "bg-gray-200/30 dark:bg-gray-300/30",
|
|
987
|
+
}[holdVariant || "red"]
|
|
988
|
+
|
|
989
|
+
const defaultIcon = {
|
|
990
|
+
red: <Trash2 className="w-4 h-4" />,
|
|
991
|
+
green: <ArchiveX className="w-4 h-4" />,
|
|
992
|
+
blue: <XCircle className="w-4 h-4" />,
|
|
993
|
+
orange: <AlertCircle className="w-4 h-4" />,
|
|
994
|
+
grey: <Ban className="w-4 h-4" />,
|
|
995
|
+
}[holdVariant || "red"]
|
|
996
|
+
|
|
997
|
+
useEffect(() => {
|
|
998
|
+
if (!isHolding) {
|
|
999
|
+
setProgress(0)
|
|
1000
|
+
if (animationFrameRef.current) {
|
|
1001
|
+
cancelAnimationFrame(animationFrameRef.current)
|
|
1002
|
+
}
|
|
1003
|
+
return
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const animate = () => {
|
|
1007
|
+
if (startTimeRef.current === null) {
|
|
1008
|
+
startTimeRef.current = Date.now()
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const elapsed = Date.now() - startTimeRef.current
|
|
1012
|
+
const newProgress = Math.min((elapsed / holdDuration) * 100, 100)
|
|
1013
|
+
setProgress(newProgress)
|
|
1014
|
+
|
|
1015
|
+
if (newProgress >= 100) {
|
|
1016
|
+
setIsHolding(false)
|
|
1017
|
+
startTimeRef.current = null
|
|
1018
|
+
onComplete?.()
|
|
1019
|
+
} else {
|
|
1020
|
+
animationFrameRef.current = requestAnimationFrame(animate)
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
animationFrameRef.current = requestAnimationFrame(animate)
|
|
1025
|
+
|
|
1026
|
+
return () => {
|
|
1027
|
+
if (animationFrameRef.current) {
|
|
1028
|
+
cancelAnimationFrame(animationFrameRef.current)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}, [isHolding, holdDuration, onComplete])
|
|
1032
|
+
|
|
1033
|
+
const handleHoldStart = () => {
|
|
1034
|
+
if (disabled) return
|
|
1035
|
+
setIsHolding(true)
|
|
1036
|
+
startTimeRef.current = null
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const handleHoldEnd = () => {
|
|
1040
|
+
setIsHolding(false)
|
|
1041
|
+
startTimeRef.current = null
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return (
|
|
1045
|
+
<Button
|
|
1046
|
+
className={cn(holdButtonVariants({ holdVariant }), className)}
|
|
1047
|
+
onMouseDown={handleHoldStart}
|
|
1048
|
+
onMouseUp={handleHoldEnd}
|
|
1049
|
+
onMouseLeave={handleHoldEnd}
|
|
1050
|
+
onTouchStart={handleHoldStart}
|
|
1051
|
+
onTouchEnd={handleHoldEnd}
|
|
1052
|
+
onTouchCancel={handleHoldEnd}
|
|
1053
|
+
disabled={disabled}
|
|
1054
|
+
asChild={false}
|
|
1055
|
+
{...props}
|
|
1056
|
+
>
|
|
1057
|
+
<>
|
|
1058
|
+
<div
|
|
1059
|
+
className={cn("absolute left-0 top-0 h-full transition-all duration-100", progressColor)}
|
|
1060
|
+
style={{ width: `${progress}%` }}
|
|
1061
|
+
/>
|
|
1062
|
+
<span className="relative z-10 w-full flex items-center justify-center gap-2">
|
|
1063
|
+
{icon || defaultIcon}
|
|
1064
|
+
{isHolding ? releaseText : holdText}
|
|
1065
|
+
</span>
|
|
1066
|
+
</>
|
|
1067
|
+
</Button>
|
|
1068
|
+
)
|
|
1069
|
+
}
|
|
1070
|
+
const InputButtonContext = React.createContext<
|
|
1071
|
+
InputButtonContextType | undefined
|
|
1072
|
+
>(undefined);
|
|
1073
|
+
const useInputButton = (): InputButtonContextType => {
|
|
1074
|
+
const context = React.useContext(InputButtonContext);
|
|
1075
|
+
if (!context) {
|
|
1076
|
+
throw new Error('useInputButton must be used within a InputButton');
|
|
1077
|
+
}
|
|
1078
|
+
return context;
|
|
1079
|
+
};
|
|
1080
|
+
function InputButtonProvider({
|
|
1081
|
+
className,
|
|
1082
|
+
transition = { type: 'spring', stiffness: 300, damping: 20 },
|
|
1083
|
+
showInput,
|
|
1084
|
+
setShowInput,
|
|
1085
|
+
id,
|
|
1086
|
+
children,
|
|
1087
|
+
...props
|
|
1088
|
+
}: InputButtonProviderProps) {
|
|
1089
|
+
const localId = React.useId();
|
|
1090
|
+
const [localShowInput, setLocalShowInput] = React.useState(false);
|
|
1091
|
+
|
|
1092
|
+
return (
|
|
1093
|
+
<InputButtonContext.Provider
|
|
1094
|
+
value={{
|
|
1095
|
+
showInput: showInput ?? localShowInput,
|
|
1096
|
+
setShowInput: setShowInput ?? setLocalShowInput,
|
|
1097
|
+
transition,
|
|
1098
|
+
id: id ?? localId,
|
|
1099
|
+
}}
|
|
1100
|
+
>
|
|
1101
|
+
<div
|
|
1102
|
+
data-slot="input-button-provider"
|
|
1103
|
+
className={cn(
|
|
1104
|
+
'relative w-fit flex items-center justify-center h-10',
|
|
1105
|
+
(showInput || localShowInput) && 'w-full max-w-[400px]',
|
|
1106
|
+
className,
|
|
1107
|
+
)}
|
|
1108
|
+
{...props}
|
|
1109
|
+
>
|
|
1110
|
+
{children}
|
|
1111
|
+
</div>
|
|
1112
|
+
</InputButtonContext.Provider>
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
function InputButton({ className, ...props }: InputButtonProps) {
|
|
1116
|
+
return (
|
|
1117
|
+
<motion.div
|
|
1118
|
+
data-slot="input-button"
|
|
1119
|
+
className={cn('flex size-full', className)}
|
|
1120
|
+
{...props}
|
|
1121
|
+
/>
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
function InputButtonAction({ className, ...props }: InputButtonActionProps) {
|
|
1125
|
+
const { transition, setShowInput, id } = useInputButton();
|
|
1126
|
+
|
|
1127
|
+
return (
|
|
1128
|
+
<motion.button
|
|
1129
|
+
data-slot="input-button-action"
|
|
1130
|
+
className={cn(
|
|
1131
|
+
'bg-background text-sm whitespace-nowrap disabled:pointer-events-none disabled:opacity-50 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive rounded-full border text-background-foreground cursor-pointer pl-4 pr-12 size-full font-medium',
|
|
1132
|
+
className,
|
|
1133
|
+
)}
|
|
1134
|
+
layoutId={`input-button-action-${id}`}
|
|
1135
|
+
transition={transition}
|
|
1136
|
+
onClick={() => setShowInput((prev) => !prev)}
|
|
1137
|
+
{...props}
|
|
1138
|
+
/>
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
function InputButtonSubmit({
|
|
1142
|
+
className,
|
|
1143
|
+
children,
|
|
1144
|
+
icon: Icon = ArrowRight,
|
|
1145
|
+
...props
|
|
1146
|
+
}: InputButtonSubmitProps) {
|
|
1147
|
+
const { transition, showInput, setShowInput, id } = useInputButton();
|
|
1148
|
+
|
|
1149
|
+
return (
|
|
1150
|
+
<motion.button
|
|
1151
|
+
data-slot="input-button-submit"
|
|
1152
|
+
layoutId={`input-button-submit-${id}`}
|
|
1153
|
+
transition={transition}
|
|
1154
|
+
className={cn(
|
|
1155
|
+
"z-[1] [&_svg:not([class*='size-'])]:size-4 cursor-pointer disabled:pointer-events-none disabled:opacity-50 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap bg-primary hover:bg-primary/90 transition-colors text-primary-foreground rounded-full text-sm flex items-center justify-center font-medium absolute inset-y-1 right-1",
|
|
1156
|
+
showInput ? 'px-4' : 'aspect-square',
|
|
1157
|
+
className,
|
|
1158
|
+
)}
|
|
1159
|
+
onClick={() => setShowInput((prev) => !prev)}
|
|
1160
|
+
{...props}
|
|
1161
|
+
>
|
|
1162
|
+
{showInput ? (
|
|
1163
|
+
<motion.span
|
|
1164
|
+
key="show-button"
|
|
1165
|
+
initial={{ opacity: 0, scale: 0 }}
|
|
1166
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
1167
|
+
transition={{ duration: 0.2 }}
|
|
1168
|
+
>
|
|
1169
|
+
{children}
|
|
1170
|
+
</motion.span>
|
|
1171
|
+
) : (
|
|
1172
|
+
<motion.span
|
|
1173
|
+
key="show-icon"
|
|
1174
|
+
initial={{ opacity: 0, scale: 0 }}
|
|
1175
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
1176
|
+
transition={{ duration: 0.2 }}
|
|
1177
|
+
>
|
|
1178
|
+
{React.createElement(Icon as React.ComponentType<any>, { className: "size-4" })}
|
|
1179
|
+
</motion.span>
|
|
1180
|
+
)}
|
|
1181
|
+
</motion.button>
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
function InputButtonInput({ className, ...props }: InputButtonInputProps) {
|
|
1185
|
+
const { transition, showInput, id } = useInputButton();
|
|
1186
|
+
|
|
1187
|
+
return (
|
|
1188
|
+
<AnimatePresence>
|
|
1189
|
+
{showInput && (
|
|
1190
|
+
<div className="absolute inset-0 size-full flex items-center justify-center">
|
|
1191
|
+
<motion.div
|
|
1192
|
+
layoutId={`input-button-input-${id}`}
|
|
1193
|
+
className="size-full flex items-center bg-background rounded-full relative"
|
|
1194
|
+
transition={transition}
|
|
1195
|
+
>
|
|
1196
|
+
<input
|
|
1197
|
+
data-slot="input-button-input"
|
|
1198
|
+
className={cn(
|
|
1199
|
+
'size-full selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground inset-0 pl-4 focus-visible:border-ring border focus-visible:ring-ring/50 focus-visible:ring-[3px] pr-32 py-2 text-sm bg-background rounded-full focus:outline-none absolute shrink-0 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive disabled:pointer-events-none disabled:cursor-not-allowed',
|
|
1200
|
+
className,
|
|
1201
|
+
)}
|
|
1202
|
+
{...props}
|
|
1203
|
+
/>
|
|
1204
|
+
</motion.div>
|
|
1205
|
+
</div>
|
|
1206
|
+
)}
|
|
1207
|
+
</AnimatePresence>
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
function LiquidButton({
|
|
1211
|
+
className,
|
|
1212
|
+
variant,
|
|
1213
|
+
size,
|
|
1214
|
+
...props
|
|
1215
|
+
}: LiquidButtonProps) {
|
|
1216
|
+
return (
|
|
1217
|
+
<motion.button
|
|
1218
|
+
whileTap={{ scale: 0.95 }}
|
|
1219
|
+
whileHover={{ scale: 1.05 }}
|
|
1220
|
+
className={cn(buttonVariants({ variant, size, className }), "relative inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium cursor-pointer overflow-hidden disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive [background:_linear-gradient(var(--liquid-button-color)_0_0)_no-repeat_calc(200%-var(--liquid-button-fill,0%))_100%/200%_var(--liquid-button-fill,0.2em)] hover:[--liquid-button-fill:100%] hover:[--liquid-button-delay:0.3s] [transition:_background_0.3s_var(--liquid-button-delay,0s),_color_0.3s_var(--liquid-button-delay,0s),_background-position_0.3s_calc(0.3s_-_var(--liquid-button-delay,0s))] focus:outline-none [--liquid-button-color:var(--primary)]",)}
|
|
1221
|
+
{...props}
|
|
1222
|
+
/>
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
function SuccessParticles({ buttonRef, particleCount }: SuccessParticlesProps) {
|
|
1226
|
+
const rect = buttonRef.current?.getBoundingClientRect()
|
|
1227
|
+
if (!rect) return null
|
|
1228
|
+
|
|
1229
|
+
const centerX = rect.left + rect.width / 2
|
|
1230
|
+
const centerY = rect.top + rect.height / 2
|
|
1231
|
+
|
|
1232
|
+
return (
|
|
1233
|
+
<>
|
|
1234
|
+
{[...Array(particleCount)].map((_, i) => (
|
|
1235
|
+
<div
|
|
1236
|
+
key={i}
|
|
1237
|
+
className="fixed w-1 h-1 bg-foreground rounded-full pointer-events-none"
|
|
1238
|
+
style={{
|
|
1239
|
+
left: centerX,
|
|
1240
|
+
top: centerY,
|
|
1241
|
+
animation: `particle-${i} 0.6s ease-out forwards`,
|
|
1242
|
+
animationDelay: `${i * 0.1}s`,
|
|
1243
|
+
}}
|
|
1244
|
+
/>
|
|
1245
|
+
))}
|
|
1246
|
+
<style>
|
|
1247
|
+
{[...Array(particleCount)].map((_, i) => `
|
|
1248
|
+
@keyframes particle-${i} {
|
|
1249
|
+
0% {
|
|
1250
|
+
transform: scale(0) translate(0, 0);
|
|
1251
|
+
opacity: 1;
|
|
1252
|
+
}
|
|
1253
|
+
50% {
|
|
1254
|
+
transform: scale(1) translate(${(i % 2 ? 1 : -1) * (Math.random() * 50 + 20)}px, ${-Math.random() * 50 - 20}px);
|
|
1255
|
+
opacity: 1;
|
|
1256
|
+
}
|
|
1257
|
+
100% {
|
|
1258
|
+
transform: scale(0) translate(${(i % 2 ? 1 : -1) * (Math.random() * 50 + 20)}px, ${-Math.random() * 50 - 20}px);
|
|
1259
|
+
opacity: 0;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
`).join('')}
|
|
1263
|
+
</style>
|
|
1264
|
+
</>
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
function ParticleButton({
|
|
1268
|
+
children,
|
|
1269
|
+
onClick,
|
|
1270
|
+
onSuccess,
|
|
1271
|
+
successDuration = 1000,
|
|
1272
|
+
particleCount = 6,
|
|
1273
|
+
showIcon = true,
|
|
1274
|
+
icon,
|
|
1275
|
+
variant,
|
|
1276
|
+
size,
|
|
1277
|
+
align,
|
|
1278
|
+
className,
|
|
1279
|
+
disabled,
|
|
1280
|
+
...props
|
|
1281
|
+
}: ParticleButtonProps) {
|
|
1282
|
+
const [showParticles, setShowParticles] = useState(false)
|
|
1283
|
+
const buttonRef = useRef<HTMLButtonElement>(null)
|
|
1284
|
+
|
|
1285
|
+
const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
1286
|
+
if (disabled) return
|
|
1287
|
+
|
|
1288
|
+
setShowParticles(true)
|
|
1289
|
+
onClick?.(e)
|
|
1290
|
+
onSuccess?.()
|
|
1291
|
+
|
|
1292
|
+
setTimeout(() => {
|
|
1293
|
+
setShowParticles(false)
|
|
1294
|
+
}, successDuration)
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const getParticleCount = () => {
|
|
1298
|
+
if (typeof particleCount === 'number') return particleCount
|
|
1299
|
+
switch (particleCount) {
|
|
1300
|
+
case 'low':
|
|
1301
|
+
return 4
|
|
1302
|
+
case 'high':
|
|
1303
|
+
return 10
|
|
1304
|
+
default:
|
|
1305
|
+
return 6
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return (
|
|
1310
|
+
<>
|
|
1311
|
+
{showParticles && (
|
|
1312
|
+
<SuccessParticles
|
|
1313
|
+
buttonRef={buttonRef}
|
|
1314
|
+
particleCount={getParticleCount()}
|
|
1315
|
+
/>
|
|
1316
|
+
)}
|
|
1317
|
+
<Button
|
|
1318
|
+
ref={buttonRef}
|
|
1319
|
+
onClick={handleClick}
|
|
1320
|
+
variant={variant}
|
|
1321
|
+
size={size}
|
|
1322
|
+
align={align}
|
|
1323
|
+
disabled={disabled}
|
|
1324
|
+
className={cn(
|
|
1325
|
+
particleButtonVariants(), "relative transition-transform duration-100",
|
|
1326
|
+
showParticles && "scale-95",
|
|
1327
|
+
className
|
|
1328
|
+
)}
|
|
1329
|
+
{...props}
|
|
1330
|
+
>
|
|
1331
|
+
{children}
|
|
1332
|
+
{showIcon && (icon || <MousePointerClick className="h-4 w-4" />)}
|
|
1333
|
+
</Button>
|
|
1334
|
+
</>
|
|
1335
|
+
)
|
|
1336
|
+
}
|
|
1337
|
+
const RatingContext = createContext<RatingContextValue | null>(null);
|
|
1338
|
+
const useRating = () => {
|
|
1339
|
+
const context = useContext(RatingContext);
|
|
1340
|
+
if (!context) {
|
|
1341
|
+
throw new Error('useRating must be used within a Rating1 component');
|
|
1342
|
+
}
|
|
1343
|
+
return context;
|
|
1344
|
+
};
|
|
1345
|
+
const RatingButton = ({
|
|
1346
|
+
index: providedIndex,
|
|
1347
|
+
size = 20,
|
|
1348
|
+
className,
|
|
1349
|
+
icon = <StarIcon />,
|
|
1350
|
+
}: RatingButtonProps) => {
|
|
1351
|
+
const {
|
|
1352
|
+
value,
|
|
1353
|
+
readOnly,
|
|
1354
|
+
hoverValue,
|
|
1355
|
+
focusedStar,
|
|
1356
|
+
handleValueChange,
|
|
1357
|
+
handleKeyDown,
|
|
1358
|
+
setHoverValue,
|
|
1359
|
+
setFocusedStar,
|
|
1360
|
+
} = useRating();
|
|
1361
|
+
|
|
1362
|
+
const index = providedIndex ?? 0;
|
|
1363
|
+
const isActive = index < (hoverValue ?? focusedStar ?? value ?? 0);
|
|
1364
|
+
let tabIndex = -1;
|
|
1365
|
+
|
|
1366
|
+
if (!readOnly) {
|
|
1367
|
+
tabIndex = value === index + 1 ? 0 : -1;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const handleClick = useCallback(
|
|
1371
|
+
(event: MouseEvent<HTMLButtonElement>) => {
|
|
1372
|
+
handleValueChange(event, index + 1);
|
|
1373
|
+
},
|
|
1374
|
+
[handleValueChange, index]
|
|
1375
|
+
);
|
|
1376
|
+
|
|
1377
|
+
const handleMouseEnter = useCallback(() => {
|
|
1378
|
+
if (!readOnly) {
|
|
1379
|
+
setHoverValue(index + 1);
|
|
1380
|
+
}
|
|
1381
|
+
}, [readOnly, setHoverValue, index]);
|
|
1382
|
+
|
|
1383
|
+
const handleFocus = useCallback(() => {
|
|
1384
|
+
setFocusedStar(index + 1);
|
|
1385
|
+
}, [setFocusedStar, index]);
|
|
1386
|
+
|
|
1387
|
+
const handleBlur = useCallback(() => {
|
|
1388
|
+
setFocusedStar(null);
|
|
1389
|
+
}, [setFocusedStar]);
|
|
1390
|
+
|
|
1391
|
+
return (
|
|
1392
|
+
<button
|
|
1393
|
+
className={cn(
|
|
1394
|
+
'rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
1395
|
+
'p-0.5',
|
|
1396
|
+
readOnly && 'cursor-default',
|
|
1397
|
+
className
|
|
1398
|
+
)}
|
|
1399
|
+
disabled={readOnly}
|
|
1400
|
+
onBlur={handleBlur}
|
|
1401
|
+
onClick={handleClick}
|
|
1402
|
+
onFocus={handleFocus}
|
|
1403
|
+
onKeyDown={handleKeyDown}
|
|
1404
|
+
onMouseEnter={handleMouseEnter}
|
|
1405
|
+
tabIndex={tabIndex}
|
|
1406
|
+
type="button"
|
|
1407
|
+
>
|
|
1408
|
+
{cloneElement(icon, {
|
|
1409
|
+
size,
|
|
1410
|
+
className: cn(
|
|
1411
|
+
'transition-colors duration-200',
|
|
1412
|
+
isActive && 'fill-current',
|
|
1413
|
+
!readOnly && 'cursor-pointer'
|
|
1414
|
+
),
|
|
1415
|
+
'aria-hidden': 'true',
|
|
1416
|
+
})}
|
|
1417
|
+
</button>
|
|
1418
|
+
);
|
|
1419
|
+
};
|
|
1420
|
+
const Rating = ({
|
|
1421
|
+
value: controlledValue,
|
|
1422
|
+
onValueChange: controlledOnValueChange,
|
|
1423
|
+
defaultValue = 0,
|
|
1424
|
+
onChange,
|
|
1425
|
+
readOnly = false,
|
|
1426
|
+
className,
|
|
1427
|
+
children,
|
|
1428
|
+
...props
|
|
1429
|
+
}: RatingProps) => {
|
|
1430
|
+
const [hoverValue, setHoverValue] = useState<number | null>(null);
|
|
1431
|
+
const [focusedStar, setFocusedStar] = useState<number | null>(null);
|
|
1432
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
1433
|
+
const [value, onValueChange] = useControllableState({
|
|
1434
|
+
defaultProp: defaultValue,
|
|
1435
|
+
prop: controlledValue,
|
|
1436
|
+
onChange: controlledOnValueChange,
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
const handleValueChange = useCallback(
|
|
1440
|
+
(
|
|
1441
|
+
event: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>,
|
|
1442
|
+
newValue: number
|
|
1443
|
+
) => {
|
|
1444
|
+
if (!readOnly) {
|
|
1445
|
+
onChange?.(event, newValue);
|
|
1446
|
+
onValueChange?.(newValue);
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
[readOnly, onChange, onValueChange]
|
|
1450
|
+
);
|
|
1451
|
+
|
|
1452
|
+
const handleKeyDown = useCallback(
|
|
1453
|
+
(event: KeyboardEvent<HTMLButtonElement>) => {
|
|
1454
|
+
if (readOnly) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const total = Children.count(children);
|
|
1459
|
+
let newValue = focusedStar !== null ? focusedStar : (value ?? 0);
|
|
1460
|
+
|
|
1461
|
+
switch (event.key) {
|
|
1462
|
+
case 'ArrowRight':
|
|
1463
|
+
if (event.shiftKey || event.metaKey) {
|
|
1464
|
+
newValue = total;
|
|
1465
|
+
} else {
|
|
1466
|
+
newValue = Math.min(total, newValue + 1);
|
|
1467
|
+
}
|
|
1468
|
+
break;
|
|
1469
|
+
case 'ArrowLeft':
|
|
1470
|
+
if (event.shiftKey || event.metaKey) {
|
|
1471
|
+
newValue = 1;
|
|
1472
|
+
} else {
|
|
1473
|
+
newValue = Math.max(1, newValue - 1);
|
|
1474
|
+
}
|
|
1475
|
+
break;
|
|
1476
|
+
default:
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
event.preventDefault();
|
|
1481
|
+
setFocusedStar(newValue);
|
|
1482
|
+
handleValueChange(event, newValue);
|
|
1483
|
+
},
|
|
1484
|
+
[focusedStar, value, children, readOnly, handleValueChange]
|
|
1485
|
+
);
|
|
1486
|
+
|
|
1487
|
+
useEffect(() => {
|
|
1488
|
+
if (focusedStar !== null && containerRef.current) {
|
|
1489
|
+
const buttons = containerRef.current.querySelectorAll('button');
|
|
1490
|
+
buttons[focusedStar - 1]?.focus();
|
|
1491
|
+
}
|
|
1492
|
+
}, [focusedStar]);
|
|
1493
|
+
|
|
1494
|
+
const contextValue: RatingContextValue = {
|
|
1495
|
+
value: value ?? 0,
|
|
1496
|
+
readOnly,
|
|
1497
|
+
hoverValue,
|
|
1498
|
+
focusedStar,
|
|
1499
|
+
handleValueChange,
|
|
1500
|
+
handleKeyDown,
|
|
1501
|
+
setHoverValue,
|
|
1502
|
+
setFocusedStar,
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
return (
|
|
1506
|
+
<RatingContext.Provider value={contextValue}>
|
|
1507
|
+
<div
|
|
1508
|
+
aria-label="Rating1"
|
|
1509
|
+
className={cn('inline-flex items-center gap-0.5', className)}
|
|
1510
|
+
onMouseLeave={() => setHoverValue(null)}
|
|
1511
|
+
ref={containerRef}
|
|
1512
|
+
role="radiogroup"
|
|
1513
|
+
{...props}
|
|
1514
|
+
>
|
|
1515
|
+
{Children.map(children, (child, index) => {
|
|
1516
|
+
if (!child) {
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
return cloneElement(child as ReactElement<RatingButtonProps>, {
|
|
1521
|
+
index,
|
|
1522
|
+
});
|
|
1523
|
+
})}
|
|
1524
|
+
</div>
|
|
1525
|
+
</RatingContext.Provider>
|
|
1526
|
+
);
|
|
1527
|
+
};
|
|
1528
|
+
function RippleButton({
|
|
1529
|
+
ref,
|
|
1530
|
+
children,
|
|
1531
|
+
onClick,
|
|
1532
|
+
className,
|
|
1533
|
+
rippleClassName,
|
|
1534
|
+
variant,
|
|
1535
|
+
size,
|
|
1536
|
+
scale = 10,
|
|
1537
|
+
transition = { duration: 0.6, ease: 'easeOut' },
|
|
1538
|
+
...props
|
|
1539
|
+
}: RippleButtonProps) {
|
|
1540
|
+
const [ripples, setRipples] = React.useState<Ripple[]>([]);
|
|
1541
|
+
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
|
1542
|
+
React.useImperativeHandle(ref, () => buttonRef.current as HTMLButtonElement);
|
|
1543
|
+
|
|
1544
|
+
const createRipple = React.useCallback(
|
|
1545
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
1546
|
+
const button = buttonRef.current;
|
|
1547
|
+
if (!button) return;
|
|
1548
|
+
|
|
1549
|
+
const rect = button.getBoundingClientRect();
|
|
1550
|
+
const x = event.clientX - rect.left;
|
|
1551
|
+
const y = event.clientY - rect.top;
|
|
1552
|
+
|
|
1553
|
+
const newRipple: Ripple = {
|
|
1554
|
+
id: Date.now(),
|
|
1555
|
+
x,
|
|
1556
|
+
y,
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1559
|
+
setRipples((prev) => [...prev, newRipple]);
|
|
1560
|
+
|
|
1561
|
+
setTimeout(() => {
|
|
1562
|
+
setRipples((prev) => prev.filter((r) => r.id !== newRipple.id));
|
|
1563
|
+
}, 600);
|
|
1564
|
+
},
|
|
1565
|
+
[],
|
|
1566
|
+
);
|
|
1567
|
+
|
|
1568
|
+
const handleClick = React.useCallback(
|
|
1569
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
1570
|
+
createRipple(event);
|
|
1571
|
+
if (onClick) {
|
|
1572
|
+
onClick(event);
|
|
1573
|
+
}
|
|
1574
|
+
},
|
|
1575
|
+
[createRipple, onClick],
|
|
1576
|
+
);
|
|
1577
|
+
|
|
1578
|
+
return (
|
|
1579
|
+
<motion.button
|
|
1580
|
+
ref={buttonRef}
|
|
1581
|
+
data-slot="ripple-button"
|
|
1582
|
+
onClick={handleClick}
|
|
1583
|
+
whileTap={{ scale: 0.95 }}
|
|
1584
|
+
whileHover={{ scale: 1.05 }}
|
|
1585
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
1586
|
+
{...props}
|
|
1587
|
+
>
|
|
1588
|
+
{children}
|
|
1589
|
+
{ripples.map((ripple) => (
|
|
1590
|
+
<motion.span
|
|
1591
|
+
key={ripple.id}
|
|
1592
|
+
initial={{ scale: 0, opacity: 0.5 }}
|
|
1593
|
+
animate={{ scale, opacity: 0 }}
|
|
1594
|
+
transition={transition}
|
|
1595
|
+
className={cn(
|
|
1596
|
+
rippleVariants({ variant, className: rippleClassName }),
|
|
1597
|
+
)}
|
|
1598
|
+
style={{
|
|
1599
|
+
top: ripple.y - 10,
|
|
1600
|
+
left: ripple.x - 10,
|
|
1601
|
+
}}
|
|
1602
|
+
/>
|
|
1603
|
+
))}
|
|
1604
|
+
</motion.button>
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
function SocialButton({
|
|
1608
|
+
shareButtons = [
|
|
1609
|
+
{ icon: Twitter, label: "Share on Twitter" },
|
|
1610
|
+
{ icon: Instagram, label: "Share on Instagram" },
|
|
1611
|
+
{ icon: Linkedin, label: "Share on LinkedIn" },
|
|
1612
|
+
{ icon: Link, label: "Copy link" },
|
|
1613
|
+
],
|
|
1614
|
+
label = "Hover me",
|
|
1615
|
+
icon,
|
|
1616
|
+
variant,
|
|
1617
|
+
className,
|
|
1618
|
+
...props
|
|
1619
|
+
}: SocialButtonProps) {
|
|
1620
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
1621
|
+
const [activeIndex, setActiveIndex] = useState<number | null>(null)
|
|
1622
|
+
|
|
1623
|
+
const handleShare = (index: number, onClick?: () => void) => {
|
|
1624
|
+
setActiveIndex(index)
|
|
1625
|
+
onClick?.()
|
|
1626
|
+
setTimeout(() => setActiveIndex(null), 300)
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
const buttonBgClass = variant === "dark"
|
|
1630
|
+
? "bg-black dark:bg-white text-white dark:text-black hover:bg-gray-900 dark:hover:bg-gray-100"
|
|
1631
|
+
: "bg-white dark:bg-black text-black dark:text-white hover:bg-gray-50 dark:hover:bg-gray-950"
|
|
1632
|
+
|
|
1633
|
+
return (
|
|
1634
|
+
<div
|
|
1635
|
+
className="relative"
|
|
1636
|
+
onMouseEnter={() => setIsVisible(true)}
|
|
1637
|
+
onMouseLeave={() => setIsVisible(false)}
|
|
1638
|
+
>
|
|
1639
|
+
<div
|
|
1640
|
+
className="transition-opacity duration-200"
|
|
1641
|
+
style={{ opacity: isVisible ? 0 : 1 }}
|
|
1642
|
+
>
|
|
1643
|
+
<Button
|
|
1644
|
+
className={cn(socialButtonVariants({ variant }), className)}
|
|
1645
|
+
asChild={false}
|
|
1646
|
+
{...props}
|
|
1647
|
+
>
|
|
1648
|
+
<span className="flex items-center gap-2">
|
|
1649
|
+
{icon || <Link className="w-4 h-4" />}
|
|
1650
|
+
{label}
|
|
1651
|
+
</span>
|
|
1652
|
+
</Button>
|
|
1653
|
+
</div>
|
|
1654
|
+
|
|
1655
|
+
<div
|
|
1656
|
+
className="absolute top-0 left-0 flex h-10 overflow-hidden transition-all duration-300"
|
|
1657
|
+
style={{
|
|
1658
|
+
width: isVisible ? `${shareButtons.length * 40}px` : '0px',
|
|
1659
|
+
}}
|
|
1660
|
+
>
|
|
1661
|
+
{shareButtons.map((button, i) => (
|
|
1662
|
+
<button
|
|
1663
|
+
type="button"
|
|
1664
|
+
key={`share-${button.label}`}
|
|
1665
|
+
aria-label={button.label}
|
|
1666
|
+
onClick={() => handleShare(i, button.onClick)}
|
|
1667
|
+
className={cn(
|
|
1668
|
+
"h-10 w-10 flex items-center justify-center",
|
|
1669
|
+
buttonBgClass,
|
|
1670
|
+
i === 0 && "rounded-l-md",
|
|
1671
|
+
i === shareButtons.length - 1 && "rounded-r-md",
|
|
1672
|
+
"border-r border-white/10 dark:border-black/10 last:border-r-0",
|
|
1673
|
+
"outline-none relative overflow-hidden transition-all duration-200"
|
|
1674
|
+
)}
|
|
1675
|
+
style={{
|
|
1676
|
+
opacity: isVisible ? 1 : 0,
|
|
1677
|
+
transform: isVisible ? 'translateX(0)' : 'translateX(-20px)',
|
|
1678
|
+
transitionDelay: isVisible ? `${i * 50}ms` : '0ms',
|
|
1679
|
+
}}
|
|
1680
|
+
>
|
|
1681
|
+
<div
|
|
1682
|
+
className="relative z-10 transition-transform duration-200"
|
|
1683
|
+
style={{
|
|
1684
|
+
transform: activeIndex === i ? 'scale(0.85)' : 'scale(1)',
|
|
1685
|
+
}}
|
|
1686
|
+
>
|
|
1687
|
+
<button.icon className="w-4 h-4" />
|
|
1688
|
+
</div>
|
|
1689
|
+
<div
|
|
1690
|
+
className="absolute inset-0 bg-white dark:bg-black transition-opacity duration-200"
|
|
1691
|
+
style={{
|
|
1692
|
+
opacity: activeIndex === i ? 0.15 : 0,
|
|
1693
|
+
}}
|
|
1694
|
+
/>
|
|
1695
|
+
</button>
|
|
1696
|
+
))}
|
|
1697
|
+
</div>
|
|
1698
|
+
</div>
|
|
1699
|
+
)
|
|
1700
|
+
}
|
|
1701
|
+
function SwitchButton({
|
|
1702
|
+
checked = false,
|
|
1703
|
+
onCheckedChange,
|
|
1704
|
+
className,
|
|
1705
|
+
variant = 'default',
|
|
1706
|
+
size = 'md',
|
|
1707
|
+
disabled = false,
|
|
1708
|
+
labels
|
|
1709
|
+
}: SwitchButtonProps) {
|
|
1710
|
+
const [isChecked, setIsChecked] = useState(checked);
|
|
1711
|
+
|
|
1712
|
+
const handleToggle = () => {
|
|
1713
|
+
if (disabled) return;
|
|
1714
|
+
|
|
1715
|
+
const newChecked = !isChecked;
|
|
1716
|
+
setIsChecked(newChecked);
|
|
1717
|
+
onCheckedChange?.(newChecked);
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
const sizeClasses = {
|
|
1721
|
+
sm: 'w-16 h-8',
|
|
1722
|
+
md: 'w-20 h-10',
|
|
1723
|
+
lg: 'w-24 h-12'
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
const knobSizeClasses = {
|
|
1727
|
+
sm: 'w-6 h-6',
|
|
1728
|
+
md: 'w-8 h-8',
|
|
1729
|
+
lg: 'w-10 h-10'
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
const variantClasses = {
|
|
1733
|
+
default: {
|
|
1734
|
+
background: isChecked ? 'bg-muted-foreground' : 'bg-muted',
|
|
1735
|
+
knob: 'bg-background',
|
|
1736
|
+
text: 'text-background'
|
|
1737
|
+
},
|
|
1738
|
+
primary: {
|
|
1739
|
+
background: isChecked ? 'bg-primary' : 'bg-muted',
|
|
1740
|
+
knob: 'bg-background',
|
|
1741
|
+
text: 'text-primary-foreground'
|
|
1742
|
+
},
|
|
1743
|
+
secondary: {
|
|
1744
|
+
background: isChecked ? 'bg-secondary' : 'bg-muted',
|
|
1745
|
+
knob: 'bg-background',
|
|
1746
|
+
text: 'text-secondary-foreground'
|
|
1747
|
+
},
|
|
1748
|
+
destructive: {
|
|
1749
|
+
background: isChecked ? 'bg-destructive' : 'bg-muted',
|
|
1750
|
+
knob: 'bg-background',
|
|
1751
|
+
text: 'text-destructive-foreground'
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1755
|
+
const variantConfig = variantClasses[variant];
|
|
1756
|
+
|
|
1757
|
+
return (
|
|
1758
|
+
<button
|
|
1759
|
+
type="button"
|
|
1760
|
+
role="switch"
|
|
1761
|
+
aria-checked={isChecked}
|
|
1762
|
+
onClick={handleToggle}
|
|
1763
|
+
disabled={disabled}
|
|
1764
|
+
className={cn(
|
|
1765
|
+
'relative inline-flex items-center rounded-full border-2 border-transparent transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
1766
|
+
sizeClasses[size],
|
|
1767
|
+
variantConfig.background,
|
|
1768
|
+
className
|
|
1769
|
+
)}
|
|
1770
|
+
>
|
|
1771
|
+
<span
|
|
1772
|
+
className={cn(
|
|
1773
|
+
'inline-block transform rounded-full shadow-lg transition-all duration-200',
|
|
1774
|
+
knobSizeClasses[size],
|
|
1775
|
+
variantConfig.knob,
|
|
1776
|
+
isChecked
|
|
1777
|
+
? size === 'sm' ? 'translate-x-8' :
|
|
1778
|
+
size === 'md' ? 'translate-x-10' : 'translate-x-12'
|
|
1779
|
+
: 'translate-x-1'
|
|
1780
|
+
)}
|
|
1781
|
+
/>
|
|
1782
|
+
|
|
1783
|
+
{labels && (
|
|
1784
|
+
<>
|
|
1785
|
+
<span
|
|
1786
|
+
className={cn(
|
|
1787
|
+
'absolute left-1 text-xs font-medium transition-opacity duration-200',
|
|
1788
|
+
variantConfig.text,
|
|
1789
|
+
!isChecked ? 'opacity-100' : 'opacity-0'
|
|
1790
|
+
)}
|
|
1791
|
+
>
|
|
1792
|
+
{labels.unchecked}
|
|
1793
|
+
</span>
|
|
1794
|
+
<span
|
|
1795
|
+
className={cn(
|
|
1796
|
+
'absolute right-1 text-xs font-medium transition-opacity duration-200',
|
|
1797
|
+
variantConfig.text,
|
|
1798
|
+
isChecked ? 'opacity-100' : 'opacity-0'
|
|
1799
|
+
)}
|
|
1800
|
+
>
|
|
1801
|
+
{labels.checked}
|
|
1802
|
+
</span>
|
|
1803
|
+
</>
|
|
1804
|
+
)}
|
|
1805
|
+
</button>
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
const SplitButton = ({
|
|
1809
|
+
label = null,
|
|
1810
|
+
icon,
|
|
1811
|
+
items,
|
|
1812
|
+
onClick,
|
|
1813
|
+
className,
|
|
1814
|
+
containerClassName,
|
|
1815
|
+
variant = "default",
|
|
1816
|
+
align = "default",
|
|
1817
|
+
size = "sm",
|
|
1818
|
+
children,
|
|
1819
|
+
...props
|
|
1820
|
+
}: SplitButtonProps) => {
|
|
1821
|
+
return (
|
|
1822
|
+
<div className={cn("inline-flex", containerClassName)}>
|
|
1823
|
+
<Button
|
|
1824
|
+
onClick={onClick}
|
|
1825
|
+
className={cn("rounded-r-none border-r-0", buttonVariants({ variant, size, className, align }))}
|
|
1826
|
+
{...props}
|
|
1827
|
+
>
|
|
1828
|
+
{children}
|
|
1829
|
+
</Button>
|
|
1830
|
+
<DropdownMenu>
|
|
1831
|
+
<DropdownMenuTrigger asChild>
|
|
1832
|
+
<Button className={cn("rounded-l-none px-2", buttonVariants({ variant, size, className }))} >
|
|
1833
|
+
{label !== null ? (
|
|
1834
|
+
<>
|
|
1835
|
+
{label}
|
|
1836
|
+
</>
|
|
1837
|
+
) : (
|
|
1838
|
+
<ChevronDown className="h-4 w-4" />
|
|
1839
|
+
)}
|
|
1840
|
+
</Button>
|
|
1841
|
+
</DropdownMenuTrigger>
|
|
1842
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
1843
|
+
{items.map((item, index) => (
|
|
1844
|
+
<React.Fragment key={index}>
|
|
1845
|
+
{item.separator && <DropdownMenuSeparator />}
|
|
1846
|
+
<DropdownMenuItem
|
|
1847
|
+
onClick={item.command}
|
|
1848
|
+
className="cursor-pointer"
|
|
1849
|
+
|
|
1850
|
+
>
|
|
1851
|
+
{item.icon && <span className="mr-2">{item.icon}</span>}
|
|
1852
|
+
{item.label}
|
|
1853
|
+
</DropdownMenuItem>
|
|
1854
|
+
</React.Fragment>
|
|
1855
|
+
))}
|
|
1856
|
+
</DropdownMenuContent>
|
|
1857
|
+
</DropdownMenu>
|
|
1858
|
+
</div>
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
function ToggleButton({
|
|
1862
|
+
checked = false,
|
|
1863
|
+
onChange,
|
|
1864
|
+
disabled = false,
|
|
1865
|
+
className,
|
|
1866
|
+
variant = "default",
|
|
1867
|
+
size = "md",
|
|
1868
|
+
children,
|
|
1869
|
+
...props
|
|
1870
|
+
}: ButtonToggleProps) {
|
|
1871
|
+
const handleClick = () => {
|
|
1872
|
+
if (!disabled && onChange) {
|
|
1873
|
+
onChange(!checked)
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
1878
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1879
|
+
e.preventDefault()
|
|
1880
|
+
handleClick()
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
return (
|
|
1885
|
+
<button
|
|
1886
|
+
type="button"
|
|
1887
|
+
className={cn(
|
|
1888
|
+
toggleButtonVariants({
|
|
1889
|
+
variant,
|
|
1890
|
+
size,
|
|
1891
|
+
checked: checked ? "true" : "false"
|
|
1892
|
+
}),
|
|
1893
|
+
className
|
|
1894
|
+
)}
|
|
1895
|
+
onClick={handleClick}
|
|
1896
|
+
onKeyDown={handleKeyDown}
|
|
1897
|
+
disabled={disabled}
|
|
1898
|
+
role="switch"
|
|
1899
|
+
aria-checked={checked}
|
|
1900
|
+
aria-disabled={disabled}
|
|
1901
|
+
{...props}
|
|
1902
|
+
>
|
|
1903
|
+
{children}
|
|
1904
|
+
</button>
|
|
1905
|
+
)
|
|
1906
|
+
}
|
|
1907
|
+
SplitButton.displayName = "SplitButton"
|
|
1908
|
+
function SlideButton({
|
|
1909
|
+
className,
|
|
1910
|
+
variant,
|
|
1911
|
+
size,
|
|
1912
|
+
text = "Browse Components",
|
|
1913
|
+
hoverText,
|
|
1914
|
+
to = "/",
|
|
1915
|
+
prefetch = "intent",
|
|
1916
|
+
...props
|
|
1917
|
+
}: Omit<React.ComponentProps<typeof Link>, 'to'> &
|
|
1918
|
+
VariantProps<typeof slideTextButtonVariants> & {
|
|
1919
|
+
text?: string
|
|
1920
|
+
hoverText?: string
|
|
1921
|
+
to?: string
|
|
1922
|
+
}) {
|
|
1923
|
+
const slideText = hoverText ?? text
|
|
1924
|
+
|
|
1925
|
+
return (
|
|
1926
|
+
<Link
|
|
1927
|
+
data-slot="slide-text-button"
|
|
1928
|
+
className={cn(slideTextButtonVariants({ variant, size, className }))}
|
|
1929
|
+
to={to}
|
|
1930
|
+
prefetch={prefetch}
|
|
1931
|
+
{...props}
|
|
1932
|
+
>
|
|
1933
|
+
<span className="group-hover:-translate-y-full relative inline-block transition-transform duration-300 ease-in-out">
|
|
1934
|
+
<span className="flex items-center gap-2 opacity-100 transition-opacity duration-300 group-hover:opacity-0">
|
|
1935
|
+
<span className="font-medium">{text}</span>
|
|
1936
|
+
</span>
|
|
1937
|
+
<span className="absolute top-full left-0 flex items-center gap-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
|
1938
|
+
<span className="font-medium">{slideText}</span>
|
|
1939
|
+
</span>
|
|
1940
|
+
</span>
|
|
1941
|
+
</Link>
|
|
1942
|
+
)
|
|
1943
|
+
}
|
|
1944
|
+
const TextRevealButton = React.forwardRef<
|
|
1945
|
+
HTMLButtonElement,
|
|
1946
|
+
TextRevealButtonProps
|
|
1947
|
+
>(({
|
|
1948
|
+
text = "shadcn.io",
|
|
1949
|
+
revealColor = "#37FF8B",
|
|
1950
|
+
strokeColor = "rgba(100, 100, 100, 0.7)",
|
|
1951
|
+
className,
|
|
1952
|
+
style,
|
|
1953
|
+
...props
|
|
1954
|
+
}, ref) => {
|
|
1955
|
+
return (
|
|
1956
|
+
<button
|
|
1957
|
+
ref={ref}
|
|
1958
|
+
className={cn(
|
|
1959
|
+
"group relative cursor-pointer bg-transparent border-none p-0 m-0 h-auto font-['Arial'] uppercase",
|
|
1960
|
+
className
|
|
1961
|
+
)}
|
|
1962
|
+
style={{
|
|
1963
|
+
fontSize: '2em',
|
|
1964
|
+
letterSpacing: '3px',
|
|
1965
|
+
color: 'transparent',
|
|
1966
|
+
WebkitTextStroke: `1px ${strokeColor}`,
|
|
1967
|
+
...style,
|
|
1968
|
+
}}
|
|
1969
|
+
{...props}
|
|
1970
|
+
>
|
|
1971
|
+
<span>
|
|
1972
|
+
{text}
|
|
1973
|
+
</span>
|
|
1974
|
+
<span
|
|
1975
|
+
aria-hidden="true"
|
|
1976
|
+
className="absolute inset-0 w-0 overflow-hidden transition-all duration-500 group-hover:w-full group-hover:[filter:drop-shadow(0_0_23px_#37FF8B)]"
|
|
1977
|
+
style={{
|
|
1978
|
+
color: revealColor,
|
|
1979
|
+
borderRight: `6px solid ${revealColor}`,
|
|
1980
|
+
WebkitTextStroke: `1px ${revealColor}`,
|
|
1981
|
+
}}
|
|
1982
|
+
>
|
|
1983
|
+
{text}
|
|
1984
|
+
</span>
|
|
1985
|
+
</button>
|
|
1986
|
+
);
|
|
1987
|
+
});
|
|
1988
|
+
TextRevealButton.displayName = "TextRevealButton";
|
|
1989
|
+
const ShimmerButton = React.forwardRef<
|
|
1990
|
+
HTMLButtonElement,
|
|
1991
|
+
ShimmerButtonProps
|
|
1992
|
+
>(
|
|
1993
|
+
(
|
|
1994
|
+
{
|
|
1995
|
+
shimmerColor = "#ffffff",
|
|
1996
|
+
shimmerSize = "0.05em",
|
|
1997
|
+
shimmerDuration = "3s",
|
|
1998
|
+
borderRadius = "100px",
|
|
1999
|
+
background = "rgba(0, 0, 0, 1)",
|
|
2000
|
+
className,
|
|
2001
|
+
children,
|
|
2002
|
+
...props
|
|
2003
|
+
},
|
|
2004
|
+
ref
|
|
2005
|
+
) => {
|
|
2006
|
+
return (
|
|
2007
|
+
<button
|
|
2008
|
+
style={
|
|
2009
|
+
{
|
|
2010
|
+
"--spread": "90deg",
|
|
2011
|
+
"--shimmer-color": shimmerColor,
|
|
2012
|
+
"--radius": borderRadius,
|
|
2013
|
+
"--speed": shimmerDuration,
|
|
2014
|
+
"--cut": shimmerSize,
|
|
2015
|
+
"--bg": background,
|
|
2016
|
+
} as CSSProperties
|
|
2017
|
+
}
|
|
2018
|
+
className={cn(
|
|
2019
|
+
"group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden [border-radius:var(--radius)] border border-white/10 px-6 py-3 whitespace-nowrap text-white [background:var(--bg)]",
|
|
2020
|
+
"transform-gpu transition-transform duration-300 ease-in-out active:translate-y-px",
|
|
2021
|
+
className
|
|
2022
|
+
)}
|
|
2023
|
+
ref={ref}
|
|
2024
|
+
{...props}
|
|
2025
|
+
>
|
|
2026
|
+
<div
|
|
2027
|
+
className={cn(
|
|
2028
|
+
"-z-30 blur-[2px]",
|
|
2029
|
+
"[container-type:size] absolute inset-0 overflow-visible"
|
|
2030
|
+
)}
|
|
2031
|
+
>
|
|
2032
|
+
<div className="animate-shimmer-slide absolute inset-0 [aspect-ratio:1] h-[100cqh] [border-radius:0] [mask:none]">
|
|
2033
|
+
<div className="animate-spin-around absolute -inset-full w-auto [translate:0_0] rotate-0 [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))]" />
|
|
2034
|
+
</div>
|
|
2035
|
+
</div>
|
|
2036
|
+
{children}
|
|
2037
|
+
<div
|
|
2038
|
+
className={cn(
|
|
2039
|
+
"absolute inset-0 size-full",
|
|
2040
|
+
"rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#ffffff1f]",
|
|
2041
|
+
"transform-gpu transition-all duration-300 ease-in-out",
|
|
2042
|
+
"group-hover:shadow-[inset_0_-6px_10px_#ffffff3f]",
|
|
2043
|
+
"group-active:shadow-[inset_0_-10px_10px_#ffffff3f]"
|
|
2044
|
+
)}
|
|
2045
|
+
/>
|
|
2046
|
+
<div
|
|
2047
|
+
className={cn(
|
|
2048
|
+
"absolute [inset:var(--cut)] -z-20 [border-radius:var(--radius)] [background:var(--bg)]"
|
|
2049
|
+
)}
|
|
2050
|
+
/>
|
|
2051
|
+
</button>
|
|
2052
|
+
)
|
|
2053
|
+
}
|
|
2054
|
+
)
|
|
2055
|
+
function TooltipButton({ icon, content, onClick = null, size = 'icon', variant = 'ghost', className }) {
|
|
2056
|
+
return (
|
|
2057
|
+
<TooltipProvider>
|
|
2058
|
+
<Tooltip>
|
|
2059
|
+
<TooltipTrigger asChild>
|
|
2060
|
+
<Button
|
|
2061
|
+
size={size}
|
|
2062
|
+
variant={variant}
|
|
2063
|
+
className={cn('', className)}
|
|
2064
|
+
onClick={onClick}
|
|
2065
|
+
>
|
|
2066
|
+
{icon}
|
|
2067
|
+
</Button>
|
|
2068
|
+
</TooltipTrigger>
|
|
2069
|
+
<TooltipContent>
|
|
2070
|
+
<p>{content}</p>
|
|
2071
|
+
</TooltipContent>
|
|
2072
|
+
</Tooltip>
|
|
2073
|
+
</TooltipProvider>
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
const RatingContext1 = createContext<RatingContextValue | null>(null);
|
|
2077
|
+
const useRating1 = () => {
|
|
2078
|
+
const context = useContext(RatingContext1);
|
|
2079
|
+
if (!context) {
|
|
2080
|
+
throw new Error('useRating1 must be used within a Rate component');
|
|
2081
|
+
}
|
|
2082
|
+
return context;
|
|
2083
|
+
};
|
|
2084
|
+
const RatingButton1 = ({
|
|
2085
|
+
index: providedIndex,
|
|
2086
|
+
size = 20,
|
|
2087
|
+
className,
|
|
2088
|
+
icon = <StarIcon />,
|
|
2089
|
+
}: RatingButtonProps) => {
|
|
2090
|
+
const {
|
|
2091
|
+
value,
|
|
2092
|
+
readOnly,
|
|
2093
|
+
hoverValue,
|
|
2094
|
+
focusedStar,
|
|
2095
|
+
handleValueChange,
|
|
2096
|
+
handleKeyDown,
|
|
2097
|
+
setHoverValue,
|
|
2098
|
+
setFocusedStar,
|
|
2099
|
+
} = useRating1();
|
|
2100
|
+
const index = providedIndex ?? 0;
|
|
2101
|
+
const isActive = index < (hoverValue ?? focusedStar ?? value ?? 0);
|
|
2102
|
+
let tabIndex = -1;
|
|
2103
|
+
if (!readOnly) {
|
|
2104
|
+
tabIndex = value === index + 1 ? 0 : -1;
|
|
2105
|
+
}
|
|
2106
|
+
const handleClick = useCallback(
|
|
2107
|
+
(event: MouseEvent<HTMLButtonElement>) => {
|
|
2108
|
+
handleValueChange(event, index + 1);
|
|
2109
|
+
},
|
|
2110
|
+
[handleValueChange, index]
|
|
2111
|
+
);
|
|
2112
|
+
const handleMouseEnter = useCallback(() => {
|
|
2113
|
+
if (!readOnly) {
|
|
2114
|
+
setHoverValue(index + 1);
|
|
2115
|
+
}
|
|
2116
|
+
}, [readOnly, setHoverValue, index]);
|
|
2117
|
+
const handleFocus = useCallback(() => {
|
|
2118
|
+
setFocusedStar(index + 1);
|
|
2119
|
+
}, [setFocusedStar, index]);
|
|
2120
|
+
const handleBlur = useCallback(() => {
|
|
2121
|
+
setFocusedStar(null);
|
|
2122
|
+
}, [setFocusedStar]);
|
|
2123
|
+
return (
|
|
2124
|
+
<button
|
|
2125
|
+
className={cn(
|
|
2126
|
+
'rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
2127
|
+
'p-0.5',
|
|
2128
|
+
readOnly && 'cursor-default',
|
|
2129
|
+
className
|
|
2130
|
+
)}
|
|
2131
|
+
disabled={readOnly}
|
|
2132
|
+
onBlur={handleBlur}
|
|
2133
|
+
onClick={handleClick}
|
|
2134
|
+
onFocus={handleFocus}
|
|
2135
|
+
onKeyDown={handleKeyDown}
|
|
2136
|
+
onMouseEnter={handleMouseEnter}
|
|
2137
|
+
tabIndex={tabIndex}
|
|
2138
|
+
type="button"
|
|
2139
|
+
>
|
|
2140
|
+
{cloneElement(icon, {
|
|
2141
|
+
size,
|
|
2142
|
+
className: cn(
|
|
2143
|
+
'transition-colors duration-200',
|
|
2144
|
+
isActive && 'fill-current',
|
|
2145
|
+
!readOnly && 'cursor-pointer'
|
|
2146
|
+
),
|
|
2147
|
+
'aria-hidden': 'true',
|
|
2148
|
+
})}
|
|
2149
|
+
</button>
|
|
2150
|
+
);
|
|
2151
|
+
};
|
|
2152
|
+
const Rate1 = ({
|
|
2153
|
+
value: controlledValue,
|
|
2154
|
+
onValueChange: controlledOnValueChange,
|
|
2155
|
+
defaultValue = 0,
|
|
2156
|
+
onChange,
|
|
2157
|
+
readOnly = false,
|
|
2158
|
+
className,
|
|
2159
|
+
children,
|
|
2160
|
+
...props
|
|
2161
|
+
}: RatingProps) => {
|
|
2162
|
+
const [hoverValue, setHoverValue] = useState<number | null>(null);
|
|
2163
|
+
const [focusedStar, setFocusedStar] = useState<number | null>(null);
|
|
2164
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
2165
|
+
const [value, onValueChange] = useControllableState({
|
|
2166
|
+
defaultProp: defaultValue,
|
|
2167
|
+
prop: controlledValue,
|
|
2168
|
+
onChange: controlledOnValueChange,
|
|
2169
|
+
});
|
|
2170
|
+
const handleValueChange = useCallback(
|
|
2171
|
+
(
|
|
2172
|
+
event: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>,
|
|
2173
|
+
newValue: number
|
|
2174
|
+
) => {
|
|
2175
|
+
if (!readOnly) {
|
|
2176
|
+
onChange?.(event, newValue);
|
|
2177
|
+
onValueChange?.(newValue);
|
|
2178
|
+
}
|
|
2179
|
+
},
|
|
2180
|
+
[readOnly, onChange, onValueChange]
|
|
2181
|
+
);
|
|
2182
|
+
const handleKeyDown = useCallback(
|
|
2183
|
+
(event: KeyboardEvent<HTMLButtonElement>) => {
|
|
2184
|
+
if (readOnly) {
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
const total = Children.count(children);
|
|
2188
|
+
let newValue = focusedStar !== null ? focusedStar : (value ?? 0);
|
|
2189
|
+
switch (event.key) {
|
|
2190
|
+
case 'ArrowRight':
|
|
2191
|
+
if (event.shiftKey || event.metaKey) {
|
|
2192
|
+
newValue = total;
|
|
2193
|
+
} else {
|
|
2194
|
+
newValue = Math.min(total, newValue + 1);
|
|
2195
|
+
}
|
|
2196
|
+
break;
|
|
2197
|
+
case 'ArrowLeft':
|
|
2198
|
+
if (event.shiftKey || event.metaKey) {
|
|
2199
|
+
newValue = 1;
|
|
2200
|
+
} else {
|
|
2201
|
+
newValue = Math.max(1, newValue - 1);
|
|
2202
|
+
}
|
|
2203
|
+
break;
|
|
2204
|
+
default:
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
event.preventDefault();
|
|
2208
|
+
setFocusedStar(newValue);
|
|
2209
|
+
handleValueChange(event, newValue);
|
|
2210
|
+
},
|
|
2211
|
+
[focusedStar, value, children, readOnly, handleValueChange]
|
|
2212
|
+
);
|
|
2213
|
+
useEffect(() => {
|
|
2214
|
+
if (focusedStar !== null && containerRef.current) {
|
|
2215
|
+
const buttons = containerRef.current.querySelectorAll('button');
|
|
2216
|
+
buttons[focusedStar - 1]?.focus();
|
|
2217
|
+
}
|
|
2218
|
+
}, [focusedStar]);
|
|
2219
|
+
const contextValue: RatingContextValue = {
|
|
2220
|
+
value: value ?? 0,
|
|
2221
|
+
readOnly,
|
|
2222
|
+
hoverValue,
|
|
2223
|
+
focusedStar,
|
|
2224
|
+
handleValueChange,
|
|
2225
|
+
handleKeyDown,
|
|
2226
|
+
setHoverValue,
|
|
2227
|
+
setFocusedStar,
|
|
2228
|
+
};
|
|
2229
|
+
return (
|
|
2230
|
+
<RatingContext1.Provider value={contextValue}>
|
|
2231
|
+
<div
|
|
2232
|
+
aria-label="Rate"
|
|
2233
|
+
className={cn('inline-flex items-center gap-0.5', className)}
|
|
2234
|
+
onMouseLeave={() => setHoverValue(null)}
|
|
2235
|
+
ref={containerRef}
|
|
2236
|
+
role="radiogroup"
|
|
2237
|
+
{...props}
|
|
2238
|
+
>
|
|
2239
|
+
{Children.map(children, (child, index) => {
|
|
2240
|
+
if (!child) {
|
|
2241
|
+
return null;
|
|
2242
|
+
}
|
|
2243
|
+
return cloneElement(child as ReactElement<RatingButtonProps>, {
|
|
2244
|
+
index,
|
|
2245
|
+
});
|
|
2246
|
+
})}
|
|
2247
|
+
</div>
|
|
2248
|
+
</RatingContext1.Provider>
|
|
2249
|
+
);
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
export function ButtonXDemo() {
|
|
2253
|
+
const [clicked, setClicked] = useState("")
|
|
2254
|
+
const [rating, setRating] = useState(0)
|
|
2255
|
+
const [switchChecked, setSwitchChecked] = useState(false)
|
|
2256
|
+
const [toggleChecked, setToggleChecked] = useState(false)
|
|
2257
|
+
|
|
2258
|
+
return (
|
|
2259
|
+
<div className="flex flex-col space-y-8 p-6 max-w-6xl mx-auto">
|
|
2260
|
+
<div>
|
|
2261
|
+
<h2 className="text-2xl font-bold mb-4">Button Variants</h2>
|
|
2262
|
+
<div className="flex gap-4 flex-wrap items-center">
|
|
2263
|
+
<Button variant="default">Default</Button>
|
|
2264
|
+
<Button variant="destructive">Destructive</Button>
|
|
2265
|
+
<Button variant="outline">Outline</Button>
|
|
2266
|
+
<Button variant="secondary">Secondary</Button>
|
|
2267
|
+
<Button variant="ghost">Ghost</Button>
|
|
2268
|
+
<Button variant="link">Link</Button>
|
|
2269
|
+
</div>
|
|
2270
|
+
</div>
|
|
2271
|
+
|
|
2272
|
+
<div>
|
|
2273
|
+
<h2 className="text-2xl font-bold mb-4">Command Button Variants</h2>
|
|
2274
|
+
<div className="flex gap-4 flex-wrap items-center">
|
|
2275
|
+
<CommandButton
|
|
2276
|
+
variant="default"
|
|
2277
|
+
onClick={() => setClicked("Default clicked")}
|
|
2278
|
+
>
|
|
2279
|
+
CMD + K
|
|
2280
|
+
</CommandButton>
|
|
2281
|
+
<CommandButton
|
|
2282
|
+
variant="primary"
|
|
2283
|
+
onClick={() => setClicked("Primary clicked")}
|
|
2284
|
+
>
|
|
2285
|
+
CMD + P
|
|
2286
|
+
</CommandButton>
|
|
2287
|
+
<CommandButton
|
|
2288
|
+
variant="secondary"
|
|
2289
|
+
onClick={() => setClicked("Secondary clicked")}
|
|
2290
|
+
>
|
|
2291
|
+
CMD + S
|
|
2292
|
+
</CommandButton>
|
|
2293
|
+
</div>
|
|
2294
|
+
{clicked && (
|
|
2295
|
+
<p className="mt-4 text-sm text-muted-foreground">{clicked}</p>
|
|
2296
|
+
)}
|
|
2297
|
+
</div>
|
|
2298
|
+
|
|
2299
|
+
<div>
|
|
2300
|
+
<h2 className="text-2xl font-bold mb-4">Attract Button</h2>
|
|
2301
|
+
<div className="flex justify-center gap-4">
|
|
2302
|
+
<AttractButton attractVariant="default" />
|
|
2303
|
+
<AttractButton attractVariant="violet" />
|
|
2304
|
+
<AttractButton attractVariant="blue" />
|
|
2305
|
+
<AttractButton attractVariant="green" />
|
|
2306
|
+
</div>
|
|
2307
|
+
</div>
|
|
2308
|
+
|
|
2309
|
+
<div>
|
|
2310
|
+
<h2 className="text-2xl font-bold mb-4">Copy Button</h2>
|
|
2311
|
+
<div className="flex justify-center gap-4">
|
|
2312
|
+
<CopyButton content="Hello World!" />
|
|
2313
|
+
<CopyButton content="Copy this text" variant="outline" />
|
|
2314
|
+
</div>
|
|
2315
|
+
</div>
|
|
2316
|
+
|
|
2317
|
+
<div>
|
|
2318
|
+
<h2 className="text-2xl font-bold mb-4">Flip Button</h2>
|
|
2319
|
+
<div className="flex justify-center gap-4">
|
|
2320
|
+
<FlipButton frontText="Hover Me" backText="Flipped!" />
|
|
2321
|
+
<FlipButton frontText="Left Flip" backText="Success!" from="left" />
|
|
2322
|
+
<FlipButton frontText="Right Flip" backText="Nice!" from="right" />
|
|
2323
|
+
</div>
|
|
2324
|
+
</div>
|
|
2325
|
+
|
|
2326
|
+
<div>
|
|
2327
|
+
<h2 className="text-2xl font-bold mb-4">Float Button</h2>
|
|
2328
|
+
<FloatButton
|
|
2329
|
+
icon={<MousePointerClick className="h-4 w-4" />}
|
|
2330
|
+
tooltip="Click me!"
|
|
2331
|
+
onClick={() => alert("Float button clicked!")}
|
|
2332
|
+
/>
|
|
2333
|
+
</div>
|
|
2334
|
+
|
|
2335
|
+
<div>
|
|
2336
|
+
<h2 className="text-2xl font-bold mb-4">Hold Button</h2>
|
|
2337
|
+
<div className="flex justify-center gap-4">
|
|
2338
|
+
<HoldButton
|
|
2339
|
+
holdVariant="red"
|
|
2340
|
+
onComplete={() => alert("Hold completed!")}
|
|
2341
|
+
/>
|
|
2342
|
+
<HoldButton
|
|
2343
|
+
holdVariant="green"
|
|
2344
|
+
holdText="Hold to Archive"
|
|
2345
|
+
onComplete={() => alert("Archived!")}
|
|
2346
|
+
/>
|
|
2347
|
+
</div>
|
|
2348
|
+
</div>
|
|
2349
|
+
|
|
2350
|
+
<div>
|
|
2351
|
+
<h2 className="text-2xl font-bold mb-4">Input Button</h2>
|
|
2352
|
+
<div className="flex justify-center">
|
|
2353
|
+
<InputButtonProvider>
|
|
2354
|
+
<InputButton>
|
|
2355
|
+
<InputButtonAction>Enter your email</InputButtonAction>
|
|
2356
|
+
<InputButtonInput placeholder="email@example.com" />
|
|
2357
|
+
<InputButtonSubmit>Submit</InputButtonSubmit>
|
|
2358
|
+
</InputButton>
|
|
2359
|
+
</InputButtonProvider>
|
|
2360
|
+
</div>
|
|
2361
|
+
</div>
|
|
2362
|
+
|
|
2363
|
+
<div>
|
|
2364
|
+
<h2 className="text-2xl font-bold mb-4">Liquid Button</h2>
|
|
2365
|
+
<div className="flex justify-center gap-4">
|
|
2366
|
+
<LiquidButton>Hover Me</LiquidButton>
|
|
2367
|
+
<LiquidButton variant="outline">Liquid Effect</LiquidButton>
|
|
2368
|
+
</div>
|
|
2369
|
+
</div>
|
|
2370
|
+
|
|
2371
|
+
<div>
|
|
2372
|
+
<h2 className="text-2xl font-bold mb-4">Particle Button</h2>
|
|
2373
|
+
<div className="flex justify-center gap-4">
|
|
2374
|
+
<ParticleButton
|
|
2375
|
+
onSuccess={() => console.log("Success!")}
|
|
2376
|
+
particleCount="low"
|
|
2377
|
+
>
|
|
2378
|
+
Low Particles
|
|
2379
|
+
</ParticleButton>
|
|
2380
|
+
<ParticleButton
|
|
2381
|
+
onSuccess={() => console.log("Success!")}
|
|
2382
|
+
particleCount={6}
|
|
2383
|
+
>
|
|
2384
|
+
Medium Particles
|
|
2385
|
+
</ParticleButton>
|
|
2386
|
+
<ParticleButton
|
|
2387
|
+
onSuccess={() => console.log("Success!")}
|
|
2388
|
+
particleCount="high"
|
|
2389
|
+
>
|
|
2390
|
+
High Particles
|
|
2391
|
+
</ParticleButton>
|
|
2392
|
+
</div>
|
|
2393
|
+
</div>
|
|
2394
|
+
|
|
2395
|
+
<div>
|
|
2396
|
+
<h2 className="text-2xl font-bold mb-4">Rating Component</h2>
|
|
2397
|
+
<div className="flex justify-center flex-col items-center gap-4">
|
|
2398
|
+
<Rating
|
|
2399
|
+
value={rating}
|
|
2400
|
+
onValueChange={(value) => setRating(value)}
|
|
2401
|
+
>
|
|
2402
|
+
{Array.from({ length: 5 }).map((_, index) => (
|
|
2403
|
+
<RatingButton key={index} />
|
|
2404
|
+
))}
|
|
2405
|
+
</Rating>
|
|
2406
|
+
<p className="text-sm text-muted-foreground">
|
|
2407
|
+
Current rating: {rating} stars
|
|
2408
|
+
</p>
|
|
2409
|
+
</div>
|
|
2410
|
+
</div>
|
|
2411
|
+
|
|
2412
|
+
<div>
|
|
2413
|
+
<h2 className="text-2xl font-bold mb-4">Ripple Button</h2>
|
|
2414
|
+
<div className="flex justify-center gap-4">
|
|
2415
|
+
<RippleButton>Click Me</RippleButton>
|
|
2416
|
+
<RippleButton variant="outline">Ripple Effect</RippleButton>
|
|
2417
|
+
<RippleButton variant="secondary">Secondary Ripple</RippleButton>
|
|
2418
|
+
</div>
|
|
2419
|
+
</div>
|
|
2420
|
+
|
|
2421
|
+
<div>
|
|
2422
|
+
<h2 className="text-2xl font-bold mb-4">Social Button</h2>
|
|
2423
|
+
<div className="flex justify-center gap-4">
|
|
2424
|
+
<SocialButton variant="light" />
|
|
2425
|
+
<SocialButton variant="dark" />
|
|
2426
|
+
</div>
|
|
2427
|
+
</div>
|
|
2428
|
+
|
|
2429
|
+
<div>
|
|
2430
|
+
<h2 className="text-2xl font-bold mb-4">Split Button</h2>
|
|
2431
|
+
<div className="flex justify-center">
|
|
2432
|
+
<SplitButton
|
|
2433
|
+
onClick={() => alert("Primary action")}
|
|
2434
|
+
items={[
|
|
2435
|
+
{ label: "Action 1", command: () => alert("Action 1") },
|
|
2436
|
+
{ label: "Action 2", command: () => alert("Action 2") },
|
|
2437
|
+
{ separator: true },
|
|
2438
|
+
{ label: "Delete", command: () => alert("Delete") },
|
|
2439
|
+
]}
|
|
2440
|
+
>
|
|
2441
|
+
Primary Action
|
|
2442
|
+
</SplitButton>
|
|
2443
|
+
</div>
|
|
2444
|
+
</div>
|
|
2445
|
+
|
|
2446
|
+
<div>
|
|
2447
|
+
<h2 className="text-2xl font-bold mb-4">Switch Button</h2>
|
|
2448
|
+
<div className="flex justify-center gap-4 flex-col items-center">
|
|
2449
|
+
<SwitchButton
|
|
2450
|
+
checked={switchChecked}
|
|
2451
|
+
onCheckedChange={setSwitchChecked}
|
|
2452
|
+
labels={{ checked: "ON", unchecked: "OFF" }}
|
|
2453
|
+
/>
|
|
2454
|
+
<p className="text-sm text-muted-foreground">
|
|
2455
|
+
Switch is: {switchChecked ? "ON" : "OFF"}
|
|
2456
|
+
</p>
|
|
2457
|
+
</div>
|
|
2458
|
+
</div>
|
|
2459
|
+
|
|
2460
|
+
<div>
|
|
2461
|
+
<h2 className="text-2xl font-bold mb-4">Toggle Button</h2>
|
|
2462
|
+
<div className="flex justify-center gap-4 flex-col items-center">
|
|
2463
|
+
<ToggleButton
|
|
2464
|
+
checked={toggleChecked}
|
|
2465
|
+
onChange={setToggleChecked}
|
|
2466
|
+
>
|
|
2467
|
+
Toggle Me
|
|
2468
|
+
</ToggleButton>
|
|
2469
|
+
<p className="text-sm text-muted-foreground">
|
|
2470
|
+
Toggle is: {toggleChecked ? "ON" : "OFF"}
|
|
2471
|
+
</p>
|
|
2472
|
+
</div>
|
|
2473
|
+
</div>
|
|
2474
|
+
|
|
2475
|
+
<div>
|
|
2476
|
+
<h2 className="text-2xl font-bold mb-4">Slide Button</h2>
|
|
2477
|
+
<div className="flex justify-center gap-4">
|
|
2478
|
+
<SlideButton text="Hover Me" hoverText="Slide Effect!" />
|
|
2479
|
+
<SlideButton text="Components" hoverText="Browse Now!" variant="ghost" />
|
|
2480
|
+
</div>
|
|
2481
|
+
</div>
|
|
2482
|
+
|
|
2483
|
+
<div>
|
|
2484
|
+
<h2 className="text-2xl font-bold mb-4">Text Reveal Button</h2>
|
|
2485
|
+
<div className="flex justify-center">
|
|
2486
|
+
<TextRevealButton text="HOVER ME" />
|
|
2487
|
+
</div>
|
|
2488
|
+
</div>
|
|
2489
|
+
|
|
2490
|
+
<div>
|
|
2491
|
+
<h2 className="text-2xl font-bold mb-4">Shimmer Button</h2>
|
|
2492
|
+
<div className="flex justify-center gap-4">
|
|
2493
|
+
<ShimmerButton>Shimmer Effect</ShimmerButton>
|
|
2494
|
+
<ShimmerButton shimmerColor="#00ff00">Green Shimmer</ShimmerButton>
|
|
2495
|
+
</div>
|
|
2496
|
+
</div>
|
|
2497
|
+
|
|
2498
|
+
<div>
|
|
2499
|
+
<h2 className="text-2xl font-bold mb-4">Tooltip Button</h2>
|
|
2500
|
+
<div className="flex justify-center gap-4">
|
|
2501
|
+
<TooltipButton
|
|
2502
|
+
icon={<MousePointerClick className="h-4 w-4" />}
|
|
2503
|
+
content="Click me!"
|
|
2504
|
+
onClick={() => alert("Clicked!")}
|
|
2505
|
+
/>
|
|
2506
|
+
<TooltipButton
|
|
2507
|
+
icon={<Download className="h-4 w-4" />}
|
|
2508
|
+
content="Download"
|
|
2509
|
+
onClick={() => alert("Download clicked!")}
|
|
2510
|
+
/>
|
|
2511
|
+
</div>
|
|
2512
|
+
</div>
|
|
2513
|
+
|
|
2514
|
+
<div>
|
|
2515
|
+
<h2 className="text-2xl font-bold mb-4">Button Effects</h2>
|
|
2516
|
+
<div className="flex gap-4 flex-wrap items-center">
|
|
2517
|
+
<Button effect="ringHover">Ring Hover</Button>
|
|
2518
|
+
<Button effect="shine">Shine</Button>
|
|
2519
|
+
<Button effect="shineHover">Shine Hover</Button>
|
|
2520
|
+
<Button effect="gooeyRight">Gooey Right</Button>
|
|
2521
|
+
<Button effect="gooeyLeft">Gooey Left</Button>
|
|
2522
|
+
<Button effect="hoverUnderline">Hover Underline</Button>
|
|
2523
|
+
</div>
|
|
2524
|
+
</div>
|
|
2525
|
+
|
|
2526
|
+
<div>
|
|
2527
|
+
<h2 className="text-2xl font-bold mb-4">Button Sizes</h2>
|
|
2528
|
+
<div className="flex gap-4 flex-wrap items-center">
|
|
2529
|
+
<Button size="sm">Small</Button>
|
|
2530
|
+
<Button size="default">Default</Button>
|
|
2531
|
+
<Button size="lg">Large</Button>
|
|
2532
|
+
<Button size="icon">
|
|
2533
|
+
<MousePointerClick className="h-4 w-4" />
|
|
2534
|
+
</Button>
|
|
2535
|
+
</div>
|
|
2536
|
+
</div>
|
|
2537
|
+
|
|
2538
|
+
<div>
|
|
2539
|
+
<h2 className="text-2xl font-bold mb-4">Advanced Variants</h2>
|
|
2540
|
+
<div className="flex gap-4 flex-wrap items-center">
|
|
2541
|
+
<Button variant="elevated">Elevated</Button>
|
|
2542
|
+
<Button variant="filled">Filled</Button>
|
|
2543
|
+
<Button variant="tonal">Tonal</Button>
|
|
2544
|
+
<Button variant="soft">Soft</Button>
|
|
2545
|
+
<Button variant="glass">Glass</Button>
|
|
2546
|
+
<Button variant="success">Success</Button>
|
|
2547
|
+
<Button variant="warning">Warning</Button>
|
|
2548
|
+
<Button variant="info">Info</Button>
|
|
2549
|
+
<Button variant="shine">Shine</Button>
|
|
2550
|
+
<Button variant="pulse">Pulse</Button>
|
|
2551
|
+
</div>
|
|
2552
|
+
</div>
|
|
2553
|
+
</div>
|
|
2554
|
+
)
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
|
|
2558
|
+
|
|
2559
|
+
const rippleVariants = cva('absolute rounded-full size-5 pointer-events-none', {
|
|
2560
|
+
variants: {
|
|
2561
|
+
variant: {
|
|
2562
|
+
default: 'bg-primary-foreground',
|
|
2563
|
+
destructive: 'bg-destructive',
|
|
2564
|
+
outline: 'bg-input',
|
|
2565
|
+
secondary: 'bg-secondary',
|
|
2566
|
+
ghost: 'bg-accent',
|
|
2567
|
+
},
|
|
2568
|
+
},
|
|
2569
|
+
defaultVariants: {
|
|
2570
|
+
variant: 'default',
|
|
2571
|
+
},
|
|
2572
|
+
});
|
|
2573
|
+
interface Particle {
|
|
2574
|
+
id: number
|
|
2575
|
+
x: number
|
|
2576
|
+
y: number
|
|
2577
|
+
}
|
|
2578
|
+
type FlipDirection = 'top' | 'bottom' | 'left' | 'right';
|
|
2579
|
+
|
|
2580
|
+
type FlipButtonProps = HTMLMotionProps<'button'> & {
|
|
2581
|
+
frontText: string;
|
|
2582
|
+
backText: string;
|
|
2583
|
+
transition?: Transition;
|
|
2584
|
+
frontClassName?: string;
|
|
2585
|
+
backClassName?: string;
|
|
2586
|
+
from?: FlipDirection;
|
|
2587
|
+
};
|
|
2588
|
+
type InputButtonContextType = {
|
|
2589
|
+
showInput: boolean;
|
|
2590
|
+
setShowInput: React.Dispatch<React.SetStateAction<boolean>>;
|
|
2591
|
+
transition: Transition;
|
|
2592
|
+
id: string;
|
|
2593
|
+
};
|
|
2594
|
+
type InputButtonProviderProps = React.ComponentProps<'div'> &
|
|
2595
|
+
Partial<InputButtonContextType>;
|
|
2596
|
+
interface AttractButtonProps
|
|
2597
|
+
extends Omit<React.ComponentProps<"button">, "children">,
|
|
2598
|
+
VariantProps<typeof attractButtonVariants> {
|
|
2599
|
+
particleCount?: number
|
|
2600
|
+
attractRadius?: number
|
|
2601
|
+
icon?: React.ReactNode
|
|
2602
|
+
activeText?: string
|
|
2603
|
+
inactiveText?: string
|
|
2604
|
+
}
|
|
2605
|
+
type CopyButtonProps = Omit<HTMLMotionProps<'button'>, 'children' | 'onCopy'> &
|
|
2606
|
+
VariantProps<typeof buttonVariants> & {
|
|
2607
|
+
content?: string;
|
|
2608
|
+
delay?: number;
|
|
2609
|
+
onCopy?: (content: string) => void;
|
|
2610
|
+
isCopied?: boolean;
|
|
2611
|
+
onCopyChange?: (isCopied: boolean) => void;
|
|
2612
|
+
};
|
|
2613
|
+
|
|
2614
|
+
interface FloatButtonProps {
|
|
2615
|
+
icon?: React.ReactNode
|
|
2616
|
+
description?: string
|
|
2617
|
+
tooltip?: string
|
|
2618
|
+
type?: "default" | "primary"
|
|
2619
|
+
shape?: "circle" | "square"
|
|
2620
|
+
size?: "small" | "default" | "large"
|
|
2621
|
+
className?: string
|
|
2622
|
+
onClick?: () => void
|
|
2623
|
+
children?: React.ReactNode
|
|
2624
|
+
}
|
|
2625
|
+
type InputButtonInputProps = React.ComponentProps<'input'>;
|
|
2626
|
+
type InputButtonSubmitProps = HTMLMotionProps<'button'> & {
|
|
2627
|
+
icon?: React.ElementType;
|
|
2628
|
+
};
|
|
2629
|
+
type InputButtonProps = HTMLMotionProps<'div'>;
|
|
2630
|
+
type InputButtonActionProps = HTMLMotionProps<'button'>;
|
|
2631
|
+
type LiquidButtonProps = HTMLMotionProps<'button'> &
|
|
2632
|
+
VariantProps<typeof buttonVariants>;
|
|
2633
|
+
interface ParticleButtonProps
|
|
2634
|
+
extends Omit<React.ComponentProps<"button">, "onClick">,
|
|
2635
|
+
VariantProps<typeof buttonVariants> {
|
|
2636
|
+
onSuccess?: () => void
|
|
2637
|
+
successDuration?: number
|
|
2638
|
+
particleCount?: number
|
|
2639
|
+
showIcon?: boolean
|
|
2640
|
+
icon?: React.ReactNode
|
|
2641
|
+
}
|
|
2642
|
+
interface SuccessParticlesProps {
|
|
2643
|
+
buttonRef: React.RefObject<HTMLButtonElement>
|
|
2644
|
+
particleCount: number
|
|
2645
|
+
}
|
|
2646
|
+
type RatingButtonProps = LucideProps & {
|
|
2647
|
+
index?: number;
|
|
2648
|
+
icon?: ReactElement<LucideProps>;
|
|
2649
|
+
};
|
|
2650
|
+
type RatingProps = {
|
|
2651
|
+
defaultValue?: number;
|
|
2652
|
+
value?: number;
|
|
2653
|
+
onChange?: (
|
|
2654
|
+
event: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>,
|
|
2655
|
+
value: number
|
|
2656
|
+
) => void;
|
|
2657
|
+
onValueChange?: (value: number) => void;
|
|
2658
|
+
readOnly?: boolean;
|
|
2659
|
+
className?: string;
|
|
2660
|
+
children?: ReactNode;
|
|
2661
|
+
};
|
|
2662
|
+
|
|
2663
|
+
type Ripple = {
|
|
2664
|
+
id: number;
|
|
2665
|
+
x: number;
|
|
2666
|
+
y: number;
|
|
2667
|
+
};
|
|
2668
|
+
|
|
2669
|
+
type RippleButtonProps = HTMLMotionProps<'button'> & {
|
|
2670
|
+
children: React.ReactNode;
|
|
2671
|
+
rippleClassName?: string;
|
|
2672
|
+
scale?: number;
|
|
2673
|
+
transition?: Transition;
|
|
2674
|
+
} & VariantProps<typeof buttonVariants>;
|
|
2675
|
+
interface ShimmerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
|
2676
|
+
shimmerColor?: string
|
|
2677
|
+
shimmerSize?: string
|
|
2678
|
+
borderRadius?: string
|
|
2679
|
+
shimmerDuration?: string
|
|
2680
|
+
background?: string
|
|
2681
|
+
className?: string
|
|
2682
|
+
children?: React.ReactNode
|
|
2683
|
+
}
|
|
2684
|
+
const slideTextButtonVariants = cva(
|
|
2685
|
+
"group relative inline-flex items-center justify-center overflow-hidden rounded-lg font-medium text-md tracking-tighter transition-all duration-300 outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50",
|
|
2686
|
+
{
|
|
2687
|
+
variants: {
|
|
2688
|
+
variant: {
|
|
2689
|
+
default:
|
|
2690
|
+
"bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90",
|
|
2691
|
+
ghost:
|
|
2692
|
+
"border border-black/10 text-black hover:bg-black/5 dark:border-white/10 dark:text-white dark:hover:bg-white/5",
|
|
2693
|
+
},
|
|
2694
|
+
size: {
|
|
2695
|
+
sm: "h-8 px-6 text-sm min-w-[180px]",
|
|
2696
|
+
default: "h-10 px-8 md:min-w-56",
|
|
2697
|
+
lg: "h-12 px-10 text-lg min-w-[280px]",
|
|
2698
|
+
},
|
|
2699
|
+
},
|
|
2700
|
+
defaultVariants: {
|
|
2701
|
+
variant: "default",
|
|
2702
|
+
size: "default",
|
|
2703
|
+
},
|
|
2704
|
+
}
|
|
2705
|
+
)
|
|
2706
|
+
const socialButtonVariants = cva(
|
|
2707
|
+
"min-w-40 relative transition-colors duration-200",
|
|
2708
|
+
{
|
|
2709
|
+
variants: {
|
|
2710
|
+
variant: {
|
|
2711
|
+
light: "bg-white dark:bg-black hover:bg-gray-50 dark:hover:bg-gray-950 text-black dark:text-white border border-black/10 dark:border-white/10",
|
|
2712
|
+
dark: "bg-black dark:bg-white hover:bg-gray-900 dark:hover:bg-gray-100 text-white dark:text-black border border-white/10 dark:border-black/10",
|
|
2713
|
+
outline: "bg-transparent hover:bg-accent text-foreground border border-border",
|
|
2714
|
+
},
|
|
2715
|
+
},
|
|
2716
|
+
defaultVariants: {
|
|
2717
|
+
variant: "light",
|
|
2718
|
+
},
|
|
2719
|
+
}
|
|
2720
|
+
)
|
|
2721
|
+
|
|
2722
|
+
interface ShareButton {
|
|
2723
|
+
icon: React.ComponentType<{ className?: string }>
|
|
2724
|
+
label: string
|
|
2725
|
+
onClick?: () => void
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
interface SocialButtonProps
|
|
2729
|
+
extends Omit<React.ComponentProps<"button">, "children">,
|
|
2730
|
+
VariantProps<typeof socialButtonVariants> {
|
|
2731
|
+
shareButtons?: ShareButton[]
|
|
2732
|
+
label?: string
|
|
2733
|
+
icon?: React.ReactNode
|
|
2734
|
+
}
|
|
2735
|
+
interface SplitButtonItem {
|
|
2736
|
+
label: string;
|
|
2737
|
+
icon?: React.ReactNode;
|
|
2738
|
+
command?: () => void;
|
|
2739
|
+
separator?: boolean;
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
interface SplitButtonProps {
|
|
2743
|
+
label: string;
|
|
2744
|
+
icon?: React.ReactNode;
|
|
2745
|
+
items: SplitButtonItem[];
|
|
2746
|
+
onClick?: () => void;
|
|
2747
|
+
className?: string;
|
|
2748
|
+
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost";
|
|
2749
|
+
size?: "default" | "sm" | "lg" | "icon";
|
|
2750
|
+
disabled?: boolean;
|
|
2751
|
+
align?: "left" | "middle" | "right"
|
|
2752
|
+
}
|
|
2753
|
+
interface SwitchButtonProps {
|
|
2754
|
+
checked?: boolean;
|
|
2755
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
2756
|
+
className?: string;
|
|
2757
|
+
variant?: 'default' | 'primary' | 'secondary' | 'destructive';
|
|
2758
|
+
size?: 'sm' | 'md' | 'lg';
|
|
2759
|
+
disabled?: boolean;
|
|
2760
|
+
labels?: {
|
|
2761
|
+
checked: string;
|
|
2762
|
+
unchecked: string;
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
interface TextRevealButtonProps
|
|
2766
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
2767
|
+
text?: string;
|
|
2768
|
+
revealColor?: string;
|
|
2769
|
+
strokeColor?: string;
|
|
2770
|
+
}
|
|
2771
|
+
interface ButtonToggleProps {
|
|
2772
|
+
checked?: boolean
|
|
2773
|
+
onChange?: (checked: boolean) => void
|
|
2774
|
+
disabled?: boolean
|
|
2775
|
+
className?: string
|
|
2776
|
+
variant?: "default" | "outline" | "ghost"
|
|
2777
|
+
size?: "sm" | "md" | "lg" | "icon"
|
|
2778
|
+
children?: React.ReactNode
|
|
2779
|
+
}
|
|
2780
|
+
const toggleButtonVariants = cva(
|
|
2781
|
+
"inline-flex items-center justify-center rounded-md 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:opacity-50",
|
|
2782
|
+
{
|
|
2783
|
+
variants: {
|
|
2784
|
+
variant: {
|
|
2785
|
+
default: "",
|
|
2786
|
+
outline: "",
|
|
2787
|
+
ghost: "",
|
|
2788
|
+
},
|
|
2789
|
+
size: {
|
|
2790
|
+
sm: "h-8 px-3 text-xs",
|
|
2791
|
+
md: "h-10 px-4 py-2 text-sm",
|
|
2792
|
+
lg: "h-12 px-6 text-base",
|
|
2793
|
+
},
|
|
2794
|
+
checked: {
|
|
2795
|
+
true: "",
|
|
2796
|
+
false: "",
|
|
2797
|
+
},
|
|
2798
|
+
},
|
|
2799
|
+
compoundVariants: [
|
|
2800
|
+
// Default variant - checked state
|
|
2801
|
+
{
|
|
2802
|
+
variant: "default",
|
|
2803
|
+
checked: true,
|
|
2804
|
+
class: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
2805
|
+
},
|
|
2806
|
+
// Default variant - unchecked state
|
|
2807
|
+
{
|
|
2808
|
+
variant: "default",
|
|
2809
|
+
checked: false,
|
|
2810
|
+
class: "bg-background text-foreground border border-input shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
2811
|
+
},
|
|
2812
|
+
// Outline variant - checked state
|
|
2813
|
+
{
|
|
2814
|
+
variant: "outline",
|
|
2815
|
+
checked: true,
|
|
2816
|
+
class: "border border-primary bg-background text-primary shadow-sm hover:bg-accent",
|
|
2817
|
+
},
|
|
2818
|
+
// Outline variant - unchecked state
|
|
2819
|
+
{
|
|
2820
|
+
variant: "outline",
|
|
2821
|
+
checked: false,
|
|
2822
|
+
class: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
2823
|
+
},
|
|
2824
|
+
// Ghost variant - checked state
|
|
2825
|
+
{
|
|
2826
|
+
variant: "ghost",
|
|
2827
|
+
checked: true,
|
|
2828
|
+
class: "bg-accent text-accent-foreground hover:bg-accent/80",
|
|
2829
|
+
},
|
|
2830
|
+
// Ghost variant - unchecked state
|
|
2831
|
+
{
|
|
2832
|
+
variant: "ghost",
|
|
2833
|
+
checked: false,
|
|
2834
|
+
class: "hover:bg-accent hover:text-accent-foreground",
|
|
2835
|
+
},
|
|
2836
|
+
],
|
|
2837
|
+
defaultVariants: {
|
|
2838
|
+
variant: "default",
|
|
2839
|
+
size: "md",
|
|
2840
|
+
checked: false,
|
|
2841
|
+
},
|
|
2842
|
+
}
|
|
2843
|
+
)
|
|
2844
|
+
|
|
2845
|
+
type RatingContextValue = {
|
|
2846
|
+
value: number;
|
|
2847
|
+
readOnly: boolean;
|
|
2848
|
+
hoverValue: number | null;
|
|
2849
|
+
focusedStar: number | null;
|
|
2850
|
+
handleValueChange: (
|
|
2851
|
+
event: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>,
|
|
2852
|
+
value: number
|
|
2853
|
+
) => void;
|
|
2854
|
+
handleKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
|
|
2855
|
+
setHoverValue: (value: number | null) => void;
|
|
2856
|
+
setFocusedStar: (value: number | null) => void;
|
|
2857
|
+
};
|