@refraction-ui/astro 0.4.2 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/dist/accordion/index.ts +1 -0
  2. package/dist/ai/ai-manager.ts +121 -0
  3. package/dist/ai/index.ts +23 -0
  4. package/dist/ai/mock-providers.ts +68 -0
  5. package/dist/ai/sanitizer.ts +174 -0
  6. package/dist/ai/tts-manager.ts +80 -0
  7. package/dist/ai/types.ts +72 -0
  8. package/dist/animated-text/animated-text.styles.ts +28 -0
  9. package/dist/animated-text/animated-text.ts +83 -0
  10. package/dist/animated-text/index.ts +15 -0
  11. package/dist/app-shell/app-shell.ts +252 -0
  12. package/dist/app-shell/auth-shell.ts +84 -0
  13. package/dist/app-shell/index.ts +22 -0
  14. package/dist/app-shell/page-shell.ts +125 -0
  15. package/dist/astro-accordion/Button.astro +1 -1
  16. package/dist/astro-ai/index.ts +1 -1
  17. package/dist/astro-animated-text/AnimatedText.astro +1 -1
  18. package/dist/astro-animated-text/TypewriterText.astro +1 -1
  19. package/dist/astro-app-shell/AppShell.astro +1 -1
  20. package/dist/astro-app-shell/AuthShell.astro +1 -1
  21. package/dist/astro-app-shell/AuthShellCard.astro +1 -1
  22. package/dist/astro-app-shell/PageShell.astro +1 -1
  23. package/dist/astro-app-shell/PageShellSection.astro +1 -1
  24. package/dist/astro-app-shell/index.ts +1 -1
  25. package/dist/astro-auth/index.ts +2 -2
  26. package/dist/astro-avatar/Avatar.astro +1 -1
  27. package/dist/astro-avatar/AvatarFallback.astro +1 -1
  28. package/dist/astro-avatar/AvatarImage.astro +1 -1
  29. package/dist/astro-avatar-group/AvatarGroup.astro +1 -1
  30. package/dist/astro-badge/Badge.astro +1 -1
  31. package/dist/astro-bottom-nav/BottomNav.astro +1 -1
  32. package/dist/astro-breadcrumbs/Breadcrumbs.astro +1 -1
  33. package/dist/astro-button/Button.astro +1 -1
  34. package/dist/astro-calendar/Calendar.astro +1 -1
  35. package/dist/astro-card/Card.astro +1 -1
  36. package/dist/astro-card/CardContent.astro +1 -1
  37. package/dist/astro-card/CardDescription.astro +1 -1
  38. package/dist/astro-card/CardFooter.astro +1 -1
  39. package/dist/astro-card/CardHeader.astro +1 -1
  40. package/dist/astro-card/CardTitle.astro +1 -1
  41. package/dist/astro-card-grid/CardGrid.astro +1 -1
  42. package/dist/astro-carousel/Button.astro +1 -1
  43. package/dist/astro-charts/Bars.astro +1 -1
  44. package/dist/astro-charts/Chart.astro +1 -1
  45. package/dist/astro-charts/Circles.astro +1 -1
  46. package/dist/astro-charts/Histogram.astro +1 -1
  47. package/dist/astro-charts/Line.astro +1 -1
  48. package/dist/astro-charts/PieChart.astro +1 -1
  49. package/dist/astro-charts/ScatterPlot.astro +1 -1
  50. package/dist/astro-charts/index.ts +1 -1
  51. package/dist/astro-checkbox/Checkbox.astro +1 -1
  52. package/dist/astro-code-editor/CodeEditor.astro +1 -1
  53. package/dist/astro-collapsible/Collapsible.astro +1 -1
  54. package/dist/astro-collapsible/CollapsibleContent.astro +1 -1
  55. package/dist/astro-command/Command.astro +1 -1
  56. package/dist/astro-command/CommandGroup.astro +1 -1
  57. package/dist/astro-command/CommandInput.astro +1 -1
  58. package/dist/astro-command/CommandItem.astro +1 -1
  59. package/dist/astro-command-input/CommandInput.astro +1 -1
  60. package/dist/astro-content-protection/ContentProtection.astro +1 -1
  61. package/dist/astro-data-table/DataTable.astro +1 -1
  62. package/dist/astro-date-picker/DatePicker.astro +1 -1
  63. package/dist/astro-device-frame/DeviceFrame.astro +1 -1
  64. package/dist/astro-dialog/DialogContent.astro +1 -1
  65. package/dist/astro-dialog/DialogOverlay.astro +1 -1
  66. package/dist/astro-diff-viewer/DiffViewer.astro +1 -1
  67. package/dist/astro-dropdown-menu/DropdownMenuContent.astro +1 -1
  68. package/dist/astro-dropdown-menu/DropdownMenuItem.astro +1 -1
  69. package/dist/astro-emoji-picker/EmojiPicker.astro +1 -1
  70. package/dist/astro-feedback-dialog/FeedbackDialog.astro +1 -1
  71. package/dist/astro-file-upload/FileUpload.astro +1 -1
  72. package/dist/astro-footer/Footer.astro +1 -1
  73. package/dist/astro-inline-editor/InlineEditor.astro +1 -1
  74. package/dist/astro-input/Input.astro +1 -1
  75. package/dist/astro-input-group/InputGroup.astro +1 -1
  76. package/dist/astro-input-group/InputGroupAddon.astro +1 -1
  77. package/dist/astro-input-group/InputGroupButton.astro +1 -1
  78. package/dist/astro-install-prompt/InstallPrompt.astro +1 -1
  79. package/dist/astro-keyboard-shortcut/ShortcutBadge.astro +1 -1
  80. package/dist/astro-language-selector/LanguageSelector.astro +1 -1
  81. package/dist/astro-link-card/LinkCard.astro +1 -1
  82. package/dist/astro-markdown-renderer/MarkdownRenderer.astro +1 -1
  83. package/dist/astro-mobile-nav/MobileNav.astro +1 -1
  84. package/dist/astro-mobile-nav/MobileNavLink.astro +1 -1
  85. package/dist/astro-mobile-nav/MobileNavTrigger.astro +1 -1
  86. package/dist/astro-navbar/Navbar.astro +1 -1
  87. package/dist/astro-otp-input/OtpInput.astro +1 -1
  88. package/dist/astro-payment/Payment.astro +1 -1
  89. package/dist/astro-popover/PopoverContent.astro +1 -1
  90. package/dist/astro-presence-indicator/PresenceIndicator.astro +1 -1
  91. package/dist/astro-progress-display/BadgeDisplay.astro +1 -1
  92. package/dist/astro-progress-display/ProgressBar.astro +1 -1
  93. package/dist/astro-progress-display/StatsGrid.astro +1 -1
  94. package/dist/astro-radio/RadioGroup.astro +1 -1
  95. package/dist/astro-radio/RadioItem.astro +1 -1
  96. package/dist/astro-reaction-bar/ReactionBar.astro +1 -1
  97. package/dist/astro-resizable-layout/ResizableDivider.astro +1 -1
  98. package/dist/astro-resizable-layout/ResizableLayout.astro +1 -1
  99. package/dist/astro-resizable-layout/ResizablePane.astro +1 -1
  100. package/dist/astro-resizable-layout/index.ts +1 -1
  101. package/dist/astro-rich-editor/index.ts +2 -2
  102. package/dist/astro-search-bar/SearchBar.astro +1 -1
  103. package/dist/astro-search-bar/SearchResults.astro +1 -1
  104. package/dist/astro-select/Select.astro +1 -1
  105. package/dist/astro-select/SelectContent.astro +1 -1
  106. package/dist/astro-select/SelectItem.astro +1 -1
  107. package/dist/astro-select/SelectTrigger.astro +1 -1
  108. package/dist/astro-sidebar/Sidebar.astro +1 -1
  109. package/dist/astro-skeleton/Skeleton.astro +1 -1
  110. package/dist/astro-slide-viewer/SlideViewer.astro +1 -1
  111. package/dist/astro-status-indicator/StatusIndicator.astro +1 -1
  112. package/dist/astro-switch/Switch.astro +1 -1
  113. package/dist/astro-table-of-contents/TableOfContents.astro +1 -1
  114. package/dist/astro-tabs/Tabs.astro +1 -1
  115. package/dist/astro-tabs/TabsList.astro +1 -1
  116. package/dist/astro-tabs/TabsTrigger.astro +1 -1
  117. package/dist/astro-textarea/Textarea.astro +1 -1
  118. package/dist/astro-theme/ThemeScript.astro +1 -1
  119. package/dist/astro-thread-view/ThreadView.astro +1 -1
  120. package/dist/astro-toast/Toast.astro +1 -1
  121. package/dist/astro-tooltip/TooltipContent.astro +1 -1
  122. package/dist/astro-version-selector/VersionSelector.astro +1 -1
  123. package/dist/astro-video-player/VideoPlayer.astro +1 -1
  124. package/dist/auth/adapters/mock.ts +74 -0
  125. package/dist/auth/auth-machine.ts +120 -0
  126. package/dist/auth/index.ts +22 -0
  127. package/dist/auth/rbac.ts +44 -0
  128. package/dist/auth/types.ts +44 -0
  129. package/dist/avatar/avatar.styles.ts +47 -0
  130. package/dist/avatar/avatar.ts +73 -0
  131. package/dist/avatar/index.ts +15 -0
  132. package/dist/avatar-group/avatar-group.styles.ts +63 -0
  133. package/dist/avatar-group/avatar-group.ts +106 -0
  134. package/dist/avatar-group/index.ts +18 -0
  135. package/dist/badge/badge.styles.ts +24 -0
  136. package/dist/badge/badge.ts +48 -0
  137. package/dist/badge/index.ts +10 -0
  138. package/dist/bottom-nav/bottom-nav.styles.ts +18 -0
  139. package/dist/bottom-nav/bottom-nav.ts +33 -0
  140. package/dist/bottom-nav/index.ts +2 -0
  141. package/dist/breadcrumbs/breadcrumbs.styles.ts +20 -0
  142. package/dist/breadcrumbs/breadcrumbs.ts +72 -0
  143. package/dist/breadcrumbs/index.ts +8 -0
  144. package/dist/button/button.styles.ts +38 -0
  145. package/dist/button/button.ts +64 -0
  146. package/dist/button/index.ts +10 -0
  147. package/dist/calendar/calendar.styles.ts +21 -0
  148. package/dist/calendar/calendar.ts +202 -0
  149. package/dist/calendar/index.ts +9 -0
  150. package/dist/callout/callout.ts +1 -0
  151. package/dist/callout/index.ts +1 -0
  152. package/dist/card/card.styles.ts +55 -0
  153. package/dist/card/card.ts +45 -0
  154. package/dist/card/index.ts +20 -0
  155. package/dist/card-grid/card-grid.ts +9 -0
  156. package/dist/card-grid/index.ts +1 -0
  157. package/dist/carousel/index.ts +1 -0
  158. package/dist/charts/axis.ts +50 -0
  159. package/dist/charts/chart.ts +73 -0
  160. package/dist/charts/colorblind.ts +100 -0
  161. package/dist/charts/data.ts +72 -0
  162. package/dist/charts/geometry.ts +54 -0
  163. package/dist/charts/index.ts +61 -0
  164. package/dist/charts/scales.ts +151 -0
  165. package/dist/checkbox/checkbox.styles.ts +33 -0
  166. package/dist/checkbox/checkbox.ts +73 -0
  167. package/dist/checkbox/index.ts +10 -0
  168. package/dist/code-block/code-block.ts +1 -0
  169. package/dist/code-block/index.ts +1 -0
  170. package/dist/code-editor/code-editor.styles.ts +32 -0
  171. package/dist/code-editor/code-editor.ts +140 -0
  172. package/dist/code-editor/index.ts +9 -0
  173. package/dist/collapsible/collapsible.styles.ts +14 -0
  174. package/dist/collapsible/collapsible.ts +93 -0
  175. package/dist/collapsible/index.ts +7 -0
  176. package/dist/command/command.styles.ts +27 -0
  177. package/dist/command/command.ts +179 -0
  178. package/dist/command/index.ts +14 -0
  179. package/dist/command-input/command-input.ts +176 -0
  180. package/dist/command-input/index.ts +1 -0
  181. package/dist/content-protection/content-protection.styles.ts +9 -0
  182. package/dist/content-protection/content-protection.ts +68 -0
  183. package/dist/content-protection/index.ts +8 -0
  184. package/dist/data-table/data-table.styles.ts +55 -0
  185. package/dist/data-table/data-table.ts +183 -0
  186. package/dist/data-table/index.ts +15 -0
  187. package/dist/date-picker/date-picker.styles.ts +31 -0
  188. package/dist/date-picker/date-picker.ts +333 -0
  189. package/dist/date-picker/index.ts +18 -0
  190. package/dist/device-frame/device-frame.styles.ts +21 -0
  191. package/dist/device-frame/device-frame.ts +112 -0
  192. package/dist/device-frame/index.ts +10 -0
  193. package/dist/dialog/dialog.styles.ts +8 -0
  194. package/dist/dialog/dialog.ts +133 -0
  195. package/dist/dialog/index.ts +8 -0
  196. package/dist/diff-viewer/diff-viewer.styles.ts +124 -0
  197. package/dist/diff-viewer/diff-viewer.ts +255 -0
  198. package/dist/diff-viewer/index.ts +20 -0
  199. package/dist/dropdown-menu/dropdown-menu.styles.ts +9 -0
  200. package/dist/dropdown-menu/dropdown-menu.ts +139 -0
  201. package/dist/dropdown-menu/index.ts +9 -0
  202. package/dist/emoji-picker/emoji-data.ts +282 -0
  203. package/dist/emoji-picker/emoji-picker.styles.ts +32 -0
  204. package/dist/emoji-picker/emoji-picker.ts +165 -0
  205. package/dist/emoji-picker/index.ts +25 -0
  206. package/dist/feedback-dialog/feedback-dialog.styles.ts +15 -0
  207. package/dist/feedback-dialog/feedback-dialog.ts +147 -0
  208. package/dist/feedback-dialog/index.ts +11 -0
  209. package/dist/file-upload/file-upload.styles.ts +30 -0
  210. package/dist/file-upload/file-upload.ts +260 -0
  211. package/dist/file-upload/index.ts +19 -0
  212. package/dist/footer/footer.styles.ts +5 -0
  213. package/dist/footer/footer.ts +32 -0
  214. package/dist/footer/index.ts +9 -0
  215. package/dist/index.ts +1 -1
  216. package/dist/inline-editor/index.ts +13 -0
  217. package/dist/inline-editor/inline-editor.styles.ts +42 -0
  218. package/dist/inline-editor/inline-editor.ts +96 -0
  219. package/dist/input/index.ts +9 -0
  220. package/dist/input/input.styles.ts +15 -0
  221. package/dist/input/input.ts +84 -0
  222. package/dist/input-group/index.ts +13 -0
  223. package/dist/input-group/input-group.styles.ts +50 -0
  224. package/dist/input-group/input-group.ts +44 -0
  225. package/dist/install-prompt/index.ts +9 -0
  226. package/dist/install-prompt/install-prompt.styles.ts +5 -0
  227. package/dist/install-prompt/install-prompt.ts +68 -0
  228. package/dist/keyboard-shortcut/index.ts +12 -0
  229. package/dist/keyboard-shortcut/keyboard-shortcut.styles.ts +8 -0
  230. package/dist/keyboard-shortcut/keyboard-shortcut.ts +153 -0
  231. package/dist/language-selector/index.ts +8 -0
  232. package/dist/language-selector/language-selector.styles.ts +28 -0
  233. package/dist/language-selector/language-selector.ts +149 -0
  234. package/dist/link-card/index.ts +1 -0
  235. package/dist/link-card/link-card.ts +9 -0
  236. package/dist/markdown-renderer/index.ts +8 -0
  237. package/dist/markdown-renderer/markdown-renderer.styles.ts +31 -0
  238. package/dist/markdown-renderer/markdown-renderer.ts +251 -0
  239. package/dist/mobile-nav/index.ts +14 -0
  240. package/dist/mobile-nav/mobile-nav.styles.ts +39 -0
  241. package/dist/mobile-nav/mobile-nav.ts +74 -0
  242. package/dist/navbar/index.ts +9 -0
  243. package/dist/navbar/navbar.styles.ts +29 -0
  244. package/dist/navbar/navbar.ts +39 -0
  245. package/dist/otp-input/index.ts +13 -0
  246. package/dist/otp-input/otp-input.styles.ts +51 -0
  247. package/dist/otp-input/otp-input.ts +198 -0
  248. package/dist/pagination/index.ts +1 -0
  249. package/dist/payment/index.ts +1 -0
  250. package/dist/payment/payment.ts +16 -0
  251. package/dist/popover/index.ts +7 -0
  252. package/dist/popover/popover.styles.ts +16 -0
  253. package/dist/popover/popover.ts +89 -0
  254. package/dist/presence-indicator/index.ts +14 -0
  255. package/dist/presence-indicator/presence-indicator.styles.ts +29 -0
  256. package/dist/presence-indicator/presence-indicator.ts +63 -0
  257. package/dist/progress-display/index.ts +15 -0
  258. package/dist/progress-display/progress-display.styles.ts +72 -0
  259. package/dist/progress-display/progress-display.ts +60 -0
  260. package/dist/radio/index.ts +8 -0
  261. package/dist/radio/radio.styles.ts +34 -0
  262. package/dist/radio/radio.ts +70 -0
  263. package/dist/reaction-bar/index.ts +14 -0
  264. package/dist/reaction-bar/reaction-bar.styles.ts +23 -0
  265. package/dist/reaction-bar/reaction-bar.ts +87 -0
  266. package/dist/resizable-layout/index.ts +12 -0
  267. package/dist/resizable-layout/resizable-layout.styles.ts +50 -0
  268. package/dist/resizable-layout/resizable-layout.ts +139 -0
  269. package/dist/rich-editor/editor.ts +342 -0
  270. package/dist/rich-editor/emoji.ts +175 -0
  271. package/dist/rich-editor/history.ts +79 -0
  272. package/dist/rich-editor/index.ts +106 -0
  273. package/dist/rich-editor/markdown-shortcuts.ts +248 -0
  274. package/dist/rich-editor/mentions.ts +162 -0
  275. package/dist/rich-editor/model.ts +206 -0
  276. package/dist/rich-editor/operations.ts +772 -0
  277. package/dist/rich-editor/selection.ts +157 -0
  278. package/dist/rich-editor/serialization.ts +711 -0
  279. package/dist/rich-editor/slash-commands.ts +212 -0
  280. package/dist/search-bar/index.ts +7 -0
  281. package/dist/search-bar/search-bar.styles.ts +29 -0
  282. package/dist/search-bar/search-bar.ts +130 -0
  283. package/dist/select/index.ts +13 -0
  284. package/dist/select/select.styles.ts +52 -0
  285. package/dist/select/select.ts +152 -0
  286. package/dist/sidebar/index.ts +9 -0
  287. package/dist/sidebar/sidebar.styles.ts +27 -0
  288. package/dist/sidebar/sidebar.ts +59 -0
  289. package/dist/skeleton/index.ts +8 -0
  290. package/dist/skeleton/skeleton.styles.ts +16 -0
  291. package/dist/skeleton/skeleton.ts +39 -0
  292. package/dist/skip-to-content/index.ts +1 -0
  293. package/dist/skip-to-content/skip-to-content.ts +1 -0
  294. package/dist/slide-viewer/index.ts +16 -0
  295. package/dist/slide-viewer/slide-viewer.styles.ts +57 -0
  296. package/dist/slide-viewer/slide-viewer.ts +152 -0
  297. package/dist/slider/index.ts +1 -0
  298. package/dist/slider/slider.ts +2 -0
  299. package/dist/status-indicator/index.ts +15 -0
  300. package/dist/status-indicator/status-indicator.styles.ts +41 -0
  301. package/dist/status-indicator/status-indicator.ts +66 -0
  302. package/dist/steps/index.ts +1 -0
  303. package/dist/steps/steps.ts +1 -0
  304. package/dist/switch/index.ts +7 -0
  305. package/dist/switch/switch.styles.ts +55 -0
  306. package/dist/switch/switch.ts +63 -0
  307. package/dist/table-of-contents/index.ts +1 -0
  308. package/dist/table-of-contents/table-of-contents.ts +37 -0
  309. package/dist/tabs/index.ts +8 -0
  310. package/dist/tabs/tabs.styles.ts +9 -0
  311. package/dist/tabs/tabs.ts +133 -0
  312. package/dist/textarea/index.ts +7 -0
  313. package/dist/textarea/textarea.styles.ts +15 -0
  314. package/dist/textarea/textarea.ts +59 -0
  315. package/dist/theme/dom-adapters.ts +60 -0
  316. package/dist/theme/index.ts +18 -0
  317. package/dist/theme/theme-machine.ts +130 -0
  318. package/dist/theme/theme-script.ts +18 -0
  319. package/dist/thread-view/index.ts +26 -0
  320. package/dist/thread-view/thread-view.styles.ts +35 -0
  321. package/dist/thread-view/thread-view.ts +176 -0
  322. package/dist/toast/index.ts +13 -0
  323. package/dist/toast/toast.styles.ts +19 -0
  324. package/dist/toast/toast.ts +203 -0
  325. package/dist/tooltip/index.ts +7 -0
  326. package/dist/tooltip/tooltip.styles.ts +16 -0
  327. package/dist/tooltip/tooltip.ts +109 -0
  328. package/dist/version-selector/index.ts +12 -0
  329. package/dist/version-selector/version-selector.styles.ts +41 -0
  330. package/dist/version-selector/version-selector.ts +136 -0
  331. package/dist/video-player/index.ts +12 -0
  332. package/dist/video-player/video-player.styles.ts +42 -0
  333. package/dist/video-player/video-player.ts +100 -0
  334. package/package.json +1 -1
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Slash command system.
3
+ * When user types "/" at the start of a block or after a space, a menu appears
4
+ * with available commands that can be filtered by typing.
5
+ */
6
+
7
+ import type { Document, BlockType } from './model.js'
8
+ import type { Selection } from './selection.js'
9
+ import type { EditorState } from './operations.js'
10
+ import { changeBlockType, insertBlock, deleteSelection } from './operations.js'
11
+ import { createCollapsedSelection, createSelection, createPosition } from './selection.js'
12
+ import { findBlockById, getBlockText, cloneDocument, createTextSegment } from './model.js'
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface SlashCommand {
19
+ name: string
20
+ label: string
21
+ description: string
22
+ icon?: string
23
+ keywords: string[]
24
+ action: (doc: Document, sel: Selection) => EditorState
25
+ }
26
+
27
+ export interface SlashCommandMenu {
28
+ isOpen: boolean
29
+ query: string
30
+ filteredCommands: SlashCommand[]
31
+ selectedIndex: number
32
+ open(query?: string): void
33
+ close(): void
34
+ search(query: string): void
35
+ selectNext(): void
36
+ selectPrevious(): void
37
+ execute(doc: Document, sel: Selection): EditorState | null
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Built-in commands
42
+ // ---------------------------------------------------------------------------
43
+
44
+ function makeBlockCommand(
45
+ name: string,
46
+ label: string,
47
+ description: string,
48
+ keywords: string[],
49
+ blockType: BlockType,
50
+ meta?: Record<string, unknown>,
51
+ ): SlashCommand {
52
+ return {
53
+ name,
54
+ label,
55
+ description,
56
+ keywords,
57
+ action: (doc: Document, sel: Selection): EditorState => {
58
+ return changeBlockType(doc, sel, blockType, meta)
59
+ },
60
+ }
61
+ }
62
+
63
+ export const BUILT_IN_COMMANDS: SlashCommand[] = [
64
+ makeBlockCommand('heading1', 'Heading 1', 'Large section heading', ['h1', 'title', 'heading'], 'heading', { level: 1 }),
65
+ makeBlockCommand('heading2', 'Heading 2', 'Medium section heading', ['h2', 'subtitle', 'heading'], 'heading', { level: 2 }),
66
+ makeBlockCommand('heading3', 'Heading 3', 'Small section heading', ['h3', 'heading'], 'heading', { level: 3 }),
67
+ makeBlockCommand('bullet-list', 'Bullet List', 'Create a bulleted list', ['ul', 'unordered', 'bullet', 'list'], 'list-item', { listType: 'bullet' }),
68
+ makeBlockCommand('numbered-list', 'Numbered List', 'Create a numbered list', ['ol', 'ordered', 'number', 'list'], 'list-item', { listType: 'ordered' }),
69
+ makeBlockCommand('quote', 'Quote', 'Create a blockquote', ['blockquote', 'citation', 'quote'], 'blockquote'),
70
+ makeBlockCommand('code', 'Code Block', 'Create a code block', ['code', 'pre', 'snippet', 'programming'], 'code-block'),
71
+ makeBlockCommand('divider', 'Divider', 'Insert a horizontal divider', ['hr', 'horizontal', 'rule', 'separator', 'line'], 'divider'),
72
+ {
73
+ name: 'image',
74
+ label: 'Image',
75
+ description: 'Insert an image',
76
+ keywords: ['img', 'picture', 'photo'],
77
+ action: (doc: Document, sel: Selection): EditorState => {
78
+ return changeBlockType(doc, sel, 'image')
79
+ },
80
+ },
81
+ ]
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Fuzzy filter
85
+ // ---------------------------------------------------------------------------
86
+ function matchesQuery(command: SlashCommand, query: string): boolean {
87
+ const q = query.toLowerCase()
88
+ if (q === '') return true
89
+ if (command.name.toLowerCase().includes(q)) return true
90
+ if (command.label.toLowerCase().includes(q)) return true
91
+ if (command.description.toLowerCase().includes(q)) return true
92
+ return command.keywords.some((kw) => kw.toLowerCase().includes(q))
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Menu factory
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export function createSlashCommandMenu(commands?: SlashCommand[]): SlashCommandMenu {
100
+ const allCommands = commands ?? BUILT_IN_COMMANDS
101
+
102
+ const state = {
103
+ isOpen: false,
104
+ query: '',
105
+ filteredCommands: [] as SlashCommand[],
106
+ selectedIndex: 0,
107
+ }
108
+
109
+ function updateFiltered() {
110
+ state.filteredCommands = allCommands.filter((cmd) => matchesQuery(cmd, state.query))
111
+ if (state.selectedIndex >= state.filteredCommands.length) {
112
+ state.selectedIndex = Math.max(0, state.filteredCommands.length - 1)
113
+ }
114
+ }
115
+
116
+ const menu: SlashCommandMenu = {
117
+ get isOpen() { return state.isOpen },
118
+ get query() { return state.query },
119
+ get filteredCommands() { return state.filteredCommands },
120
+ get selectedIndex() { return state.selectedIndex },
121
+
122
+ open(query?: string) {
123
+ state.isOpen = true
124
+ state.query = query ?? ''
125
+ state.selectedIndex = 0
126
+ updateFiltered()
127
+ },
128
+
129
+ close() {
130
+ state.isOpen = false
131
+ state.query = ''
132
+ state.filteredCommands = []
133
+ state.selectedIndex = 0
134
+ },
135
+
136
+ search(query: string) {
137
+ state.query = query
138
+ updateFiltered()
139
+ },
140
+
141
+ selectNext() {
142
+ if (state.filteredCommands.length === 0) return
143
+ state.selectedIndex = (state.selectedIndex + 1) % state.filteredCommands.length
144
+ },
145
+
146
+ selectPrevious() {
147
+ if (state.filteredCommands.length === 0) return
148
+ state.selectedIndex =
149
+ (state.selectedIndex - 1 + state.filteredCommands.length) % state.filteredCommands.length
150
+ },
151
+
152
+ execute(doc: Document, sel: Selection): EditorState | null {
153
+ if (!state.isOpen || state.filteredCommands.length === 0) return null
154
+ const command = state.filteredCommands[state.selectedIndex]
155
+ if (!command) return null
156
+
157
+ // Remove the slash and query text from the block before executing
158
+ const block = findBlockById(doc, sel.anchor.blockId)
159
+ if (block) {
160
+ const text = getBlockText(block)
161
+ // Find the "/" that started this command
162
+ const slashIdx = text.lastIndexOf('/', sel.anchor.offset)
163
+ if (slashIdx !== -1) {
164
+ const d = cloneDocument(doc)
165
+ const b = d.blocks.find((bl) => bl.id === block.id)!
166
+ const beforeSlash = text.slice(0, slashIdx)
167
+ const afterCursor = text.slice(sel.anchor.offset)
168
+ b.content = [createTextSegment(beforeSlash + afterCursor)]
169
+ const newSel = createCollapsedSelection(b.id, slashIdx)
170
+ const result = command.action(d, newSel)
171
+ menu.close()
172
+ return result
173
+ }
174
+ }
175
+
176
+ const result = command.action(doc, sel)
177
+ menu.close()
178
+ return result
179
+ },
180
+ }
181
+
182
+ return menu
183
+ }
184
+
185
+ /**
186
+ * Detect whether the user has typed a "/" that should trigger the slash command menu.
187
+ */
188
+ export function detectSlashTrigger(
189
+ doc: Document,
190
+ sel: Selection,
191
+ ): { triggered: boolean; query: string } {
192
+ const block = findBlockById(doc, sel.anchor.blockId)
193
+ if (!block) return { triggered: false, query: '' }
194
+
195
+ const text = getBlockText(block)
196
+ const textBeforeCursor = text.slice(0, sel.anchor.offset)
197
+
198
+ // Look for "/" preceded by start-of-text or a space
199
+ const slashIdx = textBeforeCursor.lastIndexOf('/')
200
+ if (slashIdx === -1) return { triggered: false, query: '' }
201
+
202
+ // The slash must be at position 0 or preceded by a space
203
+ if (slashIdx > 0 && textBeforeCursor[slashIdx - 1] !== ' ') {
204
+ return { triggered: false, query: '' }
205
+ }
206
+
207
+ const query = textBeforeCursor.slice(slashIdx + 1)
208
+ // If query contains spaces, the slash command is no longer active
209
+ if (query.includes(' ')) return { triggered: false, query: '' }
210
+
211
+ return { triggered: true, query }
212
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ createSearchBar,
3
+ type SearchBarProps,
4
+ type SearchBarAPI,
5
+ } from './search-bar.js'
6
+
7
+ export { searchBarVariants, searchResultVariants } from './search-bar.styles.js'
@@ -0,0 +1,29 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ export const searchBarVariants = cva({
4
+ base: 'relative flex items-center w-full rounded-md border bg-background text-foreground',
5
+ variants: {
6
+ size: {
7
+ sm: 'h-8 text-sm px-2',
8
+ md: 'h-10 text-base px-3',
9
+ lg: 'h-12 text-lg px-4',
10
+ },
11
+ },
12
+ defaultVariants: {
13
+ size: 'md',
14
+ },
15
+ })
16
+
17
+ export const searchResultVariants = cva({
18
+ base: 'absolute top-full left-0 w-full mt-1 rounded-md border bg-popover text-popover-foreground shadow-md z-50 overflow-hidden',
19
+ variants: {
20
+ size: {
21
+ sm: 'text-sm',
22
+ md: 'text-base',
23
+ lg: 'text-lg',
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ size: 'md',
28
+ },
29
+ })
@@ -0,0 +1,130 @@
1
+ import type { KeyboardHandlerMap } from '../shared/index.ts'
2
+ import { generateId, Keys, createKeyboardHandler } from '../shared/index.ts'
3
+
4
+ export interface SearchBarProps {
5
+ value?: string
6
+ onValueChange?: (value: string) => void
7
+ onSearch?: (value: string) => void
8
+ debounceMs?: number
9
+ placeholder?: string
10
+ loading?: boolean
11
+ }
12
+
13
+ export interface SearchBarAPI {
14
+ /** Current state */
15
+ state: { value: string; isSearching: boolean }
16
+ /** Props to spread on the input element */
17
+ inputProps: {
18
+ role: 'combobox'
19
+ 'aria-expanded': boolean
20
+ 'aria-controls': string
21
+ 'aria-autocomplete': 'list'
22
+ }
23
+ /** Props to spread on the results list element */
24
+ listProps: {
25
+ role: 'listbox'
26
+ id: string
27
+ }
28
+ /** Set the search value */
29
+ setValue: (value: string) => void
30
+ /** Clear the search value */
31
+ clear: () => void
32
+ /** Keyboard handler map */
33
+ keyboardHandlers: KeyboardHandlerMap
34
+ /** Pre-built keyboard event handler */
35
+ handleKeyDown: (event: KeyboardEvent) => void
36
+ }
37
+
38
+ export function createSearchBar(props: SearchBarProps = {}): SearchBarAPI {
39
+ const {
40
+ value: initialValue = '',
41
+ onValueChange,
42
+ onSearch,
43
+ debounceMs = 300,
44
+ placeholder: _placeholder,
45
+ loading = false,
46
+ } = props
47
+
48
+ const listId = generateId('rfr-search-list')
49
+
50
+ let currentValue = initialValue
51
+ let isSearching = loading
52
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined
53
+
54
+ const state = { value: currentValue, isSearching }
55
+
56
+ function setValue(value: string) {
57
+ currentValue = value
58
+ state.value = value
59
+ onValueChange?.(value)
60
+
61
+ if (debounceTimer !== undefined) {
62
+ clearTimeout(debounceTimer)
63
+ }
64
+
65
+ if (value.length > 0) {
66
+ isSearching = true
67
+ state.isSearching = true
68
+ debounceTimer = setTimeout(() => {
69
+ onSearch?.(value)
70
+ isSearching = false
71
+ state.isSearching = false
72
+ }, debounceMs)
73
+ } else {
74
+ isSearching = false
75
+ state.isSearching = false
76
+ }
77
+ }
78
+
79
+ function clear() {
80
+ if (debounceTimer !== undefined) {
81
+ clearTimeout(debounceTimer)
82
+ }
83
+ currentValue = ''
84
+ state.value = ''
85
+ isSearching = false
86
+ state.isSearching = false
87
+ onValueChange?.('')
88
+ }
89
+
90
+ const inputProps = {
91
+ role: 'combobox' as const,
92
+ 'aria-expanded': currentValue.length > 0,
93
+ 'aria-controls': listId,
94
+ 'aria-autocomplete': 'list' as const,
95
+ }
96
+
97
+ const listProps = {
98
+ role: 'listbox' as const,
99
+ id: listId,
100
+ }
101
+
102
+ const keyboardHandlers: KeyboardHandlerMap = {
103
+ [Keys.ArrowDown]: () => {
104
+ // Focus first result - signaled via handler
105
+ },
106
+ [Keys.Escape]: () => {
107
+ clear()
108
+ },
109
+ [Keys.Enter]: () => {
110
+ if (debounceTimer !== undefined) {
111
+ clearTimeout(debounceTimer)
112
+ }
113
+ onSearch?.(currentValue)
114
+ isSearching = false
115
+ state.isSearching = false
116
+ },
117
+ }
118
+
119
+ const handleKeyDown = createKeyboardHandler(keyboardHandlers)
120
+
121
+ return {
122
+ state,
123
+ inputProps,
124
+ listProps,
125
+ setValue,
126
+ clear,
127
+ keyboardHandlers,
128
+ handleKeyDown,
129
+ }
130
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ createSelect,
3
+ type SelectProps,
4
+ type SelectAPI,
5
+ type SelectOption,
6
+ } from './select.js'
7
+
8
+ export {
9
+ selectTriggerVariants,
10
+ selectContentVariants,
11
+ selectItemVariants,
12
+ selectTokens,
13
+ } from './select.styles.js'
@@ -0,0 +1,52 @@
1
+ import { cva } from '../shared/index.ts'
2
+ import type { TokenContract } from '../shared/index.ts'
3
+
4
+ export const selectTokens: TokenContract = {
5
+ name: 'select',
6
+ tokens: {
7
+ bg: { variable: '--rfr-select-bg', fallback: 'hsl(var(--background))' },
8
+ fg: { variable: '--rfr-select-fg', fallback: 'hsl(var(--foreground))' },
9
+ border: { variable: '--rfr-select-border', fallback: 'hsl(var(--border))' },
10
+ ring: { variable: '--rfr-select-ring', fallback: 'hsl(var(--ring))' },
11
+ },
12
+ }
13
+
14
+ export const selectTriggerVariants = cva({
15
+ base: 'flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
16
+ variants: {
17
+ size: {
18
+ sm: 'h-8 text-xs',
19
+ default: 'h-9 text-sm',
20
+ lg: 'h-10 text-base',
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ size: 'default',
25
+ },
26
+ })
27
+
28
+ export const selectContentVariants = cva({
29
+ base: 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95',
30
+ variants: {
31
+ position: {
32
+ popper: 'translate-y-1',
33
+ 'item-aligned': '',
34
+ },
35
+ },
36
+ defaultVariants: {
37
+ position: 'popper',
38
+ },
39
+ })
40
+
41
+ export const selectItemVariants = cva({
42
+ base: 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
43
+ variants: {
44
+ selected: {
45
+ true: 'bg-accent/50',
46
+ false: '',
47
+ },
48
+ },
49
+ defaultVariants: {
50
+ selected: 'false',
51
+ },
52
+ })
@@ -0,0 +1,152 @@
1
+ import type { AccessibilityProps, KeyboardHandlerMap } from '../shared/index.ts'
2
+ import { Keys, generateId } from '../shared/index.ts'
3
+
4
+ export interface SelectOption {
5
+ value: string
6
+ label: string
7
+ disabled?: boolean
8
+ }
9
+
10
+ export interface SelectProps {
11
+ value?: string
12
+ options?: SelectOption[]
13
+ placeholder?: string
14
+ disabled?: boolean
15
+ open?: boolean
16
+ name?: string
17
+ }
18
+
19
+ export interface SelectAPI {
20
+ /** Trigger element ARIA + keyboard props */
21
+ triggerProps: {
22
+ ariaProps: Partial<AccessibilityProps>
23
+ keyboardHandlers: KeyboardHandlerMap
24
+ dataAttributes: Record<string, string>
25
+ }
26
+ /** Content (dropdown) props */
27
+ contentProps: {
28
+ ariaProps: Partial<AccessibilityProps>
29
+ dataAttributes: Record<string, string>
30
+ }
31
+ /** Get props for a single option */
32
+ getOptionProps: (option: SelectOption) => {
33
+ ariaProps: Partial<AccessibilityProps>
34
+ dataAttributes: Record<string, string>
35
+ isSelected: boolean
36
+ }
37
+ /** Whether interaction should be blocked */
38
+ isInteractive: boolean
39
+ /** Current state */
40
+ state: {
41
+ open: boolean
42
+ selectedValue: string | undefined
43
+ selectedLabel: string | undefined
44
+ disabled: boolean
45
+ placeholder: string
46
+ }
47
+ /** Generated IDs for trigger/content association */
48
+ ids: {
49
+ trigger: string
50
+ content: string
51
+ }
52
+ }
53
+
54
+ export function createSelect(props: SelectProps = {}): SelectAPI {
55
+ const {
56
+ value,
57
+ options = [],
58
+ placeholder = 'Select an option',
59
+ disabled = false,
60
+ open = false,
61
+ } = props
62
+
63
+ const isInteractive = !disabled
64
+ const selectedOption = options.find((o) => o.value === value)
65
+
66
+ const triggerId = generateId('select-trigger')
67
+ const contentId = generateId('select-content')
68
+
69
+ const triggerAriaProps: Partial<AccessibilityProps> = {
70
+ role: 'combobox',
71
+ 'aria-expanded': open,
72
+ 'aria-controls': contentId,
73
+ 'aria-labelledby': triggerId,
74
+ }
75
+ if (disabled) {
76
+ triggerAriaProps['aria-disabled'] = true
77
+ }
78
+
79
+ const triggerKeyboardHandlers: KeyboardHandlerMap = {}
80
+ if (!isInteractive) {
81
+ triggerKeyboardHandlers[Keys.Enter] = (e) => e.preventDefault()
82
+ triggerKeyboardHandlers[Keys.Space] = (e) => e.preventDefault()
83
+ triggerKeyboardHandlers[Keys.ArrowDown] = (e) => e.preventDefault()
84
+ triggerKeyboardHandlers[Keys.ArrowUp] = (e) => e.preventDefault()
85
+ }
86
+
87
+ const triggerDataAttributes: Record<string, string> = {
88
+ 'data-state': open ? 'open' : 'closed',
89
+ }
90
+ if (disabled) {
91
+ triggerDataAttributes['data-disabled'] = ''
92
+ }
93
+
94
+ const contentAriaProps: Partial<AccessibilityProps> = {
95
+ role: 'listbox',
96
+ 'aria-labelledby': triggerId,
97
+ }
98
+
99
+ const contentDataAttributes: Record<string, string> = {
100
+ 'data-state': open ? 'open' : 'closed',
101
+ }
102
+
103
+ function getOptionProps(option: SelectOption) {
104
+ const isSelected = option.value === value
105
+ const optionAriaProps: Partial<AccessibilityProps> = {
106
+ role: 'option',
107
+ 'aria-selected': isSelected,
108
+ }
109
+ if (option.disabled) {
110
+ optionAriaProps['aria-disabled'] = true
111
+ }
112
+
113
+ const optionDataAttributes: Record<string, string> = {}
114
+ if (isSelected) {
115
+ optionDataAttributes['data-state'] = 'checked'
116
+ }
117
+ if (option.disabled) {
118
+ optionDataAttributes['data-disabled'] = ''
119
+ }
120
+
121
+ return {
122
+ ariaProps: optionAriaProps,
123
+ dataAttributes: optionDataAttributes,
124
+ isSelected,
125
+ }
126
+ }
127
+
128
+ return {
129
+ triggerProps: {
130
+ ariaProps: triggerAriaProps,
131
+ keyboardHandlers: triggerKeyboardHandlers,
132
+ dataAttributes: triggerDataAttributes,
133
+ },
134
+ contentProps: {
135
+ ariaProps: contentAriaProps,
136
+ dataAttributes: contentDataAttributes,
137
+ },
138
+ getOptionProps,
139
+ isInteractive,
140
+ state: {
141
+ open,
142
+ selectedValue: selectedOption?.value,
143
+ selectedLabel: selectedOption?.label,
144
+ disabled,
145
+ placeholder,
146
+ },
147
+ ids: {
148
+ trigger: triggerId,
149
+ content: contentId,
150
+ },
151
+ }
152
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ createSidebar,
3
+ type SidebarItem,
4
+ type SidebarSection,
5
+ type SidebarProps,
6
+ type SidebarAPI,
7
+ } from './sidebar.js'
8
+
9
+ export { sidebarVariants, sidebarItemVariants } from './sidebar.styles.js'
@@ -0,0 +1,27 @@
1
+ import { cva } from '../shared/index.ts'
2
+
3
+ export const sidebarVariants = cva({
4
+ base: 'hidden md:flex flex-col border-r bg-background',
5
+ variants: {
6
+ collapsed: {
7
+ true: 'w-16',
8
+ false: 'w-64',
9
+ },
10
+ },
11
+ defaultVariants: {
12
+ collapsed: 'false',
13
+ },
14
+ })
15
+
16
+ export const sidebarItemVariants = cva({
17
+ base: 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
18
+ variants: {
19
+ active: {
20
+ true: 'bg-accent text-accent-foreground',
21
+ false: 'text-muted-foreground hover:bg-muted hover:text-foreground',
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ active: 'false',
26
+ },
27
+ })
@@ -0,0 +1,59 @@
1
+ export interface SidebarItem {
2
+ label: string
3
+ href: string
4
+ icon?: string
5
+ /** Roles required to see this item */
6
+ roles?: string[]
7
+ /** Nested items */
8
+ children?: SidebarItem[]
9
+ }
10
+
11
+ export interface SidebarSection {
12
+ title?: string
13
+ items: SidebarItem[]
14
+ }
15
+
16
+ export interface SidebarProps {
17
+ sections?: SidebarSection[]
18
+ currentPath?: string
19
+ collapsed?: boolean
20
+ userRoles?: string[]
21
+ }
22
+
23
+ export interface SidebarAPI {
24
+ ariaProps: Record<string, string>
25
+ isActive: (href: string) => boolean
26
+ isVisible: (item: SidebarItem) => boolean
27
+ itemAriaProps: (href: string) => Record<string, string>
28
+ visibleSections: SidebarSection[]
29
+ }
30
+
31
+ export function createSidebar(props: SidebarProps = {}): SidebarAPI {
32
+ const { sections = [], currentPath = '/', userRoles = [] } = props
33
+
34
+ function isActive(href: string): boolean {
35
+ if (href === '/') return currentPath === '/'
36
+ return currentPath.startsWith(href)
37
+ }
38
+
39
+ function isVisible(item: SidebarItem): boolean {
40
+ if (!item.roles || item.roles.length === 0) return true
41
+ return item.roles.some((role) => userRoles.includes(role))
42
+ }
43
+
44
+ const visibleSections = sections
45
+ .map((section) => ({
46
+ ...section,
47
+ items: section.items.filter(isVisible),
48
+ }))
49
+ .filter((section) => section.items.length > 0)
50
+
51
+ return {
52
+ ariaProps: { role: 'navigation', 'aria-label': 'Sidebar' },
53
+ isActive,
54
+ isVisible,
55
+ itemAriaProps: (href: string): Record<string, string> =>
56
+ isActive(href) ? { 'aria-current': 'page' } : {},
57
+ visibleSections,
58
+ }
59
+ }