@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,260 @@
1
+ ---
2
+ /**
3
+ * RichEditor — rich text editor component using a contenteditable div + toolbar.
4
+ *
5
+ * Uses the headless @refraction-ui/rich-editor core for document model reference.
6
+ * Renders a contenteditable div with a formatting toolbar, keyboard shortcuts,
7
+ * and optional character/word count display.
8
+ */
9
+ import { cn } from '../shared/index.ts'
10
+
11
+ interface Props extends astroHTML.JSX.HTMLAttributes {
12
+ /** Initial content (plain text or HTML) */
13
+ content?: string
14
+ /** Placeholder text shown when empty */
15
+ placeholder?: string
16
+ /** Read-only mode */
17
+ readOnly?: boolean
18
+ /** Maximum character count */
19
+ maxLength?: number
20
+ /** Show toolbar (default true) */
21
+ showToolbar?: boolean
22
+ /** Show character/word count (default false) */
23
+ showCount?: boolean
24
+ /** Hidden input name for form submission */
25
+ name?: string
26
+ }
27
+
28
+ const {
29
+ content = '',
30
+ placeholder = 'Start typing...',
31
+ readOnly = false,
32
+ maxLength,
33
+ showToolbar = true,
34
+ showCount = false,
35
+ name,
36
+ class: className,
37
+ ...props
38
+ } = Astro.props
39
+
40
+ const configJSON = JSON.stringify({
41
+ placeholder,
42
+ readOnly,
43
+ maxLength,
44
+ showCount,
45
+ })
46
+ ---
47
+
48
+ <div
49
+ class={cn(
50
+ 'rfr-rich-editor rounded-md border bg-background overflow-hidden',
51
+ readOnly && 'opacity-75',
52
+ className,
53
+ )}
54
+ data-rfr-rich-editor
55
+ data-rfr-rich-editor-config={configJSON}
56
+ {...props}
57
+ >
58
+ {showToolbar && !readOnly && (
59
+ <div
60
+ class="flex flex-wrap items-center gap-1 border-b px-2 py-1 bg-muted/50"
61
+ role="toolbar"
62
+ aria-label="Formatting options"
63
+ data-rfr-editor-toolbar
64
+ >
65
+ <button type="button" class="inline-flex items-center justify-center rounded px-2 py-1 text-sm font-bold hover:bg-muted" data-rfr-editor-action="bold" aria-label="Bold" title="Bold (Ctrl+B)">B</button>
66
+ <button type="button" class="inline-flex items-center justify-center rounded px-2 py-1 text-sm italic hover:bg-muted" data-rfr-editor-action="italic" aria-label="Italic" title="Italic (Ctrl+I)"><em>I</em></button>
67
+ <button type="button" class="inline-flex items-center justify-center rounded px-2 py-1 text-sm line-through hover:bg-muted" data-rfr-editor-action="strikethrough" aria-label="Strikethrough" title="Strikethrough"><s>S</s></button>
68
+ <button type="button" class="inline-flex items-center justify-center rounded px-2 py-1 text-sm font-mono hover:bg-muted" data-rfr-editor-action="code" aria-label="Code" title="Inline code">&lt;/&gt;</button>
69
+ <span class="mx-1 h-4 w-px bg-border" aria-hidden="true"></span>
70
+ <button type="button" class="inline-flex items-center justify-center rounded px-2 py-1 text-sm font-bold hover:bg-muted" data-rfr-editor-action="heading" aria-label="Heading" title="Heading">H</button>
71
+ <button type="button" class="inline-flex items-center justify-center rounded px-2 py-1 text-sm hover:bg-muted" data-rfr-editor-action="blockquote" aria-label="Blockquote" title="Blockquote">&ldquo;</button>
72
+ <button type="button" class="inline-flex items-center justify-center rounded px-2 py-1 text-sm hover:bg-muted" data-rfr-editor-action="list-item" aria-label="List" title="Bullet list">&bull;</button>
73
+ <span class="mx-1 h-4 w-px bg-border" aria-hidden="true"></span>
74
+ <button type="button" class="inline-flex items-center justify-center rounded px-2 py-1 text-sm hover:bg-muted" data-rfr-editor-action="undo" aria-label="Undo" title="Undo (Ctrl+Z)">&#8630;</button>
75
+ <button type="button" class="inline-flex items-center justify-center rounded px-2 py-1 text-sm hover:bg-muted" data-rfr-editor-action="redo" aria-label="Redo" title="Redo (Ctrl+Y)">&#8631;</button>
76
+ </div>
77
+ )}
78
+
79
+ <div
80
+ class={cn(
81
+ 'min-h-[8rem] px-3 py-2 text-sm prose prose-sm dark:prose-invert max-w-none focus:outline-none',
82
+ readOnly && 'cursor-default',
83
+ )}
84
+ contenteditable={!readOnly ? 'true' : 'false'}
85
+ role="textbox"
86
+ aria-multiline="true"
87
+ aria-placeholder={placeholder}
88
+ aria-readonly={readOnly ? 'true' : undefined}
89
+ data-rfr-editor-content
90
+ data-placeholder={placeholder}
91
+ >
92
+ <Fragment set:html={content} />
93
+ </div>
94
+
95
+ {showCount && (
96
+ <div
97
+ class="flex justify-end gap-3 border-t px-3 py-1 text-xs text-muted-foreground"
98
+ data-rfr-editor-count
99
+ >
100
+ <span data-rfr-editor-char-count>0 chars</span>
101
+ <span data-rfr-editor-word-count>0 words</span>
102
+ </div>
103
+ )}
104
+
105
+ {name && <input type="hidden" name={name} value={content} data-rfr-editor-input />}
106
+ </div>
107
+
108
+ <style>
109
+ [data-rfr-editor-content]:empty::before {
110
+ content: attr(data-placeholder);
111
+ color: var(--muted-foreground, #9ca3af);
112
+ pointer-events: none;
113
+ }
114
+ [data-rfr-editor-content] {
115
+ white-space: pre-wrap;
116
+ word-wrap: break-word;
117
+ }
118
+ </style>
119
+
120
+ <script>
121
+ document.querySelectorAll('[data-rfr-rich-editor]').forEach((editor) => {
122
+ const contentEl = editor.querySelector('[data-rfr-editor-content]') as HTMLElement
123
+ const toolbar = editor.querySelector('[data-rfr-editor-toolbar]')
124
+ const hiddenInput = editor.querySelector('[data-rfr-editor-input]') as HTMLInputElement | null
125
+ const charCountEl = editor.querySelector('[data-rfr-editor-char-count]')
126
+ const wordCountEl = editor.querySelector('[data-rfr-editor-word-count]')
127
+
128
+ if (!contentEl) return
129
+
130
+ let config: Record<string, unknown> = {}
131
+ try { config = JSON.parse(editor.getAttribute('data-rfr-rich-editor-config') || '{}') } catch { /* */ }
132
+
133
+ function updateCounts() {
134
+ if (!charCountEl && !wordCountEl) return
135
+ const text = contentEl.innerText || ''
136
+ if (charCountEl) charCountEl.textContent = `${text.length} chars`
137
+ if (wordCountEl) {
138
+ const trimmed = text.trim()
139
+ const wc = trimmed === '' ? 0 : trimmed.split(/\s+/).length
140
+ wordCountEl.textContent = `${wc} words`
141
+ }
142
+ }
143
+
144
+ function enforceMaxLength() {
145
+ const maxLen = config.maxLength as number | undefined
146
+ if (!maxLen) return
147
+ const text = contentEl.innerText || ''
148
+ if (text.length > maxLen) {
149
+ contentEl.innerText = text.slice(0, maxLen)
150
+ const range = document.createRange()
151
+ const sel = window.getSelection()
152
+ range.selectNodeContents(contentEl)
153
+ range.collapse(false)
154
+ sel?.removeAllRanges()
155
+ sel?.addRange(range)
156
+ }
157
+ }
158
+
159
+ function syncHiddenInput() {
160
+ if (hiddenInput) hiddenInput.value = contentEl.innerHTML
161
+ }
162
+
163
+ contentEl.addEventListener('input', () => {
164
+ enforceMaxLength()
165
+ updateCounts()
166
+ syncHiddenInput()
167
+ editor.dispatchEvent(new CustomEvent('rfr:editor:change', {
168
+ detail: { html: contentEl.innerHTML, text: contentEl.innerText || '' },
169
+ bubbles: true,
170
+ }))
171
+ })
172
+
173
+ // Toolbar actions
174
+ toolbar?.addEventListener('click', (e) => {
175
+ const btn = (e.target as HTMLElement).closest('[data-rfr-editor-action]')
176
+ if (!btn) return
177
+
178
+ const action = btn.getAttribute('data-rfr-editor-action')
179
+ contentEl.focus()
180
+
181
+ switch (action) {
182
+ case 'bold':
183
+ document.execCommand('bold')
184
+ break
185
+ case 'italic':
186
+ document.execCommand('italic')
187
+ break
188
+ case 'strikethrough':
189
+ document.execCommand('strikeThrough')
190
+ break
191
+ case 'code': {
192
+ const sel = window.getSelection()
193
+ if (sel && sel.rangeCount > 0 && !sel.isCollapsed) {
194
+ const range = sel.getRangeAt(0)
195
+ const code = document.createElement('code')
196
+ code.className = 'bg-muted px-1 rounded text-sm font-mono'
197
+ try { range.surroundContents(code) } catch { /* cross-node selection */ }
198
+ } else {
199
+ document.execCommand('formatBlock', false, 'pre')
200
+ }
201
+ break
202
+ }
203
+ case 'heading':
204
+ document.execCommand('formatBlock', false, 'h2')
205
+ break
206
+ case 'blockquote':
207
+ document.execCommand('formatBlock', false, 'blockquote')
208
+ break
209
+ case 'list-item':
210
+ document.execCommand('insertUnorderedList')
211
+ break
212
+ case 'undo':
213
+ document.execCommand('undo')
214
+ break
215
+ case 'redo':
216
+ document.execCommand('redo')
217
+ break
218
+ }
219
+
220
+ updateCounts()
221
+ syncHiddenInput()
222
+ })
223
+
224
+ // Keyboard shortcuts
225
+ contentEl.addEventListener('keydown', (e) => {
226
+ if (config.readOnly) {
227
+ e.preventDefault()
228
+ return
229
+ }
230
+ const mod = e.ctrlKey || e.metaKey
231
+ if (mod && e.key === 'b') {
232
+ e.preventDefault()
233
+ document.execCommand('bold')
234
+ } else if (mod && e.key === 'i') {
235
+ e.preventDefault()
236
+ document.execCommand('italic')
237
+ } else if (mod && e.key === 'z') {
238
+ e.preventDefault()
239
+ if (e.shiftKey) {
240
+ document.execCommand('redo')
241
+ } else {
242
+ document.execCommand('undo')
243
+ }
244
+ } else if (mod && e.key === 'y') {
245
+ e.preventDefault()
246
+ document.execCommand('redo')
247
+ }
248
+ })
249
+
250
+ // Expose API on element for external scripts
251
+ ;(editor as any).__rfr_editor = {
252
+ getHTML: () => contentEl.innerHTML,
253
+ getText: () => contentEl.innerText || '',
254
+ setContent: (html: string) => { contentEl.innerHTML = html; updateCounts(); syncHiddenInput() },
255
+ clear: () => { contentEl.innerHTML = ''; updateCounts(); syncHiddenInput() },
256
+ }
257
+
258
+ updateCounts()
259
+ })
260
+ </script>
@@ -0,0 +1,25 @@
1
+ export { default as RichEditor } from './RichEditor.astro'
2
+
3
+ // Re-export core types and utilities
4
+ export type {
5
+ BlockType,
6
+ HeadingLevel,
7
+ MarkType,
8
+ Mark,
9
+ Block,
10
+ Document,
11
+ EditorConfig,
12
+ EditorAPI,
13
+ EditorState,
14
+ } from '@refraction-ui/rich-editor'
15
+
16
+ export {
17
+ createEditor,
18
+ createDocument,
19
+ createBlock,
20
+ toMarkdown,
21
+ fromMarkdown,
22
+ toHTML,
23
+ fromHTML,
24
+ toPlainText,
25
+ } from '@refraction-ui/rich-editor'
@@ -0,0 +1,140 @@
1
+ ---
2
+ import {
3
+ createSearchBar,
4
+ searchBarVariants,
5
+ } from '@refraction-ui/search-bar'
6
+ import { cn } from '../shared/index.ts'
7
+
8
+ interface Props extends astroHTML.JSX.HTMLAttributes {
9
+ placeholder?: string
10
+ loading?: boolean
11
+ debounceMs?: number
12
+ }
13
+
14
+ const {
15
+ placeholder = 'Search...',
16
+ loading = false,
17
+ debounceMs = 300,
18
+ class: className,
19
+ ...props
20
+ } = Astro.props
21
+
22
+ const api = createSearchBar({ placeholder, loading, debounceMs })
23
+ ---
24
+
25
+ <div
26
+ data-rfr-search-bar
27
+ data-rfr-search-debounce={String(debounceMs)}
28
+ class={cn(searchBarVariants(), className)}
29
+ {...props}
30
+ >
31
+ <span class="rfr-search-icon" aria-hidden="true">&#x1F50D;</span>
32
+ <input
33
+ role={api.inputProps.role}
34
+ aria-expanded={api.inputProps['aria-expanded']}
35
+ aria-controls={api.inputProps['aria-controls']}
36
+ aria-autocomplete={api.inputProps['aria-autocomplete']}
37
+ placeholder={placeholder}
38
+ class="rfr-search-input flex-1 bg-transparent outline-none"
39
+ data-rfr-search-input
40
+ />
41
+ <span
42
+ class="rfr-search-spinner hidden"
43
+ aria-label="Loading"
44
+ data-rfr-search-spinner
45
+ >&#x23F3;</span>
46
+ <button
47
+ type="button"
48
+ class="rfr-search-clear hidden"
49
+ aria-label="Clear search"
50
+ data-rfr-search-clear
51
+ >&#x2715;</button>
52
+ <slot />
53
+ </div>
54
+
55
+ <script>
56
+ document.querySelectorAll<HTMLElement>('[data-rfr-search-bar]').forEach((root) => {
57
+ const input = root.querySelector<HTMLInputElement>('[data-rfr-search-input]')
58
+ const clearBtn = root.querySelector<HTMLButtonElement>('[data-rfr-search-clear]')
59
+ const spinner = root.querySelector<HTMLElement>('[data-rfr-search-spinner]')
60
+ if (!input || !clearBtn || !spinner) return
61
+
62
+ const debounceMs = parseInt(root.getAttribute('data-rfr-search-debounce') || '300', 10)
63
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined
64
+
65
+ function updateUI(value: string, isSearching: boolean) {
66
+ if (isSearching) {
67
+ spinner!.classList.remove('hidden')
68
+ clearBtn!.classList.add('hidden')
69
+ } else if (value.length > 0) {
70
+ spinner!.classList.add('hidden')
71
+ clearBtn!.classList.remove('hidden')
72
+ } else {
73
+ spinner!.classList.add('hidden')
74
+ clearBtn!.classList.add('hidden')
75
+ }
76
+ }
77
+
78
+ function dispatchSearch(value: string) {
79
+ root.dispatchEvent(
80
+ new CustomEvent('rfr-search', { detail: { value }, bubbles: true })
81
+ )
82
+ }
83
+
84
+ function dispatchValueChange(value: string) {
85
+ root.dispatchEvent(
86
+ new CustomEvent('rfr-search-change', { detail: { value }, bubbles: true })
87
+ )
88
+ }
89
+
90
+ function scheduleSearch(value: string) {
91
+ if (debounceTimer !== undefined) {
92
+ clearTimeout(debounceTimer)
93
+ }
94
+
95
+ if (value.length > 0) {
96
+ updateUI(value, true)
97
+ debounceTimer = setTimeout(() => {
98
+ dispatchSearch(value)
99
+ updateUI(value, false)
100
+ }, debounceMs)
101
+ } else {
102
+ updateUI(value, false)
103
+ }
104
+ }
105
+
106
+ function clear() {
107
+ if (debounceTimer !== undefined) {
108
+ clearTimeout(debounceTimer)
109
+ }
110
+ input!.value = ''
111
+ updateUI('', false)
112
+ dispatchValueChange('')
113
+ input!.focus()
114
+ }
115
+
116
+ input.addEventListener('input', () => {
117
+ const value = input.value
118
+ dispatchValueChange(value)
119
+ scheduleSearch(value)
120
+ })
121
+
122
+ input.addEventListener('keydown', (e: KeyboardEvent) => {
123
+ if (e.key === 'Escape') {
124
+ e.preventDefault()
125
+ clear()
126
+ } else if (e.key === 'Enter') {
127
+ e.preventDefault()
128
+ if (debounceTimer !== undefined) {
129
+ clearTimeout(debounceTimer)
130
+ }
131
+ dispatchSearch(input.value)
132
+ updateUI(input.value, false)
133
+ }
134
+ })
135
+
136
+ clearBtn.addEventListener('click', () => {
137
+ clear()
138
+ })
139
+ })
140
+ </script>
@@ -0,0 +1,16 @@
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
+ <li
10
+ role="option"
11
+ data-rfr-search-result-item
12
+ class={cn('px-3 py-2 cursor-pointer hover:bg-accent', className)}
13
+ {...props}
14
+ >
15
+ <slot />
16
+ </li>
@@ -0,0 +1,17 @@
1
+ ---
2
+ import { searchResultVariants } from '@refraction-ui/search-bar'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.HTMLAttributes {}
6
+
7
+ const { class: className, ...props } = Astro.props
8
+ ---
9
+
10
+ <ul
11
+ role="listbox"
12
+ data-rfr-search-results
13
+ class={cn(searchResultVariants(), className)}
14
+ {...props}
15
+ >
16
+ <slot />
17
+ </ul>
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,165 @@
1
+ ---
2
+ import { createSelect } from '@refraction-ui/select'
3
+
4
+ interface Props extends astroHTML.JSX.HTMLAttributes {
5
+ value?: string
6
+ placeholder?: string
7
+ disabled?: boolean
8
+ name?: string
9
+ }
10
+
11
+ const { value, placeholder = 'Select an option', disabled = false, name, class: className, ...props } = Astro.props
12
+ const api = createSelect({ value, disabled, placeholder })
13
+ ---
14
+
15
+ <div
16
+ data-rfr-select
17
+ data-rfr-select-value={value || ''}
18
+ data-rfr-select-disabled={disabled ? '' : undefined}
19
+ data-rfr-select-trigger-id={api.ids.trigger}
20
+ data-rfr-select-content-id={api.ids.content}
21
+ class={className}
22
+ style="position: relative;"
23
+ {...props}
24
+ >
25
+ <slot />
26
+ {name && <input type="hidden" name={name} value={value || ''} data-rfr-select-input />}
27
+ </div>
28
+
29
+ <script>
30
+ document.querySelectorAll<HTMLElement>('[data-rfr-select]').forEach((root) => {
31
+ const triggerId = root.getAttribute('data-rfr-select-trigger-id') || ''
32
+ const contentId = root.getAttribute('data-rfr-select-content-id') || ''
33
+ let isOpen = false
34
+
35
+ function getDisabled() {
36
+ return root.hasAttribute('data-rfr-select-disabled')
37
+ }
38
+
39
+ function setOpen(open: boolean) {
40
+ if (getDisabled()) return
41
+ isOpen = open
42
+ const trigger = root.querySelector('[data-rfr-select-trigger]') as HTMLElement | null
43
+ const content = root.querySelector('[data-rfr-select-content]') as HTMLElement | null
44
+
45
+ if (trigger) {
46
+ trigger.setAttribute('aria-expanded', String(isOpen))
47
+ trigger.setAttribute('data-state', isOpen ? 'open' : 'closed')
48
+ }
49
+ if (content) {
50
+ content.setAttribute('data-state', isOpen ? 'open' : 'closed')
51
+ content.hidden = !isOpen
52
+ }
53
+ }
54
+
55
+ function selectValue(value: string, label?: string) {
56
+ root.setAttribute('data-rfr-select-value', value)
57
+
58
+ // Update trigger display text
59
+ const trigger = root.querySelector('[data-rfr-select-trigger]') as HTMLElement | null
60
+ if (trigger) {
61
+ const textEl = trigger.querySelector('[data-rfr-select-value-text]') as HTMLElement | null
62
+ if (textEl) textEl.textContent = label || value
63
+ }
64
+
65
+ // Update item states
66
+ root.querySelectorAll<HTMLElement>('[data-rfr-select-item]').forEach((item) => {
67
+ const itemValue = item.getAttribute('data-rfr-select-item-value') || ''
68
+ const isSelected = itemValue === value
69
+ item.setAttribute('aria-selected', String(isSelected))
70
+ if (isSelected) {
71
+ item.setAttribute('data-state', 'checked')
72
+ } else {
73
+ item.removeAttribute('data-state')
74
+ }
75
+ // Show/hide check icon
76
+ const check = item.querySelector('[data-rfr-select-check]') as HTMLElement | null
77
+ if (check) {
78
+ check.style.display = isSelected ? '' : 'none'
79
+ }
80
+ })
81
+
82
+ // Update hidden input
83
+ const input = root.querySelector('[data-rfr-select-input]') as HTMLInputElement | null
84
+ if (input) input.value = value
85
+
86
+ root.dispatchEvent(new CustomEvent('change', { detail: { value } }))
87
+ }
88
+
89
+ // Toggle on trigger click
90
+ root.addEventListener('click', (e) => {
91
+ const trigger = (e.target as HTMLElement).closest('[data-rfr-select-trigger]')
92
+ if (trigger) {
93
+ e.stopPropagation()
94
+ setOpen(!isOpen)
95
+ return
96
+ }
97
+
98
+ const item = (e.target as HTMLElement).closest<HTMLElement>('[data-rfr-select-item]')
99
+ if (item && !item.hasAttribute('data-disabled')) {
100
+ const value = item.getAttribute('data-rfr-select-item-value') || ''
101
+ const label = item.textContent?.trim() || value
102
+ selectValue(value, label)
103
+ setOpen(false)
104
+ // Return focus to trigger
105
+ const triggerEl = root.querySelector('[data-rfr-select-trigger]') as HTMLElement | null
106
+ if (triggerEl) triggerEl.focus()
107
+ }
108
+ })
109
+
110
+ // Keyboard handling
111
+ root.addEventListener('keydown', (e) => {
112
+ const trigger = (e.target as HTMLElement).closest('[data-rfr-select-trigger]')
113
+ if (trigger) {
114
+ if (e.key === 'Enter' || e.key === ' ') {
115
+ e.preventDefault()
116
+ setOpen(!isOpen)
117
+ } else if (e.key === 'Escape') {
118
+ setOpen(false)
119
+ } else if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && !isOpen) {
120
+ e.preventDefault()
121
+ setOpen(true)
122
+ }
123
+ return
124
+ }
125
+
126
+ // Keyboard nav within content
127
+ const item = (e.target as HTMLElement).closest<HTMLElement>('[data-rfr-select-item]')
128
+ if (item) {
129
+ const items = Array.from(root.querySelectorAll<HTMLElement>('[data-rfr-select-item]:not([data-disabled])'))
130
+ const currentIndex = items.indexOf(item)
131
+
132
+ if (e.key === 'ArrowDown') {
133
+ e.preventDefault()
134
+ const next = items[(currentIndex + 1) % items.length]
135
+ if (next) next.focus()
136
+ } else if (e.key === 'ArrowUp') {
137
+ e.preventDefault()
138
+ const prev = items[(currentIndex - 1 + items.length) % items.length]
139
+ if (prev) prev.focus()
140
+ } else if (e.key === 'Enter' || e.key === ' ') {
141
+ e.preventDefault()
142
+ if (!item.hasAttribute('data-disabled')) {
143
+ const value = item.getAttribute('data-rfr-select-item-value') || ''
144
+ const label = item.textContent?.trim() || value
145
+ selectValue(value, label)
146
+ setOpen(false)
147
+ const triggerEl = root.querySelector('[data-rfr-select-trigger]') as HTMLElement | null
148
+ if (triggerEl) triggerEl.focus()
149
+ }
150
+ } else if (e.key === 'Escape') {
151
+ setOpen(false)
152
+ const triggerEl = root.querySelector('[data-rfr-select-trigger]') as HTMLElement | null
153
+ if (triggerEl) triggerEl.focus()
154
+ }
155
+ }
156
+ })
157
+
158
+ // Close on outside click
159
+ document.addEventListener('click', (e) => {
160
+ if (isOpen && !root.contains(e.target as Node)) {
161
+ setOpen(false)
162
+ }
163
+ })
164
+ })
165
+ </script>
@@ -0,0 +1,19 @@
1
+ ---
2
+ import { selectContentVariants } from '@refraction-ui/select'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.HTMLAttributes {}
6
+
7
+ const { class: className, ...props } = Astro.props
8
+ ---
9
+
10
+ <div
11
+ role="listbox"
12
+ data-state="closed"
13
+ data-rfr-select-content
14
+ hidden
15
+ class={cn(selectContentVariants(), className)}
16
+ {...props}
17
+ >
18
+ <slot />
19
+ </div>
@@ -0,0 +1,42 @@
1
+ ---
2
+ import { selectItemVariants } from '@refraction-ui/select'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ interface Props extends astroHTML.JSX.HTMLAttributes {
6
+ value: string
7
+ disabled?: boolean
8
+ selected?: boolean
9
+ }
10
+
11
+ const { value, disabled = false, selected = false, class: className, ...props } = Astro.props
12
+ ---
13
+
14
+ <div
15
+ role="option"
16
+ aria-selected={selected}
17
+ aria-disabled={disabled || undefined}
18
+ data-state={selected ? 'checked' : undefined}
19
+ data-disabled={disabled ? '' : undefined}
20
+ tabindex={disabled ? undefined : 0}
21
+ data-rfr-select-item
22
+ data-rfr-select-item-value={value}
23
+ class={cn(selectItemVariants({ selected: selected ? 'true' : 'false' }), className)}
24
+ {...props}
25
+ >
26
+ <span data-rfr-select-check style={selected ? '' : 'display: none'} class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
27
+ <svg
28
+ class="h-4 w-4"
29
+ xmlns="http://www.w3.org/2000/svg"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ stroke-width="2"
34
+ stroke-linecap="round"
35
+ stroke-linejoin="round"
36
+ aria-hidden="true"
37
+ >
38
+ <path d="M20 6L9 17l-5-5" />
39
+ </svg>
40
+ </span>
41
+ <slot />
42
+ </div>