@refraction-ui/astro 0.3.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/dist/astro-accordion/Button.astro +55 -0
  2. package/dist/astro-accordion/index.ts +1 -0
  3. package/dist/astro-ai/AIScript.astro +69 -0
  4. package/dist/astro-ai/TTSScript.astro +60 -0
  5. package/dist/astro-ai/index.ts +16 -0
  6. package/dist/astro-animated-text/AnimatedText.astro +81 -0
  7. package/dist/astro-animated-text/TypewriterText.astro +90 -0
  8. package/dist/astro-animated-text/index.ts +1 -0
  9. package/dist/astro-app-shell/AppShell.astro +195 -0
  10. package/dist/astro-app-shell/AppShellContent.astro +29 -0
  11. package/dist/astro-app-shell/AppShellHeader.astro +54 -0
  12. package/dist/astro-app-shell/AppShellMain.astro +18 -0
  13. package/dist/astro-app-shell/AppShellMobileNav.astro +27 -0
  14. package/dist/astro-app-shell/AppShellOverlay.astro +21 -0
  15. package/dist/astro-app-shell/AppShellSidebar.astro +26 -0
  16. package/dist/astro-app-shell/AuthShell.astro +31 -0
  17. package/dist/astro-app-shell/AuthShellCard.astro +24 -0
  18. package/dist/astro-app-shell/PageShell.astro +36 -0
  19. package/dist/astro-app-shell/PageShellFooter.astro +30 -0
  20. package/dist/astro-app-shell/PageShellNav.astro +33 -0
  21. package/dist/astro-app-shell/PageShellSection.astro +32 -0
  22. package/dist/astro-app-shell/index.ts +21 -0
  23. package/dist/astro-auth/AuthGuard.astro +133 -0
  24. package/dist/astro-auth/index.ts +5 -0
  25. package/dist/astro-avatar/Avatar.astro +22 -0
  26. package/dist/astro-avatar/AvatarFallback.astro +18 -0
  27. package/dist/astro-avatar/AvatarImage.astro +48 -0
  28. package/dist/astro-avatar/index.ts +1 -0
  29. package/dist/astro-avatar-group/AvatarGroup.astro +42 -0
  30. package/dist/astro-avatar-group/index.ts +1 -0
  31. package/dist/astro-badge/Badge.astro +27 -0
  32. package/dist/astro-badge/index.ts +1 -0
  33. package/dist/astro-bottom-nav/BottomNav.astro +40 -0
  34. package/dist/astro-bottom-nav/index.ts +1 -0
  35. package/dist/astro-breadcrumbs/Breadcrumbs.astro +56 -0
  36. package/dist/astro-breadcrumbs/index.ts +1 -0
  37. package/dist/astro-button/Button.astro +55 -0
  38. package/dist/astro-button/index.ts +1 -0
  39. package/dist/astro-calendar/Calendar.astro +259 -0
  40. package/dist/astro-calendar/index.ts +1 -0
  41. package/dist/astro-callout/Callout.astro +3 -0
  42. package/dist/astro-callout/index.ts +1 -0
  43. package/dist/astro-card/Card.astro +20 -0
  44. package/dist/astro-card/CardContent.astro +13 -0
  45. package/dist/astro-card/CardDescription.astro +13 -0
  46. package/dist/astro-card/CardFooter.astro +13 -0
  47. package/dist/astro-card/CardHeader.astro +13 -0
  48. package/dist/astro-card/CardTitle.astro +15 -0
  49. package/dist/astro-card/index.ts +1 -0
  50. package/dist/astro-card-grid/CardGrid.astro +17 -0
  51. package/dist/astro-card-grid/index.ts +1 -0
  52. package/dist/astro-carousel/Button.astro +55 -0
  53. package/dist/astro-carousel/index.ts +1 -0
  54. package/dist/astro-charts/Bars.astro +53 -0
  55. package/dist/astro-charts/Chart.astro +31 -0
  56. package/dist/astro-charts/Circles.astro +48 -0
  57. package/dist/astro-charts/Gradient.astro +34 -0
  58. package/dist/astro-charts/Histogram.astro +107 -0
  59. package/dist/astro-charts/Line.astro +55 -0
  60. package/dist/astro-charts/PieChart.astro +63 -0
  61. package/dist/astro-charts/ScatterPlot.astro +95 -0
  62. package/dist/astro-charts/XAxis.astro +46 -0
  63. package/dist/astro-charts/YAxis.astro +44 -0
  64. package/dist/astro-charts/index.ts +26 -0
  65. package/dist/astro-checkbox/Checkbox.astro +129 -0
  66. package/dist/astro-checkbox/index.ts +1 -0
  67. package/dist/astro-code-block/CodeBlock.astro +3 -0
  68. package/dist/astro-code-block/index.ts +1 -0
  69. package/dist/astro-code-editor/CodeEditor.astro +123 -0
  70. package/dist/astro-code-editor/index.ts +1 -0
  71. package/dist/astro-collapsible/Collapsible.astro +58 -0
  72. package/dist/astro-collapsible/CollapsibleContent.astro +21 -0
  73. package/dist/astro-collapsible/CollapsibleTrigger.astro +17 -0
  74. package/dist/astro-collapsible/index.ts +1 -0
  75. package/dist/astro-command/Command.astro +170 -0
  76. package/dist/astro-command/CommandEmpty.astro +17 -0
  77. package/dist/astro-command/CommandGroup.astro +25 -0
  78. package/dist/astro-command/CommandInput.astro +21 -0
  79. package/dist/astro-command/CommandItem.astro +26 -0
  80. package/dist/astro-command/CommandList.astro +17 -0
  81. package/dist/astro-command/CommandSeparator.astro +14 -0
  82. package/dist/astro-command/index.ts +1 -0
  83. package/dist/astro-command-input/CommandInput.astro +6 -0
  84. package/dist/astro-command-input/index.ts +1 -0
  85. package/dist/astro-content-protection/ContentProtection.astro +104 -0
  86. package/dist/astro-content-protection/index.ts +1 -0
  87. package/dist/astro-data-table/DataTable.astro +263 -0
  88. package/dist/astro-data-table/index.ts +1 -0
  89. package/dist/astro-date-picker/DatePicker.astro +398 -0
  90. package/dist/astro-date-picker/index.ts +1 -0
  91. package/dist/astro-device-frame/DeviceFrame.astro +44 -0
  92. package/dist/astro-device-frame/index.ts +1 -0
  93. package/dist/astro-dialog/Dialog.astro +138 -0
  94. package/dist/astro-dialog/DialogClose.astro +16 -0
  95. package/dist/astro-dialog/DialogContent.astro +24 -0
  96. package/dist/astro-dialog/DialogDescription.astro +15 -0
  97. package/dist/astro-dialog/DialogFooter.astro +14 -0
  98. package/dist/astro-dialog/DialogHeader.astro +14 -0
  99. package/dist/astro-dialog/DialogOverlay.astro +17 -0
  100. package/dist/astro-dialog/DialogTitle.astro +15 -0
  101. package/dist/astro-dialog/DialogTrigger.astro +18 -0
  102. package/dist/astro-dialog/index.ts +1 -0
  103. package/dist/astro-diff-viewer/DiffViewer.astro +371 -0
  104. package/dist/astro-diff-viewer/index.ts +1 -0
  105. package/dist/astro-dropdown-menu/DropdownMenu.astro +138 -0
  106. package/dist/astro-dropdown-menu/DropdownMenuContent.astro +20 -0
  107. package/dist/astro-dropdown-menu/DropdownMenuItem.astro +21 -0
  108. package/dist/astro-dropdown-menu/DropdownMenuLabel.astro +14 -0
  109. package/dist/astro-dropdown-menu/DropdownMenuSeparator.astro +14 -0
  110. package/dist/astro-dropdown-menu/DropdownMenuTrigger.astro +18 -0
  111. package/dist/astro-dropdown-menu/index.ts +1 -0
  112. package/dist/astro-emoji-picker/EmojiPicker.astro +255 -0
  113. package/dist/astro-emoji-picker/index.ts +1 -0
  114. package/dist/astro-feedback-dialog/FeedbackDialog.astro +152 -0
  115. package/dist/astro-feedback-dialog/index.ts +1 -0
  116. package/dist/astro-file-tree/astro-file-tree.astro +4 -0
  117. package/dist/astro-file-tree/index.ts +1 -0
  118. package/dist/astro-file-upload/FileUpload.astro +306 -0
  119. package/dist/astro-file-upload/index.ts +1 -0
  120. package/dist/astro-footer/Footer.astro +61 -0
  121. package/dist/astro-footer/index.ts +1 -0
  122. package/dist/astro-icon-system/astro-icon-system.astro +4 -0
  123. package/dist/astro-icon-system/index.ts +1 -0
  124. package/dist/astro-inline-editor/InlineEditor.astro +169 -0
  125. package/dist/astro-inline-editor/index.ts +1 -0
  126. package/dist/astro-input/Input.astro +49 -0
  127. package/dist/astro-input/index.ts +1 -0
  128. package/dist/astro-input-group/InputGroup.astro +29 -0
  129. package/dist/astro-input-group/InputGroupAddon.astro +14 -0
  130. package/dist/astro-input-group/InputGroupButton.astro +19 -0
  131. package/dist/astro-input-group/InputGroupText.astro +11 -0
  132. package/dist/astro-input-group/index.ts +1 -0
  133. package/dist/astro-install-prompt/InstallPrompt.astro +115 -0
  134. package/dist/astro-install-prompt/index.ts +1 -0
  135. package/dist/astro-keyboard-shortcut/KeyboardShortcut.astro +83 -0
  136. package/dist/astro-keyboard-shortcut/ShortcutBadge.astro +41 -0
  137. package/dist/astro-keyboard-shortcut/index.ts +1 -0
  138. package/dist/astro-language-selector/LanguageSelector.astro +247 -0
  139. package/dist/astro-language-selector/index.ts +1 -0
  140. package/dist/astro-link-card/LinkCard.astro +17 -0
  141. package/dist/astro-link-card/index.ts +1 -0
  142. package/dist/astro-markdown-renderer/MarkdownRenderer.astro +62 -0
  143. package/dist/astro-markdown-renderer/index.ts +1 -0
  144. package/dist/astro-mobile-nav/MobileNav.astro +84 -0
  145. package/dist/astro-mobile-nav/MobileNavContent.astro +29 -0
  146. package/dist/astro-mobile-nav/MobileNavLink.astro +16 -0
  147. package/dist/astro-mobile-nav/MobileNavTrigger.astro +57 -0
  148. package/dist/astro-mobile-nav/index.ts +1 -0
  149. package/dist/astro-navbar/Navbar.astro +49 -0
  150. package/dist/astro-navbar/index.ts +1 -0
  151. package/dist/astro-otp-input/OtpInput.astro +253 -0
  152. package/dist/astro-otp-input/index.ts +1 -0
  153. package/dist/astro-pagination/Pagination.astro +3 -0
  154. package/dist/astro-pagination/index.ts +1 -0
  155. package/dist/astro-payment/Payment.astro +19 -0
  156. package/dist/astro-payment/index.ts +1 -0
  157. package/dist/astro-popover/Popover.astro +87 -0
  158. package/dist/astro-popover/PopoverClose.astro +16 -0
  159. package/dist/astro-popover/PopoverContent.astro +23 -0
  160. package/dist/astro-popover/PopoverTrigger.astro +18 -0
  161. package/dist/astro-popover/index.ts +1 -0
  162. package/dist/astro-presence-indicator/PresenceIndicator.astro +26 -0
  163. package/dist/astro-presence-indicator/index.ts +1 -0
  164. package/dist/astro-progress-display/BadgeDisplay.astro +33 -0
  165. package/dist/astro-progress-display/ProgressBar.astro +27 -0
  166. package/dist/astro-progress-display/StatsGrid.astro +32 -0
  167. package/dist/astro-progress-display/index.ts +1 -0
  168. package/dist/astro-radio/RadioGroup.astro +111 -0
  169. package/dist/astro-radio/RadioItem.astro +36 -0
  170. package/dist/astro-radio/index.ts +1 -0
  171. package/dist/astro-reaction-bar/ReactionBar.astro +117 -0
  172. package/dist/astro-reaction-bar/index.ts +1 -0
  173. package/dist/astro-resizable-layout/ResizableDivider.astro +27 -0
  174. package/dist/astro-resizable-layout/ResizableLayout.astro +162 -0
  175. package/dist/astro-resizable-layout/ResizablePane.astro +28 -0
  176. package/dist/astro-resizable-layout/index.ts +11 -0
  177. package/dist/astro-rich-editor/RichEditor.astro +260 -0
  178. package/dist/astro-rich-editor/index.ts +25 -0
  179. package/dist/astro-search-bar/SearchBar.astro +140 -0
  180. package/dist/astro-search-bar/SearchResultItem.astro +16 -0
  181. package/dist/astro-search-bar/SearchResults.astro +17 -0
  182. package/dist/astro-search-bar/index.ts +1 -0
  183. package/dist/astro-select/Select.astro +165 -0
  184. package/dist/astro-select/SelectContent.astro +19 -0
  185. package/dist/astro-select/SelectItem.astro +42 -0
  186. package/dist/astro-select/SelectTrigger.astro +36 -0
  187. package/dist/astro-select/index.ts +1 -0
  188. package/dist/astro-sidebar/Sidebar.astro +64 -0
  189. package/dist/astro-sidebar/index.ts +1 -0
  190. package/dist/astro-skeleton/Skeleton.astro +39 -0
  191. package/dist/astro-skeleton/SkeletonText.astro +22 -0
  192. package/dist/astro-skeleton/index.ts +1 -0
  193. package/dist/astro-skip-to-content/SkipToContent.astro +3 -0
  194. package/dist/astro-skip-to-content/index.ts +1 -0
  195. package/dist/astro-slide-viewer/SlideViewer.astro +231 -0
  196. package/dist/astro-slide-viewer/index.ts +1 -0
  197. package/dist/astro-slider/index.ts +1 -0
  198. package/dist/astro-status-indicator/StatusIndicator.astro +31 -0
  199. package/dist/astro-status-indicator/index.ts +1 -0
  200. package/dist/astro-steps/Steps.astro +3 -0
  201. package/dist/astro-steps/index.ts +1 -0
  202. package/dist/astro-switch/Switch.astro +89 -0
  203. package/dist/astro-switch/index.ts +1 -0
  204. package/dist/astro-table-of-contents/TableOfContents.astro +53 -0
  205. package/dist/astro-table-of-contents/index.ts +1 -0
  206. package/dist/astro-tabs/Tabs.astro +96 -0
  207. package/dist/astro-tabs/TabsContent.astro +20 -0
  208. package/dist/astro-tabs/TabsList.astro +16 -0
  209. package/dist/astro-tabs/TabsTrigger.astro +24 -0
  210. package/dist/astro-tabs/index.ts +1 -0
  211. package/dist/astro-textarea/Textarea.astro +51 -0
  212. package/dist/astro-textarea/index.ts +1 -0
  213. package/dist/astro-theme/ThemeScript.astro +13 -0
  214. package/dist/astro-theme/ThemeToggle.astro +279 -0
  215. package/dist/astro-theme/index.ts +1 -0
  216. package/dist/astro-thread-view/ThreadView.astro +159 -0
  217. package/dist/astro-thread-view/index.ts +1 -0
  218. package/dist/astro-toast/Toast.astro +49 -0
  219. package/dist/astro-toast/Toaster.astro +142 -0
  220. package/dist/astro-toast/index.ts +1 -0
  221. package/dist/astro-tooltip/Tooltip.astro +95 -0
  222. package/dist/astro-tooltip/TooltipContent.astro +22 -0
  223. package/dist/astro-tooltip/TooltipTrigger.astro +16 -0
  224. package/dist/astro-tooltip/index.ts +1 -0
  225. package/dist/astro-version-selector/VersionSelector.astro +174 -0
  226. package/dist/astro-version-selector/index.ts +1 -0
  227. package/dist/astro-video-player/VideoPlayer.astro +153 -0
  228. package/dist/astro-video-player/index.ts +1 -0
  229. package/dist/index.ts +77 -0
  230. package/dist/shared/a11y.ts +33 -0
  231. package/dist/shared/aria.ts +30 -0
  232. package/dist/shared/cn.ts +35 -0
  233. package/dist/shared/cva.ts +79 -0
  234. package/dist/shared/engine-adapter.ts +10 -0
  235. package/dist/shared/focus-trap.ts +123 -0
  236. package/dist/shared/index.ts +60 -0
  237. package/dist/shared/keyboard.ts +36 -0
  238. package/dist/shared/live-region.ts +90 -0
  239. package/dist/shared/motion.ts +15 -0
  240. package/dist/shared/skip-link.ts +35 -0
  241. package/dist/shared/state-machine.ts +57 -0
  242. package/dist/shared/tokens.ts +16 -0
  243. package/dist/shared/types.ts +45 -0
  244. package/dist/shared/variants.ts +17 -0
  245. package/package.json +22 -20
  246. package/src/index.ts +0 -92
@@ -0,0 +1,115 @@
1
+ ---
2
+ import { installPromptVariants } from '@refraction-ui/install-prompt'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.HTMLAttributes {
6
+ /** Delay in ms before showing. Default: 3000 */
7
+ delay?: number
8
+ /** Storage key for dismissed state. Default: 'rfr-install-dismissed' */
9
+ storageKey?: string
10
+ /** Install button label. Default: 'Install' */
11
+ installLabel?: string
12
+ /** Dismiss button label. Default: 'Dismiss' */
13
+ dismissLabel?: string
14
+ /** Message text. Default: 'Install this app for a better experience' */
15
+ message?: string
16
+ }
17
+
18
+ const {
19
+ delay = 3000,
20
+ storageKey = 'rfr-install-dismissed',
21
+ installLabel = 'Install',
22
+ dismissLabel = 'Dismiss',
23
+ message = 'Install this app for a better experience',
24
+ class: className,
25
+ ...props
26
+ } = Astro.props
27
+
28
+ const classes = cn(installPromptVariants(), className)
29
+ ---
30
+
31
+ <div
32
+ data-rfr-install-prompt
33
+ data-rfr-ip-delay={delay}
34
+ data-rfr-ip-storage-key={storageKey}
35
+ class={classes}
36
+ role="banner"
37
+ aria-label="Install application"
38
+ hidden
39
+ {...props}
40
+ >
41
+ <span data-rfr-ip-message>{message}</span>
42
+ <div>
43
+ <button type="button" data-rfr-ip-install>
44
+ {installLabel}
45
+ </button>
46
+ <button type="button" data-rfr-ip-dismiss>
47
+ {dismissLabel}
48
+ </button>
49
+ </div>
50
+ </div>
51
+
52
+ <script>
53
+ function initInstallPrompts() {
54
+ document.querySelectorAll<HTMLElement>('[data-rfr-install-prompt]').forEach((el) => {
55
+ if (el.hasAttribute('data-rfr-ip-init')) return
56
+ el.setAttribute('data-rfr-ip-init', '')
57
+
58
+ const delay = Number(el.getAttribute('data-rfr-ip-delay') || 3000)
59
+ const storageKey = el.getAttribute('data-rfr-ip-storage-key') || 'rfr-install-dismissed'
60
+
61
+ // Check if already dismissed
62
+ try {
63
+ if (localStorage.getItem(storageKey) === 'true') return
64
+ } catch { /* ignore */ }
65
+
66
+ let promptEvent: { prompt(): void } | null = null
67
+
68
+ const handleBeforeInstall = (e: Event) => {
69
+ e.preventDefault()
70
+ promptEvent = e as unknown as { prompt(): void }
71
+ }
72
+
73
+ window.addEventListener('beforeinstallprompt', handleBeforeInstall)
74
+
75
+ const timer = setTimeout(() => {
76
+ // Check dismissed again in case it changed
77
+ try {
78
+ if (localStorage.getItem(storageKey) === 'true') return
79
+ } catch { /* ignore */ }
80
+
81
+ if (promptEvent) {
82
+ el.hidden = false
83
+ el.dispatchEvent(new CustomEvent('rfr-install-prompt-show', { bubbles: true }))
84
+ }
85
+ }, delay)
86
+
87
+ // Install button
88
+ const installBtn = el.querySelector<HTMLElement>('[data-rfr-ip-install]')
89
+ installBtn?.addEventListener('click', () => {
90
+ promptEvent?.prompt()
91
+ el.hidden = true
92
+ el.dispatchEvent(new CustomEvent('rfr-install-prompt-install', { bubbles: true }))
93
+ })
94
+
95
+ // Dismiss button
96
+ const dismissBtn = el.querySelector<HTMLElement>('[data-rfr-ip-dismiss]')
97
+ dismissBtn?.addEventListener('click', () => {
98
+ el.hidden = true
99
+ try {
100
+ localStorage.setItem(storageKey, 'true')
101
+ } catch { /* ignore */ }
102
+ el.dispatchEvent(new CustomEvent('rfr-install-prompt-dismiss', { bubbles: true }))
103
+ })
104
+
105
+ // Cleanup
106
+ el.addEventListener('astro:before-swap', () => {
107
+ window.removeEventListener('beforeinstallprompt', handleBeforeInstall)
108
+ clearTimeout(timer)
109
+ }, { once: true })
110
+ })
111
+ }
112
+
113
+ initInstallPrompts()
114
+ document.addEventListener('astro:after-swap', initInstallPrompts)
115
+ </script>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,83 @@
1
+ ---
2
+ interface Props {
3
+ /** Key combination (e.g., ['Ctrl', 'K'] or ['Meta', 'Shift', 'P']) */
4
+ keys: string[]
5
+ /** Whether the shortcut is enabled. Default: true */
6
+ enabled?: boolean
7
+ /** Prevent default browser behavior. Default: true */
8
+ preventDefault?: boolean
9
+ }
10
+
11
+ const {
12
+ keys,
13
+ enabled = true,
14
+ preventDefault = true,
15
+ } = Astro.props
16
+ ---
17
+
18
+ <div
19
+ data-rfr-keyboard-shortcut
20
+ data-rfr-ks-keys={JSON.stringify(keys)}
21
+ data-rfr-ks-enabled={enabled}
22
+ data-rfr-ks-prevent-default={preventDefault}
23
+ hidden
24
+ aria-hidden="true"
25
+ ></div>
26
+
27
+ <script>
28
+ function initKeyboardShortcuts() {
29
+ const MODIFIER_KEYS = new Set(['Ctrl', 'Control', 'Alt', 'Shift', 'Meta', 'Cmd', 'Command'])
30
+
31
+ function normalizeKey(key: string): string {
32
+ if (key === 'Command' || key === 'Cmd') return 'Meta'
33
+ if (key === 'Control') return 'Ctrl'
34
+ return key
35
+ }
36
+
37
+ document.querySelectorAll<HTMLElement>('[data-rfr-keyboard-shortcut]').forEach((el) => {
38
+ if (el.hasAttribute('data-rfr-ks-init')) return
39
+ el.setAttribute('data-rfr-ks-init', '')
40
+
41
+ const keys: string[] = JSON.parse(el.getAttribute('data-rfr-ks-keys') || '[]')
42
+ const enabled = el.getAttribute('data-rfr-ks-enabled') !== 'false'
43
+ const preventDefault = el.getAttribute('data-rfr-ks-prevent-default') !== 'false'
44
+
45
+ if (!enabled || keys.length === 0) return
46
+
47
+ const normalizedKeys = keys.map(normalizeKey)
48
+ const modifiers = normalizedKeys.filter((k) => MODIFIER_KEYS.has(k))
49
+ const regularKeys = normalizedKeys.filter((k) => !MODIFIER_KEYS.has(k))
50
+
51
+ function handleKeyDown(e: KeyboardEvent) {
52
+ const ctrlRequired = modifiers.includes('Ctrl')
53
+ const altRequired = modifiers.includes('Alt')
54
+ const shiftRequired = modifiers.includes('Shift')
55
+ const metaRequired = modifiers.includes('Meta')
56
+
57
+ const ctrlMatch = ctrlRequired ? e.ctrlKey : !e.ctrlKey
58
+ const altMatch = altRequired ? e.altKey : !e.altKey
59
+ const shiftMatch = shiftRequired ? e.shiftKey : !e.shiftKey
60
+ const metaMatch = metaRequired ? e.metaKey : !e.metaKey
61
+
62
+ if (!ctrlMatch || !altMatch || !shiftMatch || !metaMatch) return
63
+
64
+ if (regularKeys.length > 0) {
65
+ const eventKey = e.key.length === 1 ? e.key.toUpperCase() : e.key
66
+ const targetKey = regularKeys[0].length === 1 ? regularKeys[0].toUpperCase() : regularKeys[0]
67
+ if (eventKey !== targetKey) return
68
+ }
69
+
70
+ if (preventDefault) {
71
+ e.preventDefault()
72
+ }
73
+
74
+ el.dispatchEvent(new CustomEvent('rfr-shortcut-trigger', { detail: { keys }, bubbles: true }))
75
+ }
76
+
77
+ document.addEventListener('keydown', handleKeyDown)
78
+ })
79
+ }
80
+
81
+ initKeyboardShortcuts()
82
+ document.addEventListener('astro:after-swap', initKeyboardShortcuts)
83
+ </script>
@@ -0,0 +1,41 @@
1
+ ---
2
+ import {
3
+ createKeyboardShortcut,
4
+ formatShortcut,
5
+ shortcutBadgeStyles,
6
+ shortcutKeyStyles,
7
+ shortcutSeparatorStyles,
8
+ } from '@refraction-ui/keyboard-shortcut'
9
+ import { cn } from '../shared/index.ts'
10
+
11
+ interface Props extends astroHTML.JSX.HTMLAttributes {
12
+ /** Key combination to display */
13
+ keys: string[]
14
+ /** Use platform-aware display (Mac symbols). Default: true */
15
+ platform?: boolean
16
+ }
17
+
18
+ const { keys, platform = true, class: className, ...props } = Astro.props
19
+
20
+ const api = createKeyboardShortcut({ keys, onTrigger: () => {}, enabled: false })
21
+ const displayKeys = platform ? api.platformDisplay : api.display
22
+ const isMacDisplay = platform && displayKeys !== api.display
23
+ ---
24
+
25
+ <kbd
26
+ aria-hidden="true"
27
+ role="presentation"
28
+ class={cn(shortcutBadgeStyles, className)}
29
+ {...props}
30
+ >
31
+ {isMacDisplay ? (
32
+ <span>{displayKeys}</span>
33
+ ) : (
34
+ keys.map((key, i) => (
35
+ <>
36
+ {i > 0 && <span class={shortcutSeparatorStyles}>+</span>}
37
+ <span class={shortcutKeyStyles}>{formatShortcut([key], false)}</span>
38
+ </>
39
+ ))
40
+ )}
41
+ </kbd>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,247 @@
1
+ ---
2
+ import {
3
+ createLanguageSelector,
4
+ selectorVariants,
5
+ optionVariants,
6
+ type LanguageOption,
7
+ } from '@refraction-ui/language-selector'
8
+ import { cn } from '../shared/index.ts'
9
+
10
+ interface Props extends astroHTML.JSX.HTMLAttributes {
11
+ value?: string | string[]
12
+ options: LanguageOption[]
13
+ multiple?: boolean
14
+ placeholder?: string
15
+ }
16
+
17
+ const {
18
+ value: controlledValue,
19
+ options,
20
+ multiple = false,
21
+ placeholder = 'Select language...',
22
+ class: className,
23
+ ...props
24
+ } = Astro.props
25
+
26
+ const initialValues = Array.isArray(controlledValue)
27
+ ? controlledValue
28
+ : controlledValue
29
+ ? [controlledValue]
30
+ : []
31
+
32
+ const api = createLanguageSelector({
33
+ value: multiple ? initialValues : initialValues[0],
34
+ options,
35
+ multiple,
36
+ })
37
+
38
+ const displayLabel = initialValues.length > 0
39
+ ? options
40
+ .filter((o) => initialValues.includes(o.value))
41
+ .map((o) => o.label)
42
+ .join(', ')
43
+ : placeholder
44
+
45
+ // Group options
46
+ const groups = new Map<string, LanguageOption[]>()
47
+ const ungrouped: LanguageOption[] = []
48
+ for (const opt of options) {
49
+ if (opt.group) {
50
+ const list = groups.get(opt.group) ?? []
51
+ list.push(opt)
52
+ groups.set(opt.group, list)
53
+ } else {
54
+ ungrouped.push(opt)
55
+ }
56
+ }
57
+ ---
58
+
59
+ <div
60
+ class={cn('rfr-language-selector relative inline-block', className)}
61
+ data-rfr-language-selector
62
+ data-rfr-lang-options={JSON.stringify(options)}
63
+ data-rfr-lang-value={JSON.stringify(initialValues)}
64
+ data-rfr-lang-multiple={String(multiple)}
65
+ data-rfr-lang-placeholder={placeholder}
66
+ {...props}
67
+ >
68
+ <!-- Trigger button -->
69
+ <button
70
+ type="button"
71
+ role={api.triggerProps.role}
72
+ aria-expanded="false"
73
+ aria-controls={api.triggerProps['aria-controls']}
74
+ aria-haspopup={api.triggerProps['aria-haspopup']}
75
+ class={selectorVariants()}
76
+ data-rfr-lang-trigger
77
+ >
78
+ <span data-rfr-lang-label>{displayLabel}</span>
79
+ <span aria-hidden="true" class="ml-2">&#x25BE;</span>
80
+ </button>
81
+
82
+ <!-- Dropdown -->
83
+ <ul
84
+ role={api.contentProps.role}
85
+ id={api.contentProps.id}
86
+ class="absolute top-full left-0 w-full mt-1 rounded-md border bg-popover text-popover-foreground shadow-md z-50 overflow-auto max-h-60"
87
+ style="display:none"
88
+ data-rfr-lang-dropdown
89
+ {...(multiple ? { 'aria-multiselectable': 'true' } : {})}
90
+ >
91
+ {/* Grouped options */}
92
+ {[...groups.entries()].map(([group, opts]) => (
93
+ <li role="presentation">
94
+ <div class="px-3 py-1 text-xs font-semibold text-muted-foreground uppercase">
95
+ {group}
96
+ </div>
97
+ <ul role="group" aria-label={group}>
98
+ {opts.map((opt) => {
99
+ const optProps = api.getOptionProps(opt.value)
100
+ const isSelected = initialValues.includes(opt.value)
101
+ return (
102
+ <li
103
+ role={optProps.role}
104
+ aria-selected={optProps['aria-selected']}
105
+ data-value={opt.value}
106
+ class={optionVariants({ selected: isSelected ? 'true' : 'false' })}
107
+ data-rfr-lang-option
108
+ >
109
+ {isSelected && <span aria-hidden="true">&#x2713;</span>}
110
+ <span>{opt.label}</span>
111
+ </li>
112
+ )
113
+ })}
114
+ </ul>
115
+ </li>
116
+ ))}
117
+
118
+ {/* Ungrouped options */}
119
+ {ungrouped.map((opt) => {
120
+ const optProps = api.getOptionProps(opt.value)
121
+ const isSelected = initialValues.includes(opt.value)
122
+ return (
123
+ <li
124
+ role={optProps.role}
125
+ aria-selected={optProps['aria-selected']}
126
+ data-value={opt.value}
127
+ class={optionVariants({ selected: isSelected ? 'true' : 'false' })}
128
+ data-rfr-lang-option
129
+ >
130
+ {isSelected && <span aria-hidden="true">&#x2713;</span>}
131
+ <span>{opt.label}</span>
132
+ </li>
133
+ )
134
+ })}
135
+ </ul>
136
+ </div>
137
+
138
+ <script>
139
+ document.querySelectorAll<HTMLElement>('[data-rfr-language-selector]').forEach((root) => {
140
+ const allOptions: Array<{ value: string; label: string; group?: string }> = JSON.parse(
141
+ root.getAttribute('data-rfr-lang-options') || '[]',
142
+ )
143
+ const multiple = root.getAttribute('data-rfr-lang-multiple') === 'true'
144
+ const placeholder = root.getAttribute('data-rfr-lang-placeholder') || 'Select language...'
145
+
146
+ const trigger = root.querySelector<HTMLButtonElement>('[data-rfr-lang-trigger]')!
147
+ const dropdown = root.querySelector<HTMLElement>('[data-rfr-lang-dropdown]')!
148
+ const label = root.querySelector<HTMLElement>('[data-rfr-lang-label]')!
149
+ const optionEls = root.querySelectorAll<HTMLElement>('[data-rfr-lang-option]')
150
+
151
+ let isOpen = false
152
+ let selectedValues: string[] = JSON.parse(root.getAttribute('data-rfr-lang-value') || '[]')
153
+
154
+ function setOpen(open: boolean) {
155
+ isOpen = open
156
+ dropdown.style.display = isOpen ? '' : 'none'
157
+ trigger.setAttribute('aria-expanded', String(isOpen))
158
+ }
159
+
160
+ function updateLabel() {
161
+ if (selectedValues.length > 0) {
162
+ label.textContent = allOptions
163
+ .filter((o) => selectedValues.includes(o.value))
164
+ .map((o) => o.label)
165
+ .join(', ')
166
+ } else {
167
+ label.textContent = placeholder
168
+ }
169
+ }
170
+
171
+ function updateOptionStates() {
172
+ optionEls.forEach((optEl) => {
173
+ const val = optEl.getAttribute('data-value') || ''
174
+ const isSel = selectedValues.includes(val)
175
+ optEl.setAttribute('aria-selected', String(isSel))
176
+
177
+ // Toggle selected styling
178
+ if (isSel) {
179
+ optEl.classList.add('bg-accent/50', 'font-medium')
180
+ } else {
181
+ optEl.classList.remove('bg-accent/50', 'font-medium')
182
+ }
183
+
184
+ // Toggle checkmark
185
+ let checkmark = optEl.querySelector('[aria-hidden="true"]')
186
+ if (isSel && !checkmark) {
187
+ checkmark = document.createElement('span')
188
+ checkmark.setAttribute('aria-hidden', 'true')
189
+ checkmark.textContent = '\u2713'
190
+ optEl.insertBefore(checkmark, optEl.firstChild)
191
+ } else if (!isSel && checkmark) {
192
+ checkmark.remove()
193
+ }
194
+ })
195
+ }
196
+
197
+ function toggle(value: string) {
198
+ if (multiple) {
199
+ const index = selectedValues.indexOf(value)
200
+ if (index >= 0) {
201
+ selectedValues.splice(index, 1)
202
+ } else {
203
+ selectedValues.push(value)
204
+ }
205
+ } else {
206
+ selectedValues = [value]
207
+ setOpen(false)
208
+ }
209
+
210
+ root.setAttribute('data-rfr-lang-value', JSON.stringify(selectedValues))
211
+ updateLabel()
212
+ updateOptionStates()
213
+
214
+ const emittedValue = multiple ? [...selectedValues] : selectedValues[0] || ''
215
+ root.dispatchEvent(new CustomEvent('change', { detail: { value: emittedValue }, bubbles: true }))
216
+ }
217
+
218
+ trigger.addEventListener('click', () => setOpen(!isOpen))
219
+
220
+ optionEls.forEach((optEl) => {
221
+ optEl.addEventListener('click', () => {
222
+ const value = optEl.getAttribute('data-value') || ''
223
+ toggle(value)
224
+ })
225
+ })
226
+
227
+ trigger.addEventListener('keydown', (e) => {
228
+ if (e.key === 'Escape') {
229
+ setOpen(false)
230
+ } else if (e.key === 'Enter' || e.key === ' ') {
231
+ e.preventDefault()
232
+ setOpen(!isOpen)
233
+ } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
234
+ if (!isOpen) {
235
+ setOpen(true)
236
+ }
237
+ }
238
+ })
239
+
240
+ // Click outside to close
241
+ document.addEventListener('mousedown', (e) => {
242
+ if (isOpen && !root.contains(e.target as Node)) {
243
+ setOpen(false)
244
+ }
245
+ })
246
+ })
247
+ </script>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,17 @@
1
+ ---
2
+ import { createLinkCard } from '@refraction-ui/link-card'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.AnchorHTMLAttributes {}
6
+
7
+ const { class: className, ...props } = Astro.props
8
+ const api = createLinkCard(props as any)
9
+ ---
10
+
11
+ <a
12
+ class={cn(className)}
13
+ {...api.dataAttributes}
14
+ {...props}
15
+ >
16
+ <slot />
17
+ </a>
@@ -0,0 +1 @@
1
+ export { default as LinkCard } from './LinkCard.astro'
@@ -0,0 +1,62 @@
1
+ ---
2
+ import {
3
+ createMarkdownRenderer,
4
+ proseVariants,
5
+ type MarkdownRendererProps as CoreProps,
6
+ type ComponentDef,
7
+ } from '@refraction-ui/markdown-renderer'
8
+ import { cn } from '../shared/index.ts'
9
+
10
+ interface Props extends astroHTML.JSX.HTMLAttributes {
11
+ content: string
12
+ components?: Record<string, ComponentDef>
13
+ linkResolver?: (url: string) => string
14
+ size?: 'sm' | 'default' | 'lg'
15
+ }
16
+
17
+ const {
18
+ content,
19
+ components,
20
+ linkResolver,
21
+ size,
22
+ class: className,
23
+ ...props
24
+ } = Astro.props
25
+
26
+ const coreProps: CoreProps = { content, components, linkResolver }
27
+ const api = createMarkdownRenderer(coreProps)
28
+ const classes = cn(proseVariants({ size }), className)
29
+
30
+ /**
31
+ * Sanitize HTML to prevent XSS attacks.
32
+ * Strips script tags, on* event attributes, and javascript: URLs.
33
+ */
34
+ function sanitizeHtml(html: string): string {
35
+ let sanitized = html
36
+
37
+ // Remove <script> tags and their contents
38
+ sanitized = sanitized.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
39
+
40
+ // Remove standalone <script> opening/closing tags
41
+ sanitized = sanitized.replace(/<\/?script[^>]*>/gi, '')
42
+
43
+ // Remove on* event handler attributes
44
+ sanitized = sanitized.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
45
+
46
+ // Remove javascript: URLs from href and src attributes
47
+ sanitized = sanitized.replace(/(href|src)\s*=\s*["']?\s*javascript\s*:[^"'>]*/gi, '$1=""')
48
+
49
+ return sanitized
50
+ }
51
+
52
+ const sanitizedHtml = sanitizeHtml(api.html)
53
+ ---
54
+
55
+ <div
56
+ class={classes}
57
+ role={api.ariaProps.role}
58
+ aria-label={api.ariaProps['aria-label']}
59
+ {...props}
60
+ >
61
+ <Fragment set:html={sanitizedHtml} />
62
+ </div>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,84 @@
1
+ ---
2
+ import { mobileNavVariants } from '@refraction-ui/mobile-nav'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.HTMLAttributes {
6
+ /** Whether the nav starts open */
7
+ defaultOpen?: boolean
8
+ }
9
+
10
+ const { defaultOpen = false, class: className, ...props } = Astro.props
11
+ ---
12
+
13
+ <nav
14
+ data-rfr-mobile-nav
15
+ data-rfr-mobile-nav-default-open={defaultOpen}
16
+ data-state={defaultOpen ? 'open' : 'closed'}
17
+ class={cn(mobileNavVariants(), className)}
18
+ {...props}
19
+ >
20
+ <slot />
21
+ </nav>
22
+
23
+ <script>
24
+ function initMobileNavs() {
25
+ document.querySelectorAll<HTMLElement>('[data-rfr-mobile-nav]').forEach((root) => {
26
+ if (root.hasAttribute('data-rfr-mobile-nav-init')) return
27
+ root.setAttribute('data-rfr-mobile-nav-init', '')
28
+
29
+ const defaultOpen = root.getAttribute('data-rfr-mobile-nav-default-open') === 'true'
30
+ let isOpen = defaultOpen
31
+
32
+ const trigger = root.querySelector<HTMLElement>('[data-rfr-mobile-nav-trigger]')
33
+ const content = root.querySelector<HTMLElement>('[data-rfr-mobile-nav-content]')
34
+
35
+ function setState(open: boolean) {
36
+ isOpen = open
37
+ const state = open ? 'open' : 'closed'
38
+ root.setAttribute('data-state', state)
39
+ trigger?.setAttribute('aria-expanded', String(open))
40
+ content?.setAttribute('data-state', state)
41
+
42
+ // Update trigger icon
43
+ const openIcon = trigger?.querySelector('[data-rfr-mobile-nav-icon-open]')
44
+ const closedIcon = trigger?.querySelector('[data-rfr-mobile-nav-icon-closed]')
45
+ if (openIcon && closedIcon) {
46
+ if (open) {
47
+ openIcon.removeAttribute('hidden')
48
+ closedIcon.setAttribute('hidden', '')
49
+ } else {
50
+ openIcon.setAttribute('hidden', '')
51
+ closedIcon.removeAttribute('hidden')
52
+ }
53
+ }
54
+
55
+ root.dispatchEvent(new CustomEvent('rfr-mobile-nav-change', { detail: { open }, bubbles: true }))
56
+ }
57
+
58
+ // Trigger click
59
+ trigger?.addEventListener('click', () => setState(!isOpen))
60
+
61
+ // Escape key
62
+ root.addEventListener('keydown', (e) => {
63
+ if (!isOpen) return
64
+ if (e.key === 'Escape') {
65
+ e.preventDefault()
66
+ setState(false)
67
+ trigger?.focus()
68
+ }
69
+ })
70
+
71
+ // Set initial state for icon visibility
72
+ if (!defaultOpen) {
73
+ const openIcon = trigger?.querySelector('[data-rfr-mobile-nav-icon-open]')
74
+ openIcon?.setAttribute('hidden', '')
75
+ } else {
76
+ const closedIcon = trigger?.querySelector('[data-rfr-mobile-nav-icon-closed]')
77
+ closedIcon?.setAttribute('hidden', '')
78
+ }
79
+ })
80
+ }
81
+
82
+ initMobileNavs()
83
+ document.addEventListener('astro:after-swap', initMobileNavs)
84
+ </script>
@@ -0,0 +1,29 @@
1
+ ---
2
+ import { cn } from '../shared/index.ts'
3
+
4
+ interface Props extends astroHTML.JSX.HTMLAttributes {}
5
+
6
+ const { class: className, ...props } = Astro.props
7
+ ---
8
+
9
+ <div
10
+ data-rfr-mobile-nav-content
11
+ data-state="closed"
12
+ role="menu"
13
+ class={cn('w-full overflow-hidden border-b bg-background transition-all duration-200', className)}
14
+ {...props}
15
+ >
16
+ <slot />
17
+ </div>
18
+
19
+ <style>
20
+ [data-rfr-mobile-nav-content][data-state="open"] {
21
+ max-height: 100vh;
22
+ opacity: 1;
23
+ }
24
+ [data-rfr-mobile-nav-content][data-state="closed"] {
25
+ max-height: 0;
26
+ opacity: 0;
27
+ pointer-events: none;
28
+ }
29
+ </style>