@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,419 @@
|
|
|
1
|
+
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ useCopyToClipboard Hook ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
7
|
+
* ★ ━━━━ ☆ ━━━━ ━━━━ ☆ ━━━━ ★
|
|
8
|
+
* useCopyToClipboard: A React hook for copying text to clipboard with timeout and callbacks
|
|
9
|
+
*
|
|
10
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ Basic Usage ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
11
|
+
* ```jsx
|
|
12
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
13
|
+
*
|
|
14
|
+
* return (
|
|
15
|
+
* <button onClick={() => copyToClipboard('Hello World')}>
|
|
16
|
+
* {isCopied ? 'Copied!' : 'Copy Text'}
|
|
17
|
+
* </button>
|
|
18
|
+
* )
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ With Custom Timeout ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
22
|
+
* ```jsx
|
|
23
|
+
* // 2 second timeout
|
|
24
|
+
* const { copyToClipboard } = useCopyToClipboard({
|
|
25
|
+
* timeout: 2000
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* // 10 second timeout (for important info)
|
|
29
|
+
* const { copyToClipboard } = useCopyToClipboard({
|
|
30
|
+
* timeout: 10000
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ With Callback ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
35
|
+
* ```jsx
|
|
36
|
+
* const { copyToClipboard } = useCopyToClipboard({
|
|
37
|
+
* onCopy: () => {
|
|
38
|
+
* // Show success toast
|
|
39
|
+
* toast.success('Copied to clipboard!')
|
|
40
|
+
*
|
|
41
|
+
* // Track analytics event
|
|
42
|
+
* analytics.track('text_copied')
|
|
43
|
+
*
|
|
44
|
+
* // Update UI state
|
|
45
|
+
* setShowCopiedFeedback(true)
|
|
46
|
+
* }
|
|
47
|
+
* })
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ In Component Examples ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
51
|
+
* ```jsx
|
|
52
|
+
* // Copy button component
|
|
53
|
+
* function CopyButton({ text }) {
|
|
54
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
55
|
+
*
|
|
56
|
+
* return (
|
|
57
|
+
* <button
|
|
58
|
+
* onClick={() => copyToClipboard(text)}
|
|
59
|
+
* className={isCopied ? 'copied' : ''}
|
|
60
|
+
* >
|
|
61
|
+
* {isCopied ? '✓ Copied' : 'Copy'}
|
|
62
|
+
* </button>
|
|
63
|
+
* )
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* // Code snippet with copy
|
|
67
|
+
* function CodeSnippet({ code }) {
|
|
68
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard({
|
|
69
|
+
* timeout: 3000,
|
|
70
|
+
* onCopy: () => console.log('Code copied:', code)
|
|
71
|
+
* })
|
|
72
|
+
*
|
|
73
|
+
* return (
|
|
74
|
+
* <div className="code-block">
|
|
75
|
+
* <pre>{code}</pre>
|
|
76
|
+
* <button onClick={() => copyToClipboard(code)}>
|
|
77
|
+
* {isCopied ? 'Copied!' : 'Copy Code'}
|
|
78
|
+
* </button>
|
|
79
|
+
* </div>
|
|
80
|
+
* )
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ Multiple Copy Instances ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
85
|
+
* ```jsx
|
|
86
|
+
* // Track which item was copied
|
|
87
|
+
* function CopyList({ items }) {
|
|
88
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
89
|
+
*
|
|
90
|
+
* return (
|
|
91
|
+
* <ul>
|
|
92
|
+
* {items.map((item) => (
|
|
93
|
+
* <li key={item.id}>
|
|
94
|
+
* {item.text}
|
|
95
|
+
* <button
|
|
96
|
+
* onClick={() => copyToClipboard(item.text)}
|
|
97
|
+
* aria-label={`Copy ${item.name}`}
|
|
98
|
+
* >
|
|
99
|
+
* {isCopied === item.text ? '✓' : 'Copy'}
|
|
100
|
+
* </button>
|
|
101
|
+
* </li>
|
|
102
|
+
* ))}
|
|
103
|
+
* </ul>
|
|
104
|
+
* )
|
|
105
|
+
* }
|
|
106
|
+
*
|
|
107
|
+
* // Compare current copied text
|
|
108
|
+
* function isCurrentlyCopied(text) {
|
|
109
|
+
* return isCopied === text
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ With UI Feedback Patterns ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
114
|
+
* ```jsx
|
|
115
|
+
* // Tooltip feedback
|
|
116
|
+
* function CopyWithTooltip({ text }) {
|
|
117
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
118
|
+
*
|
|
119
|
+
* return (
|
|
120
|
+
* <Tooltip content={isCopied ? 'Copied!' : 'Click to copy'}>
|
|
121
|
+
* <button onClick={() => copyToClipboard(text)}>
|
|
122
|
+
* {text}
|
|
123
|
+
* </button>
|
|
124
|
+
* </Tooltip>
|
|
125
|
+
* )
|
|
126
|
+
* }
|
|
127
|
+
*
|
|
128
|
+
* // Icon feedback
|
|
129
|
+
* function CopyIconButton({ text }) {
|
|
130
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
131
|
+
*
|
|
132
|
+
* return (
|
|
133
|
+
* <button onClick={() => copyToClipboard(text)}>
|
|
134
|
+
* {isCopied ? <CheckIcon /> : <CopyIcon />}
|
|
135
|
+
* </button>
|
|
136
|
+
* )
|
|
137
|
+
* }
|
|
138
|
+
*
|
|
139
|
+
* // Toast notification
|
|
140
|
+
* function CopyWithToast({ text }) {
|
|
141
|
+
* const { copyToClipboard } = useCopyToClipboard({
|
|
142
|
+
* onCopy: () => toast('Copied to clipboard!')
|
|
143
|
+
* })
|
|
144
|
+
*
|
|
145
|
+
* return (
|
|
146
|
+
* <button onClick={() => copyToClipboard(text)}>
|
|
147
|
+
* Copy
|
|
148
|
+
* </button>
|
|
149
|
+
* )
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ Hook Return Values ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
154
|
+
* ```typescript
|
|
155
|
+
* interface UseCopyToClipboardReturn {
|
|
156
|
+
* // The currently copied text (null if nothing copied or timeout expired)
|
|
157
|
+
* isCopied: string | null
|
|
158
|
+
*
|
|
159
|
+
* // Function to copy text to clipboard
|
|
160
|
+
* copyToClipboard: (value: string) => void
|
|
161
|
+
* }
|
|
162
|
+
*
|
|
163
|
+
* // Usage example
|
|
164
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard(options)
|
|
165
|
+
*
|
|
166
|
+
* // Check if specific text is currently copied
|
|
167
|
+
* const isThisCopied = isCopied === 'specific text'
|
|
168
|
+
*
|
|
169
|
+
* // Check if anything is copied
|
|
170
|
+
* const hasCopied = isCopied !== null
|
|
171
|
+
* ```
|
|
172
|
+
*
|
|
173
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ Hook Options ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
174
|
+
* ```typescript
|
|
175
|
+
* interface UseCopyToClipboardOptions {
|
|
176
|
+
* // Time in milliseconds before resetting copied state (default: 4000)
|
|
177
|
+
* timeout?: number
|
|
178
|
+
*
|
|
179
|
+
* // Callback function triggered after successful copy
|
|
180
|
+
* onCopy?: () => void
|
|
181
|
+
* }
|
|
182
|
+
*
|
|
183
|
+
* // Examples of different configurations
|
|
184
|
+
* const defaultOptions = {} // Uses default 4000ms timeout
|
|
185
|
+
*
|
|
186
|
+
* const shortFeedback = { timeout: 1000 } // Quick feedback
|
|
187
|
+
*
|
|
188
|
+
* const withAnalytics = {
|
|
189
|
+
* timeout: 5000,
|
|
190
|
+
* onCopy: () => trackEvent('copy_action')
|
|
191
|
+
* }
|
|
192
|
+
*
|
|
193
|
+
* const withMultipleCallbacks = {
|
|
194
|
+
* onCopy: () => {
|
|
195
|
+
* showToast()
|
|
196
|
+
* trackAnalytics()
|
|
197
|
+
* updateUIState()
|
|
198
|
+
* }
|
|
199
|
+
* }
|
|
200
|
+
* ```
|
|
201
|
+
*
|
|
202
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ Error Handling ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
203
|
+
* ```jsx
|
|
204
|
+
* // Safe copy with error handling
|
|
205
|
+
* function SafeCopyButton({ text }) {
|
|
206
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
207
|
+
*
|
|
208
|
+
* const handleCopy = () => {
|
|
209
|
+
* try {
|
|
210
|
+
* copyToClipboard(text)
|
|
211
|
+
* } catch (error) {
|
|
212
|
+
* console.error('Failed to copy:', error)
|
|
213
|
+
* // Show error message to user
|
|
214
|
+
* toast.error('Failed to copy to clipboard')
|
|
215
|
+
* }
|
|
216
|
+
* }
|
|
217
|
+
*
|
|
218
|
+
* return (
|
|
219
|
+
* <button onClick={handleCopy}>
|
|
220
|
+
* {isCopied ? 'Copied!' : 'Copy'}
|
|
221
|
+
* </button>
|
|
222
|
+
* )
|
|
223
|
+
* }
|
|
224
|
+
*
|
|
225
|
+
* // Check clipboard API support
|
|
226
|
+
* function CopyButton({ text }) {
|
|
227
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
228
|
+
*
|
|
229
|
+
* const isClipboardSupported =
|
|
230
|
+
* typeof window !== 'undefined' &&
|
|
231
|
+
* navigator?.clipboard?.writeText
|
|
232
|
+
*
|
|
233
|
+
* if (!isClipboardSupported) {
|
|
234
|
+
* return <span>Clipboard not supported in this browser</span>
|
|
235
|
+
* }
|
|
236
|
+
*
|
|
237
|
+
* return (
|
|
238
|
+
* <button onClick={() => copyToClipboard(text)}>
|
|
239
|
+
* {isCopied ? 'Copied!' : 'Copy'}
|
|
240
|
+
* </button>
|
|
241
|
+
* )
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*
|
|
245
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ Advanced Usage Patterns ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
246
|
+
* ```jsx
|
|
247
|
+
* // Debounced copy (prevent rapid clicks)
|
|
248
|
+
* function DebouncedCopyButton({ text }) {
|
|
249
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
250
|
+
* const [isDebouncing, setIsDebouncing] = useState(false)
|
|
251
|
+
*
|
|
252
|
+
* const handleCopy = () => {
|
|
253
|
+
* if (isDebouncing) return
|
|
254
|
+
*
|
|
255
|
+
* setIsDebouncing(true)
|
|
256
|
+
* copyToClipboard(text)
|
|
257
|
+
*
|
|
258
|
+
* setTimeout(() => {
|
|
259
|
+
* setIsDebouncing(false)
|
|
260
|
+
* }, 1000)
|
|
261
|
+
* }
|
|
262
|
+
*
|
|
263
|
+
* return (
|
|
264
|
+
* <button
|
|
265
|
+
* onClick={handleCopy}
|
|
266
|
+
* disabled={isDebouncing}
|
|
267
|
+
* >
|
|
268
|
+
* {isDebouncing ? 'Copying...' : isCopied ? 'Copied!' : 'Copy'}
|
|
269
|
+
* </button>
|
|
270
|
+
* )
|
|
271
|
+
* }
|
|
272
|
+
*
|
|
273
|
+
* // Copy with confirmation
|
|
274
|
+
* function CopyWithConfirmation({ text }) {
|
|
275
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
276
|
+
* const [showConfirm, setShowConfirm] = useState(false)
|
|
277
|
+
*
|
|
278
|
+
* const handleCopy = () => {
|
|
279
|
+
* if (text.length > 1000) {
|
|
280
|
+
* setShowConfirm(true)
|
|
281
|
+
* } else {
|
|
282
|
+
* copyToClipboard(text)
|
|
283
|
+
* }
|
|
284
|
+
* }
|
|
285
|
+
*
|
|
286
|
+
* return (
|
|
287
|
+
* <>
|
|
288
|
+
* <button onClick={handleCopy}>
|
|
289
|
+
* Copy
|
|
290
|
+
* </button>
|
|
291
|
+
*
|
|
292
|
+
* {showConfirm && (
|
|
293
|
+
* <ConfirmationDialog
|
|
294
|
+
* onConfirm={() => {
|
|
295
|
+
* copyToClipboard(text)
|
|
296
|
+
* setShowConfirm(false)
|
|
297
|
+
* }}
|
|
298
|
+
* onCancel={() => setShowConfirm(false)}
|
|
299
|
+
* />
|
|
300
|
+
* )}
|
|
301
|
+
* </>
|
|
302
|
+
* )
|
|
303
|
+
* }
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ Integration Examples ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
307
|
+
* ```jsx
|
|
308
|
+
* // In a form field
|
|
309
|
+
* function CopyableInput({ value }) {
|
|
310
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
311
|
+
*
|
|
312
|
+
* return (
|
|
313
|
+
* <div className="input-group">
|
|
314
|
+
* <input type="text" value={value} readOnly />
|
|
315
|
+
* <button
|
|
316
|
+
* onClick={() => copyToClipboard(value)}
|
|
317
|
+
* className={isCopied ? 'success' : ''}
|
|
318
|
+
* >
|
|
319
|
+
* {isCopied ? '✓' : 'Copy'}
|
|
320
|
+
* </button>
|
|
321
|
+
* </div>
|
|
322
|
+
* )
|
|
323
|
+
* }
|
|
324
|
+
*
|
|
325
|
+
* // In a data table
|
|
326
|
+
* function DataTable({ rows }) {
|
|
327
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
328
|
+
*
|
|
329
|
+
* return (
|
|
330
|
+
* <table>
|
|
331
|
+
* <tbody>
|
|
332
|
+
* {rows.map(row => (
|
|
333
|
+
* <tr key={row.id}>
|
|
334
|
+
* <td>{row.value}</td>
|
|
335
|
+
* <td>
|
|
336
|
+
* <button
|
|
337
|
+
* onClick={() => copyToClipboard(row.value)}
|
|
338
|
+
* aria-label={`Copy ${row.name}`}
|
|
339
|
+
* >
|
|
340
|
+
* {isCopied === row.value ? 'Copied' : 'Copy'}
|
|
341
|
+
* </button>
|
|
342
|
+
* </td>
|
|
343
|
+
* </tr>
|
|
344
|
+
* ))}
|
|
345
|
+
* </tbody>
|
|
346
|
+
* </table>
|
|
347
|
+
* )
|
|
348
|
+
* }
|
|
349
|
+
*
|
|
350
|
+
* // With rich text/html
|
|
351
|
+
* function CopyHTMLContent({ html }) {
|
|
352
|
+
* const { isCopied, copyToClipboard } = useCopyToClipboard()
|
|
353
|
+
*
|
|
354
|
+
* const handleCopyHTML = async () => {
|
|
355
|
+
* // Convert HTML to plain text for clipboard
|
|
356
|
+
* const plainText = html.replace(/<[^>]*>/g, '')
|
|
357
|
+
* copyToClipboard(plainText)
|
|
358
|
+
* }
|
|
359
|
+
*
|
|
360
|
+
* return (
|
|
361
|
+
* <div>
|
|
362
|
+
* <div dangerouslySetInnerHTML={{ __html: html }} />
|
|
363
|
+
* <button onClick={handleCopyHTML}>
|
|
364
|
+
* {isCopied ? 'Copied as text' : 'Copy text content'}
|
|
365
|
+
* </button>
|
|
366
|
+
* </div>
|
|
367
|
+
* )
|
|
368
|
+
* }
|
|
369
|
+
* ```
|
|
370
|
+
*
|
|
371
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ Browser Support Notes ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
372
|
+
* - **Modern Browsers**: Full support in Chrome, Firefox, Safari, Edge
|
|
373
|
+
* - **HTTPS Requirement**: Clipboard API requires secure context (HTTPS)
|
|
374
|
+
* - **User Permission**: Some browsers may require user permission
|
|
375
|
+
* - **Fallback Consideration**: Consider fallback for older browsers
|
|
376
|
+
*
|
|
377
|
+
* ★ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━ Best Practices ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ☆ ━━━━━━━━━━━━━━ ★
|
|
378
|
+
* 1. **Provide Visual Feedback**: Always show when text is copied
|
|
379
|
+
* 2. **Use Appropriate Timeout**: Match timeout to user reading speed
|
|
380
|
+
* 3. **Handle Empty Values**: Check for empty or null text
|
|
381
|
+
* 4. **Add Accessibility**: Include ARIA labels for screen readers
|
|
382
|
+
* 5. **Consider Context**: Show what was copied (especially in lists)
|
|
383
|
+
* 6. **Error Recovery**: Provide fallback options if clipboard fails
|
|
384
|
+
* 7. **Security**: Be cautious with sensitive data in clipboard
|
|
385
|
+
*
|
|
386
|
+
*/
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
export function useCopyToClipboard({
|
|
390
|
+
timeout = 4000,
|
|
391
|
+
onCopy,
|
|
392
|
+
}: {
|
|
393
|
+
timeout?: number
|
|
394
|
+
onCopy?: () => void
|
|
395
|
+
} = {}) {
|
|
396
|
+
const [isCopied, setIsCopied] =React.useState<string | null>(null)
|
|
397
|
+
|
|
398
|
+
const copyToClipboard = (value: string) => {
|
|
399
|
+
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!value) return
|
|
404
|
+
|
|
405
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
406
|
+
setIsCopied(value)
|
|
407
|
+
|
|
408
|
+
if (onCopy) {
|
|
409
|
+
onCopy()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
setTimeout(() => {
|
|
413
|
+
setIsCopied(null)
|
|
414
|
+
}, timeout)
|
|
415
|
+
}, console.error)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { isCopied, copyToClipboard }
|
|
419
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// app/hooks/index.ts
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
export function useCounter(initialValue = 0, step = 1) {
|
|
5
|
+
const [count, setCount] = useState<number>(initialValue);
|
|
6
|
+
|
|
7
|
+
const increment = useCallback(() => setCount(c => c + step), [step]);
|
|
8
|
+
const decrement = useCallback(() => setCount(c => c - step), [step]);
|
|
9
|
+
const reset = useCallback(() => setCount(initialValue), [initialValue]);
|
|
10
|
+
const setValue = useCallback((value: number) => setCount(value), []);
|
|
11
|
+
|
|
12
|
+
return { count, increment, decrement, reset, setValue } as const;
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect, RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useEventListener<K extends keyof HTMLElementEventMap>(
|
|
4
|
+
eventName: K,
|
|
5
|
+
handler: (event: HTMLElementEventMap[K]) => void,
|
|
6
|
+
element?: RefObject<HTMLElement>,
|
|
7
|
+
options?: boolean | AddEventListenerOptions
|
|
8
|
+
) {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const targetElement = element?.current || window;
|
|
11
|
+
|
|
12
|
+
if (!targetElement?.addEventListener) return;
|
|
13
|
+
|
|
14
|
+
const eventListener = (event: HTMLElementEventMap[K]) => handler(event);
|
|
15
|
+
|
|
16
|
+
targetElement.addEventListener(eventName, eventListener, options);
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
targetElement.removeEventListener(eventName, eventListener, options);
|
|
20
|
+
};
|
|
21
|
+
}, [eventName, handler, element, options]);
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useExportMarkdown() {
|
|
4
|
+
const exportMarkdown = useCallback((content: string, filename: string = 'document.md') => {
|
|
5
|
+
try {
|
|
6
|
+
if (!content) {
|
|
7
|
+
console.warn('No content provided for export');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const blob = new Blob([content], { type: 'text/markdown' });
|
|
12
|
+
const url = URL.createObjectURL(blob);
|
|
13
|
+
const a = document.createElement('a');
|
|
14
|
+
a.href = url;
|
|
15
|
+
a.download = filename.endsWith('.md') ? filename : `${filename}.md`;
|
|
16
|
+
document.body.appendChild(a);
|
|
17
|
+
a.click();
|
|
18
|
+
document.body.removeChild(a);
|
|
19
|
+
|
|
20
|
+
setTimeout(() => URL.revokeObjectURL(url), 100);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Failed to export markdown:', error);
|
|
23
|
+
}
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
return exportMarkdown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* import { useExportMarkdown } from './hooks/useExportMarkdown';
|
|
32
|
+
|
|
33
|
+
function MyComponent() {
|
|
34
|
+
const exportMarkdown = useExportMarkdown();
|
|
35
|
+
const [markdown, setMarkdown] = useState('');
|
|
36
|
+
|
|
37
|
+
const handleExport = () => {
|
|
38
|
+
exportMarkdown(markdown, 'custom-filename.md');
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<button onClick={handleExport}>
|
|
43
|
+
Export as Markdown
|
|
44
|
+
</button>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
*/
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FocusProps, useFocus as useAriaFocus } from 'react-aria';
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface UseFocusOptions extends FocusProps {
|
|
5
|
+
isDisabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function useFocus(props: UseFocusOptions = {}) {
|
|
9
|
+
const ref = useRef<HTMLElement>(null);
|
|
10
|
+
const { focusProps, isFocused } = useAriaFocus(props, ref);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
focusProps,
|
|
14
|
+
isFocused,
|
|
15
|
+
ref
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// app/hooks/index.ts
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
export function useInterval(callback: () => void, delay: number | null) {
|
|
5
|
+
const savedCallback = useRef<() => void>();
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
savedCallback.current = callback;
|
|
9
|
+
}, [callback]);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
function tick() {
|
|
13
|
+
if (savedCallback.current) {
|
|
14
|
+
savedCallback.current();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (delay !== null) {
|
|
19
|
+
const id = setInterval(tick, delay);
|
|
20
|
+
return () => clearInterval(id);
|
|
21
|
+
}
|
|
22
|
+
}, [delay]);
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { cn } from '~/components/catalyst-ui/utils'
|
|
3
|
+
|
|
4
|
+
function useIsClient(): boolean {
|
|
5
|
+
const [isClient, setIsClient] = useState(false)
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
setIsClient(true)
|
|
9
|
+
}, [])
|
|
10
|
+
|
|
11
|
+
return isClient
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export { useIsClient }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
export function useMediaQuery(query: string) {
|
|
4
|
+
const [value, setValue] = React.useState(false)
|
|
5
|
+
|
|
6
|
+
React.useEffect(() => {
|
|
7
|
+
function onChange(event: MediaQueryListEvent) {
|
|
8
|
+
setValue(event.matches)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const result = matchMedia(query)
|
|
12
|
+
result.addEventListener("change", onChange)
|
|
13
|
+
setValue(result.matches)
|
|
14
|
+
|
|
15
|
+
return () => result.removeEventListener("change", onChange)
|
|
16
|
+
}, [query])
|
|
17
|
+
|
|
18
|
+
return value
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener("change", onChange)
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener("change", onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isMobile
|
|
19
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { cn } from '~/components/catalyst-ui/utils'
|
|
3
|
+
|
|
4
|
+
type BoxModel = 'content-box' | 'border-box' | 'device-pixel-content-box'
|
|
5
|
+
|
|
6
|
+
type Size = {
|
|
7
|
+
width: number
|
|
8
|
+
height: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type UseResizeObserverOptions<T extends HTMLElement> = {
|
|
12
|
+
ref: React.RefObject<T>
|
|
13
|
+
onResize?: (size: Size) => void
|
|
14
|
+
box?: BoxModel
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type UseResizeObserverReturn = {
|
|
18
|
+
width: number | undefined
|
|
19
|
+
height: number | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function useResizeObserver<T extends HTMLElement = HTMLElement>(
|
|
23
|
+
options: UseResizeObserverOptions<T>
|
|
24
|
+
): UseResizeObserverReturn {
|
|
25
|
+
const { ref, onResize, box = 'content-box' } = options
|
|
26
|
+
const [size, setSize] = useState<Size | undefined>(undefined)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const element = ref.current
|
|
30
|
+
if (!element) return
|
|
31
|
+
|
|
32
|
+
if (typeof window === 'undefined' || !window.ResizeObserver) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
37
|
+
if (!Array.isArray(entries) || !entries.length) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const entry = entries[0]
|
|
42
|
+
let width: number
|
|
43
|
+
let height: number
|
|
44
|
+
|
|
45
|
+
if (box === 'border-box') {
|
|
46
|
+
width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width
|
|
47
|
+
height = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
|
|
48
|
+
} else if (box === 'device-pixel-content-box') {
|
|
49
|
+
width = entry.devicePixelContentBoxSize?.[0]?.inlineSize ?? entry.contentRect.width
|
|
50
|
+
height = entry.devicePixelContentBoxSize?.[0]?.blockSize ?? entry.contentRect.height
|
|
51
|
+
} else {
|
|
52
|
+
width = entry.contentBoxSize?.[0]?.inlineSize ?? entry.contentRect.width
|
|
53
|
+
height = entry.contentBoxSize?.[0]?.blockSize ?? entry.contentRect.height
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const newSize = { width, height }
|
|
57
|
+
|
|
58
|
+
if (onResize) {
|
|
59
|
+
onResize(newSize)
|
|
60
|
+
} else {
|
|
61
|
+
setSize(newSize)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
resizeObserver.observe(element, { box })
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
resizeObserver.disconnect()
|
|
69
|
+
}
|
|
70
|
+
}, [ref, box, onResize])
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
width: size?.width,
|
|
74
|
+
height: size?.height,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
export { useResizeObserver, }
|