@stacksjs/components 0.2.92 → 0.2.94
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/dist/{Sidebar-yb115a2w.stx → Sidebar-4hn7vw4n.stx} +18 -0
- package/dist/index.js +5 -5
- package/package.json +5 -6
- package/src/components/CodeBlock.stx +0 -54
- package/src/components/Footer.stx +0 -66
- package/src/components/Hero.stx +0 -25
- package/src/components/Installation.stx +0 -37
- package/src/components/index.ts +0 -4
- package/src/composables/index.ts +0 -4
- package/src/composables/useCopyCode.ts +0 -73
- package/src/composables/useDarkMode.ts +0 -100
- package/src/composables/useSEO.ts +0 -230
- package/src/composables/utilities/index.ts +0 -7
- package/src/composables/utilities/useClickOutside.ts +0 -72
- package/src/composables/utilities/useFocusTrap.ts +0 -159
- package/src/composables/utilities/useKeyboard.ts +0 -124
- package/src/composables/utilities/useLocalStorage.ts +0 -132
- package/src/composables/utilities/useMediaQuery.ts +0 -118
- package/src/index.ts +0 -99
- package/src/stx.d.ts +0 -4
- package/src/ui/accordion/Accordion.stx +0 -146
- package/src/ui/accordion/AccordionItem.stx +0 -37
- package/src/ui/accordion/index.ts +0 -47
- package/src/ui/audio/Audio.stx +0 -50
- package/src/ui/audio/README.md +0 -41
- package/src/ui/audio/index.ts +0 -13
- package/src/ui/auth/Login.stx +0 -161
- package/src/ui/auth/README.md +0 -142
- package/src/ui/auth/Signup.stx +0 -159
- package/src/ui/auth/TwoFactorChallenge.stx +0 -111
- package/src/ui/auth/index.ts +0 -32
- package/src/ui/avatar/Avatar.stx +0 -77
- package/src/ui/avatar/index.ts +0 -12
- package/src/ui/badge/Badge.stx +0 -62
- package/src/ui/badge/index.ts +0 -10
- package/src/ui/breadcrumb/Breadcrumb.stx +0 -79
- package/src/ui/breadcrumb/index.ts +0 -15
- package/src/ui/button/Button.stx +0 -74
- package/src/ui/button/README.md +0 -80
- package/src/ui/button/index.ts +0 -85
- package/src/ui/calendar/Calendar.stx +0 -189
- package/src/ui/calendar/README.md +0 -254
- package/src/ui/calendar/index.ts +0 -14
- package/src/ui/card/Card.stx +0 -68
- package/src/ui/card/index.ts +0 -13
- package/src/ui/checkbox/Checkbox.stx +0 -86
- package/src/ui/checkbox/index.ts +0 -16
- package/src/ui/combobox/Combobox.stx +0 -96
- package/src/ui/combobox/ComboboxButton.stx +0 -25
- package/src/ui/combobox/ComboboxInput.stx +0 -32
- package/src/ui/combobox/ComboboxOption.stx +0 -49
- package/src/ui/combobox/ComboboxOptions.stx +0 -16
- package/src/ui/combobox/README.md +0 -150
- package/src/ui/combobox/index.ts +0 -40
- package/src/ui/command-palette/CommandPalette.stx +0 -81
- package/src/ui/command-palette/CommandPaletteItem.stx +0 -27
- package/src/ui/command-palette/README.md +0 -42
- package/src/ui/command-palette/index.ts +0 -15
- package/src/ui/dialog/Dialog.stx +0 -41
- package/src/ui/dialog/DialogBackdrop.stx +0 -9
- package/src/ui/dialog/DialogDescription.stx +0 -11
- package/src/ui/dialog/DialogPanel.stx +0 -16
- package/src/ui/dialog/DialogTitle.stx +0 -11
- package/src/ui/dialog/README.md +0 -121
- package/src/ui/dialog/index.ts +0 -32
- package/src/ui/drawer/Drawer.stx +0 -120
- package/src/ui/drawer/README.md +0 -90
- package/src/ui/drawer/index.ts +0 -9
- package/src/ui/dropdown/Dropdown.stx +0 -82
- package/src/ui/dropdown/DropdownButton.stx +0 -29
- package/src/ui/dropdown/DropdownItem.stx +0 -35
- package/src/ui/dropdown/DropdownItems.stx +0 -18
- package/src/ui/dropdown/README.md +0 -114
- package/src/ui/dropdown/index.ts +0 -29
- package/src/ui/form/Form.stx +0 -95
- package/src/ui/form/index.ts +0 -67
- package/src/ui/heatmap/Heatmap.stx +0 -294
- package/src/ui/image/Image.stx +0 -165
- package/src/ui/image/README.md +0 -164
- package/src/ui/image/index.ts +0 -22
- package/src/ui/input/EmailInput.stx +0 -30
- package/src/ui/input/NumberInput.stx +0 -126
- package/src/ui/input/PasswordInput.stx +0 -143
- package/src/ui/input/SearchInput.stx +0 -44
- package/src/ui/input/TextInput.stx +0 -132
- package/src/ui/input/index.ts +0 -180
- package/src/ui/listbox/Listbox.stx +0 -101
- package/src/ui/listbox/ListboxButton.stx +0 -28
- package/src/ui/listbox/ListboxLabel.stx +0 -11
- package/src/ui/listbox/ListboxOption.stx +0 -49
- package/src/ui/listbox/ListboxOptions.stx +0 -16
- package/src/ui/listbox/README.md +0 -135
- package/src/ui/listbox/index.ts +0 -39
- package/src/ui/navigator/Navigator.stx +0 -113
- package/src/ui/navigator/README.md +0 -352
- package/src/ui/navigator/index.ts +0 -20
- package/src/ui/notification/Notification.stx +0 -117
- package/src/ui/notification/README.md +0 -114
- package/src/ui/notification/index.ts +0 -12
- package/src/ui/pagination/Pagination.stx +0 -150
- package/src/ui/pagination/index.ts +0 -11
- package/src/ui/payment/Checkout.stx +0 -31
- package/src/ui/payment/DefaultPaymentMethod.stx +0 -62
- package/src/ui/payment/PaymentMethods.stx +0 -104
- package/src/ui/payment/README.md +0 -345
- package/src/ui/payment/SubscriptionCheckout.stx +0 -323
- package/src/ui/payment/index.ts +0 -59
- package/src/ui/popover/Popover.stx +0 -60
- package/src/ui/popover/PopoverButton.stx +0 -29
- package/src/ui/popover/PopoverPanel.stx +0 -23
- package/src/ui/popover/README.md +0 -91
- package/src/ui/popover/index.ts +0 -22
- package/src/ui/portal/Portal.stx +0 -57
- package/src/ui/portal/index.ts +0 -6
- package/src/ui/progress/Progress.stx +0 -131
- package/src/ui/progress/index.ts +0 -12
- package/src/ui/radio/Radio.stx +0 -77
- package/src/ui/radio/index.ts +0 -15
- package/src/ui/radio-group/README.md +0 -139
- package/src/ui/radio-group/RadioGroup.stx +0 -60
- package/src/ui/radio-group/RadioGroupDescription.stx +0 -11
- package/src/ui/radio-group/RadioGroupLabel.stx +0 -11
- package/src/ui/radio-group/RadioGroupOption.stx +0 -58
- package/src/ui/radio-group/index.ts +0 -30
- package/src/ui/select/README.md +0 -180
- package/src/ui/select/Select.stx +0 -82
- package/src/ui/select/index.ts +0 -21
- package/src/ui/sidebar/Sidebar.stx +0 -458
- package/src/ui/sidebar/SidebarFooter.stx +0 -71
- package/src/ui/sidebar/SidebarHeader.stx +0 -103
- package/src/ui/sidebar/SidebarItem.stx +0 -86
- package/src/ui/sidebar/SidebarSection.stx +0 -83
- package/src/ui/sidebar/index.ts +0 -153
- package/src/ui/sidebar/themes.ts +0 -240
- package/src/ui/skeleton/Skeleton.stx +0 -48
- package/src/ui/skeleton/index.ts +0 -11
- package/src/ui/spinner/Spinner.stx +0 -109
- package/src/ui/spinner/index.ts +0 -9
- package/src/ui/stepper/README.md +0 -36
- package/src/ui/stepper/Stepper.stx +0 -17
- package/src/ui/stepper/StepperStep.stx +0 -57
- package/src/ui/stepper/index.ts +0 -19
- package/src/ui/storage/README.md +0 -193
- package/src/ui/storage/index.ts +0 -347
- package/src/ui/switch/README.md +0 -114
- package/src/ui/switch/Switch.stx +0 -72
- package/src/ui/switch/index.ts +0 -10
- package/src/ui/table/README.md +0 -39
- package/src/ui/table/Table.stx +0 -14
- package/src/ui/table/TableBody.stx +0 -10
- package/src/ui/table/TableCell.stx +0 -10
- package/src/ui/table/TableHead.stx +0 -10
- package/src/ui/table/TableHeader.stx +0 -20
- package/src/ui/table/TableRow.stx +0 -11
- package/src/ui/table/index.ts +0 -35
- package/src/ui/tabs/TabPanel.stx +0 -25
- package/src/ui/tabs/Tabs.stx +0 -202
- package/src/ui/tabs/index.ts +0 -50
- package/src/ui/teleport/Teleport.stx +0 -65
- package/src/ui/teleport/index.ts +0 -7
- package/src/ui/textarea/Textarea.stx +0 -108
- package/src/ui/textarea/index.ts +0 -20
- package/src/ui/tooltip/Tooltip.stx +0 -71
- package/src/ui/tooltip/index.ts +0 -10
- package/src/ui/transition/README.md +0 -30
- package/src/ui/transition/Transition.stx +0 -102
- package/src/ui/transition/index.ts +0 -25
- package/src/ui/video/README.md +0 -47
- package/src/ui/video/Video.stx +0 -45
- package/src/ui/video/index.ts +0 -16
- package/src/ui/virtual-list/VirtualList.stx +0 -67
- package/src/ui/virtual-list/index.ts +0 -10
- package/src/ui/virtual-table/VirtualTable.stx +0 -103
- package/src/ui/virtual-table/index.ts +0 -21
- package/src/utils/PROP_VALIDATION.md +0 -458
- package/src/utils/THEME_SYSTEM.md +0 -702
- package/src/utils/accessibility.ts +0 -841
- package/src/utils/animation/easing.ts +0 -368
- package/src/utils/animation/helpers.ts +0 -223
- package/src/utils/animation/index.ts +0 -5
- package/src/utils/animation/keyframes.ts +0 -230
- package/src/utils/error-handling.ts +0 -475
- package/src/utils/highlighter.ts +0 -93
- package/src/utils/i18n.ts +0 -634
- package/src/utils/keep-alive.ts +0 -819
- package/src/utils/lazy-loading.ts +0 -530
- package/src/utils/prop-validation.ts +0 -478
- package/src/utils/theme.ts +0 -626
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
/* eslint-disable style/max-statements-per-line */
|
|
2
|
-
/**
|
|
3
|
-
* SEO and meta tag utilities for STX components
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface SEOMetaOptions {
|
|
7
|
-
title?: string
|
|
8
|
-
description?: string
|
|
9
|
-
author?: string
|
|
10
|
-
keywords?: string[]
|
|
11
|
-
ogImage?: string
|
|
12
|
-
ogImageWidth?: string
|
|
13
|
-
ogImageHeight?: string
|
|
14
|
-
twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'
|
|
15
|
-
twitterSite?: string
|
|
16
|
-
twitterCreator?: string
|
|
17
|
-
canonical?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Set document title
|
|
22
|
-
*/
|
|
23
|
-
export function setTitle(title: string, template?: string): void {
|
|
24
|
-
if (typeof document === 'undefined')
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
document.title = template ? template.replace('%s', title) : title
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Set meta tag
|
|
32
|
-
*/
|
|
33
|
-
export function setMeta(name: string, content: string, isProperty = false): void {
|
|
34
|
-
if (typeof document === 'undefined')
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
const attribute = isProperty ? 'property' : 'name'
|
|
38
|
-
let meta = document.querySelector(`meta[${attribute}="${name}"]`)
|
|
39
|
-
|
|
40
|
-
if (!meta) {
|
|
41
|
-
meta = document.createElement('meta')
|
|
42
|
-
meta.setAttribute(attribute, name)
|
|
43
|
-
document.head.appendChild(meta)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
meta.setAttribute('content', content)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Set link tag
|
|
51
|
-
*/
|
|
52
|
-
export function setLink(rel: string, href: string): void {
|
|
53
|
-
if (typeof document === 'undefined')
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
let link = document.querySelector(`link[rel="${rel}"]`) as HTMLLinkElement
|
|
57
|
-
|
|
58
|
-
if (!link) {
|
|
59
|
-
link = document.createElement('link')
|
|
60
|
-
link.setAttribute('rel', rel)
|
|
61
|
-
document.head.appendChild(link)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
link.setAttribute('href', href)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Use SEO meta tags
|
|
69
|
-
*/
|
|
70
|
-
export function useSEO(options: SEOMetaOptions): void {
|
|
71
|
-
const {
|
|
72
|
-
title,
|
|
73
|
-
description,
|
|
74
|
-
author,
|
|
75
|
-
keywords,
|
|
76
|
-
ogImage,
|
|
77
|
-
ogImageWidth,
|
|
78
|
-
ogImageHeight,
|
|
79
|
-
twitterCard = 'summary_large_image',
|
|
80
|
-
twitterSite,
|
|
81
|
-
twitterCreator,
|
|
82
|
-
canonical,
|
|
83
|
-
} = options
|
|
84
|
-
|
|
85
|
-
// Set title
|
|
86
|
-
if (title) {
|
|
87
|
-
setTitle(title)
|
|
88
|
-
setMeta('og:title', title, true)
|
|
89
|
-
setMeta('twitter:title', title)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Set description
|
|
93
|
-
if (description) {
|
|
94
|
-
setMeta('description', description)
|
|
95
|
-
setMeta('og:description', description, true)
|
|
96
|
-
setMeta('twitter:description', description)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Set author
|
|
100
|
-
if (author) {
|
|
101
|
-
setMeta('author', author)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Set keywords
|
|
105
|
-
if (keywords && keywords.length > 0) {
|
|
106
|
-
setMeta('keywords', keywords.join(', '))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Set Open Graph image
|
|
110
|
-
if (ogImage) {
|
|
111
|
-
setMeta('og:image', ogImage, true)
|
|
112
|
-
setMeta('twitter:image', ogImage)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (ogImageWidth) {
|
|
116
|
-
setMeta('og:image:width', ogImageWidth, true)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (ogImageHeight) {
|
|
120
|
-
setMeta('og:image:height', ogImageHeight, true)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Set Twitter card
|
|
124
|
-
setMeta('twitter:card', twitterCard)
|
|
125
|
-
|
|
126
|
-
if (twitterSite) {
|
|
127
|
-
setMeta('twitter:site', twitterSite)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (twitterCreator) {
|
|
131
|
-
setMeta('twitter:creator', twitterCreator)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Set canonical URL
|
|
135
|
-
if (canonical) {
|
|
136
|
-
setLink('canonical', canonical)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Set og:type
|
|
140
|
-
setMeta('og:type', 'website', true)
|
|
141
|
-
|
|
142
|
-
// Set og:url from current URL
|
|
143
|
-
if (typeof window !== 'undefined') {
|
|
144
|
-
setMeta('og:url', window.location.href, true)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Generate SEO meta tags as HTML string for server-side rendering
|
|
150
|
-
*/
|
|
151
|
-
export function generateSEOTags(options: SEOMetaOptions): string {
|
|
152
|
-
const tags: string[] = []
|
|
153
|
-
|
|
154
|
-
const {
|
|
155
|
-
title,
|
|
156
|
-
description,
|
|
157
|
-
author,
|
|
158
|
-
keywords,
|
|
159
|
-
ogImage,
|
|
160
|
-
ogImageWidth,
|
|
161
|
-
ogImageHeight,
|
|
162
|
-
twitterCard = 'summary_large_image',
|
|
163
|
-
twitterSite,
|
|
164
|
-
twitterCreator,
|
|
165
|
-
canonical,
|
|
166
|
-
} = options
|
|
167
|
-
|
|
168
|
-
if (title) {
|
|
169
|
-
tags.push(`<title>${escapeHtml(title)}</title>`)
|
|
170
|
-
tags.push(`<meta property="og:title" content="${escapeHtml(title)}" />`)
|
|
171
|
-
tags.push(`<meta name="twitter:title" content="${escapeHtml(title)}" />`)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (description) {
|
|
175
|
-
tags.push(`<meta name="description" content="${escapeHtml(description)}" />`)
|
|
176
|
-
tags.push(`<meta property="og:description" content="${escapeHtml(description)}" />`)
|
|
177
|
-
tags.push(`<meta name="twitter:description" content="${escapeHtml(description)}" />`)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (author) {
|
|
181
|
-
tags.push(`<meta name="author" content="${escapeHtml(author)}" />`)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (keywords && keywords.length > 0) {
|
|
185
|
-
tags.push(`<meta name="keywords" content="${escapeHtml(keywords.join(', '))}" />`)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (ogImage) {
|
|
189
|
-
tags.push(`<meta property="og:image" content="${escapeHtml(ogImage)}" />`)
|
|
190
|
-
tags.push(`<meta name="twitter:image" content="${escapeHtml(ogImage)}" />`)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (ogImageWidth) {
|
|
194
|
-
tags.push(`<meta property="og:image:width" content="${escapeHtml(ogImageWidth)}" />`)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (ogImageHeight) {
|
|
198
|
-
tags.push(`<meta property="og:image:height" content="${escapeHtml(ogImageHeight)}" />`)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
tags.push(`<meta name="twitter:card" content="${escapeHtml(twitterCard)}" />`)
|
|
202
|
-
|
|
203
|
-
if (twitterSite) {
|
|
204
|
-
tags.push(`<meta name="twitter:site" content="${escapeHtml(twitterSite)}" />`)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (twitterCreator) {
|
|
208
|
-
tags.push(`<meta name="twitter:creator" content="${escapeHtml(twitterCreator)}" />`)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (canonical) {
|
|
212
|
-
tags.push(`<link rel="canonical" href="${escapeHtml(canonical)}" />`)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
tags.push('<meta property="og:type" content="website" />')
|
|
216
|
-
|
|
217
|
-
return tags.join('\n')
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Escape HTML special characters
|
|
222
|
-
*/
|
|
223
|
-
function escapeHtml(str: string): string {
|
|
224
|
-
return str
|
|
225
|
-
.replace(/&/g, '&')
|
|
226
|
-
.replace(/</g, '<')
|
|
227
|
-
.replace(/>/g, '>')
|
|
228
|
-
.replace(/"/g, '"')
|
|
229
|
-
.replace(/'/g, ''')
|
|
230
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Detects clicks outside a specified element
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```ts
|
|
6
|
-
* const cleanup = useClickOutside(
|
|
7
|
-
* modalRef,
|
|
8
|
-
* () => closeModal(),
|
|
9
|
-
* { enabled: isOpen, ignore: ['.tooltip'] }
|
|
10
|
-
* )
|
|
11
|
-
* ```
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Options for useClickOutside hook
|
|
16
|
-
*/
|
|
17
|
-
export interface UseClickOutsideOptions {
|
|
18
|
-
/** Whether the hook is enabled */
|
|
19
|
-
enabled?: boolean
|
|
20
|
-
/** CSS selectors to ignore (clicks on these elements won't trigger callback) */
|
|
21
|
-
ignore?: string[]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Hook to detect clicks outside an element
|
|
26
|
-
*
|
|
27
|
-
* @param elementRef - The element to detect clicks outside of
|
|
28
|
-
* @param callback - Function to call when click occurs outside element
|
|
29
|
-
* @param options - Configuration options
|
|
30
|
-
* @returns Cleanup function to remove event listeners
|
|
31
|
-
*/
|
|
32
|
-
export function useClickOutside(
|
|
33
|
-
elementRef: HTMLElement | null,
|
|
34
|
-
callback: (event: MouseEvent | TouchEvent) => void,
|
|
35
|
-
options: UseClickOutsideOptions = {},
|
|
36
|
-
): () => void {
|
|
37
|
-
const { enabled = true, ignore = [] } = options
|
|
38
|
-
|
|
39
|
-
const handleClick = (event: MouseEvent | TouchEvent) => {
|
|
40
|
-
if (!enabled || !elementRef)
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
const target = event.target as Node
|
|
44
|
-
|
|
45
|
-
// Check if click is outside element
|
|
46
|
-
if (!elementRef.contains(target)) {
|
|
47
|
-
// Check if target matches any ignore selectors
|
|
48
|
-
const shouldIgnore = ignore.some((selector) => {
|
|
49
|
-
const elements = document.querySelectorAll(selector)
|
|
50
|
-
return Array.from(elements).some(el => el.contains(target))
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
if (!shouldIgnore) {
|
|
54
|
-
callback(event)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Add event listeners
|
|
60
|
-
if (typeof document !== 'undefined') {
|
|
61
|
-
document.addEventListener('mousedown', handleClick)
|
|
62
|
-
document.addEventListener('touchstart', handleClick)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Return cleanup function
|
|
66
|
-
return () => {
|
|
67
|
-
if (typeof document !== 'undefined') {
|
|
68
|
-
document.removeEventListener('mousedown', handleClick)
|
|
69
|
-
document.removeEventListener('touchstart', handleClick)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Traps focus within a container element (useful for modals, dialogs, dropdowns)
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```ts
|
|
6
|
-
* const { activate, deactivate } = useFocusTrap(modalRef, {
|
|
7
|
-
* initialFocus: firstInputRef,
|
|
8
|
-
* returnFocus: true
|
|
9
|
-
* })
|
|
10
|
-
*
|
|
11
|
-
* // When modal opens
|
|
12
|
-
* activate()
|
|
13
|
-
*
|
|
14
|
-
* // When modal closes
|
|
15
|
-
* deactivate()
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Options for useFocusTrap hook
|
|
21
|
-
*/
|
|
22
|
-
export interface UseFocusTrapOptions {
|
|
23
|
-
/** Whether the focus trap is enabled */
|
|
24
|
-
enabled?: boolean
|
|
25
|
-
/** Element to focus initially (defaults to first focusable element) */
|
|
26
|
-
initialFocus?: HTMLElement | null
|
|
27
|
-
/** Whether to return focus to previously focused element on deactivate */
|
|
28
|
-
returnFocus?: boolean
|
|
29
|
-
/** Fallback element to focus if no focusable elements found */
|
|
30
|
-
fallbackFocus?: HTMLElement | null
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Hook to trap focus within an element
|
|
35
|
-
*
|
|
36
|
-
* @param containerRef - The container element to trap focus within
|
|
37
|
-
* @param options - Configuration options
|
|
38
|
-
* @returns Object with activate and deactivate functions
|
|
39
|
-
*/
|
|
40
|
-
export function useFocusTrap(
|
|
41
|
-
containerRef: HTMLElement | null,
|
|
42
|
-
options: UseFocusTrapOptions = {},
|
|
43
|
-
): { activate: () => void, deactivate: () => void } {
|
|
44
|
-
const {
|
|
45
|
-
enabled = true,
|
|
46
|
-
initialFocus = null,
|
|
47
|
-
returnFocus = true,
|
|
48
|
-
fallbackFocus = null,
|
|
49
|
-
} = options
|
|
50
|
-
|
|
51
|
-
let previouslyFocusedElement: HTMLElement | null = null
|
|
52
|
-
let isActive = false
|
|
53
|
-
|
|
54
|
-
// Get all focusable elements within container
|
|
55
|
-
function getFocusableElements(): HTMLElement[] {
|
|
56
|
-
if (!containerRef)
|
|
57
|
-
return []
|
|
58
|
-
|
|
59
|
-
const focusableSelectors = [
|
|
60
|
-
'a[href]',
|
|
61
|
-
'button:not([disabled])',
|
|
62
|
-
'textarea:not([disabled])',
|
|
63
|
-
'input:not([disabled])',
|
|
64
|
-
'select:not([disabled])',
|
|
65
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
66
|
-
].join(',')
|
|
67
|
-
|
|
68
|
-
return Array.from(containerRef.querySelectorAll(focusableSelectors))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Handle tab key
|
|
72
|
-
function handleTab(event: KeyboardEvent) {
|
|
73
|
-
if (!enabled || !isActive || !containerRef)
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
const focusableElements = getFocusableElements()
|
|
77
|
-
|
|
78
|
-
if (focusableElements.length === 0) {
|
|
79
|
-
event.preventDefault()
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const firstElement = focusableElements[0]
|
|
84
|
-
const lastElement = focusableElements[focusableElements.length - 1]
|
|
85
|
-
|
|
86
|
-
if (event.shiftKey) {
|
|
87
|
-
// Shift + Tab
|
|
88
|
-
if (document.activeElement === firstElement || document.activeElement === containerRef) {
|
|
89
|
-
event.preventDefault()
|
|
90
|
-
lastElement.focus()
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
// Tab
|
|
95
|
-
if (document.activeElement === lastElement) {
|
|
96
|
-
event.preventDefault()
|
|
97
|
-
firstElement.focus()
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Handle escape key
|
|
103
|
-
function handleEscape(event: KeyboardEvent) {
|
|
104
|
-
if (event.key === 'Escape' && enabled && isActive) {
|
|
105
|
-
deactivate()
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Activate focus trap
|
|
110
|
-
function activate() {
|
|
111
|
-
if (!enabled || !containerRef || isActive)
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
// Store previously focused element
|
|
115
|
-
previouslyFocusedElement = document.activeElement as HTMLElement
|
|
116
|
-
|
|
117
|
-
// Focus initial element
|
|
118
|
-
if (initialFocus) {
|
|
119
|
-
initialFocus.focus()
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
const focusableElements = getFocusableElements()
|
|
123
|
-
if (focusableElements.length > 0) {
|
|
124
|
-
focusableElements[0].focus()
|
|
125
|
-
}
|
|
126
|
-
else if (fallbackFocus) {
|
|
127
|
-
fallbackFocus.focus()
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
containerRef.focus()
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Add event listeners
|
|
135
|
-
document.addEventListener('keydown', handleTab)
|
|
136
|
-
document.addEventListener('keydown', handleEscape)
|
|
137
|
-
|
|
138
|
-
isActive = true
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Deactivate focus trap
|
|
142
|
-
function deactivate() {
|
|
143
|
-
if (!isActive)
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
// Remove event listeners
|
|
147
|
-
document.removeEventListener('keydown', handleTab)
|
|
148
|
-
document.removeEventListener('keydown', handleEscape)
|
|
149
|
-
|
|
150
|
-
// Return focus
|
|
151
|
-
if (returnFocus && previouslyFocusedElement) {
|
|
152
|
-
previouslyFocusedElement.focus()
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
isActive = false
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return { activate, deactivate }
|
|
159
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook for keyboard event handling with support for shortcuts and modifiers
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```ts
|
|
6
|
-
* useKeyboard([
|
|
7
|
-
* { key: 'Escape', handler: () => closeModal() },
|
|
8
|
-
* { key: 's', ctrl: true, handler: () => save() },
|
|
9
|
-
* { key: ['ArrowUp', 'ArrowDown'], handler: (e) => navigate(e.key) }
|
|
10
|
-
* ])
|
|
11
|
-
* ```
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Options for useKeyboard hook
|
|
16
|
-
*/
|
|
17
|
-
export interface UseKeyboardOptions {
|
|
18
|
-
/** Whether the hook is enabled */
|
|
19
|
-
enabled?: boolean
|
|
20
|
-
/** Whether to prevent default browser behavior */
|
|
21
|
-
preventDefault?: boolean
|
|
22
|
-
/** Whether to stop event propagation */
|
|
23
|
-
stopPropagation?: boolean
|
|
24
|
-
/** Target element for event listener (defaults to document) */
|
|
25
|
-
target?: HTMLElement | Document | Window | null
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Keyboard event handler configuration
|
|
30
|
-
*/
|
|
31
|
-
export interface KeyboardHandler {
|
|
32
|
-
/** Key or keys to listen for */
|
|
33
|
-
key: string | string[]
|
|
34
|
-
/** Handler function to execute */
|
|
35
|
-
handler: (event: KeyboardEvent) => void
|
|
36
|
-
/** Whether Ctrl key must be pressed */
|
|
37
|
-
ctrl?: boolean
|
|
38
|
-
/** Whether Shift key must be pressed */
|
|
39
|
-
shift?: boolean
|
|
40
|
-
/** Whether Alt key must be pressed */
|
|
41
|
-
alt?: boolean
|
|
42
|
-
/** Whether Meta (Command on Mac) key must be pressed */
|
|
43
|
-
meta?: boolean
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Hook for keyboard event handling
|
|
48
|
-
*
|
|
49
|
-
* @param handlers - Single handler or array of keyboard handlers
|
|
50
|
-
* @param options - Configuration options
|
|
51
|
-
* @returns Cleanup function to remove event listeners
|
|
52
|
-
*/
|
|
53
|
-
export function useKeyboard(
|
|
54
|
-
handlers: KeyboardHandler | KeyboardHandler[],
|
|
55
|
-
options: UseKeyboardOptions = {},
|
|
56
|
-
): () => void {
|
|
57
|
-
const {
|
|
58
|
-
enabled = true,
|
|
59
|
-
preventDefault = true,
|
|
60
|
-
stopPropagation = false,
|
|
61
|
-
target = typeof document !== 'undefined' ? document : null,
|
|
62
|
-
} = options
|
|
63
|
-
|
|
64
|
-
const handlerArray = Array.isArray(handlers) ? handlers : [handlers]
|
|
65
|
-
|
|
66
|
-
const handleKeydown = (event: KeyboardEvent) => {
|
|
67
|
-
if (!enabled)
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
handlerArray.forEach((handler) => {
|
|
71
|
-
const keys = Array.isArray(handler.key) ? handler.key : [handler.key]
|
|
72
|
-
const keyMatches = keys.some(k => k.toLowerCase() === event.key.toLowerCase())
|
|
73
|
-
|
|
74
|
-
if (!keyMatches)
|
|
75
|
-
return
|
|
76
|
-
|
|
77
|
-
// Check modifier keys
|
|
78
|
-
if (handler.ctrl !== undefined && handler.ctrl !== event.ctrlKey)
|
|
79
|
-
return
|
|
80
|
-
if (handler.shift !== undefined && handler.shift !== event.shiftKey)
|
|
81
|
-
return
|
|
82
|
-
if (handler.alt !== undefined && handler.alt !== event.altKey)
|
|
83
|
-
return
|
|
84
|
-
if (handler.meta !== undefined && handler.meta !== event.metaKey)
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
// Execute handler
|
|
88
|
-
if (preventDefault)
|
|
89
|
-
event.preventDefault()
|
|
90
|
-
if (stopPropagation)
|
|
91
|
-
event.stopPropagation()
|
|
92
|
-
|
|
93
|
-
handler.handler(event)
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Add event listener
|
|
98
|
-
if (target) {
|
|
99
|
-
target.addEventListener('keydown', handleKeydown as EventListener)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Return cleanup function
|
|
103
|
-
return () => {
|
|
104
|
-
if (target) {
|
|
105
|
-
target.removeEventListener('keydown', handleKeydown as EventListener)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Common keyboard shortcuts
|
|
111
|
-
export const KeyboardShortcuts = {
|
|
112
|
-
ESCAPE: 'Escape',
|
|
113
|
-
ENTER: 'Enter',
|
|
114
|
-
SPACE: ' ',
|
|
115
|
-
TAB: 'Tab',
|
|
116
|
-
ARROW_UP: 'ArrowUp',
|
|
117
|
-
ARROW_DOWN: 'ArrowDown',
|
|
118
|
-
ARROW_LEFT: 'ArrowLeft',
|
|
119
|
-
ARROW_RIGHT: 'ArrowRight',
|
|
120
|
-
HOME: 'Home',
|
|
121
|
-
END: 'End',
|
|
122
|
-
PAGE_UP: 'PageUp',
|
|
123
|
-
PAGE_DOWN: 'PageDown',
|
|
124
|
-
} as const
|