betterstart-cli 0.0.12 → 0.0.14

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 (99) hide show
  1. package/dist/assets/adapters/next/plugins/blog/plugin.ts +1 -1
  2. package/dist/assets/adapters/next/plugins/blog/schemas/menus.json +155 -0
  3. package/dist/assets/adapters/next/plugins/blog/schemas/posts.json +2 -7
  4. package/dist/assets/adapters/next/templates/init/components/layouts/admin-sidebar.tsx +9 -3
  5. package/dist/assets/adapters/next/templates/init/components/layouts/sidebar-branding.tsx +1 -1
  6. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid.tsx +107 -0
  7. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-table.tsx +58 -45
  8. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode/lifecycle-hooks-tab.tsx +1 -1
  9. package/dist/assets/adapters/next/templates/init/components/shared/dev-mode-integrate.tsx +1 -1
  10. package/dist/assets/adapters/next/templates/init/components/shared/entity-filters-bar.tsx +1 -1
  11. package/dist/assets/adapters/next/templates/init/components/shared/entity-versions/entity-versions-drawer.tsx +1 -1
  12. package/dist/assets/adapters/next/templates/init/components/shared/page-header.tsx +20 -17
  13. package/dist/assets/adapters/next/templates/init/components/shared/sort-indicator.tsx +7 -3
  14. package/dist/assets/adapters/next/templates/init/data/navigation.ts +4 -1
  15. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor-slash-menu.ts +28 -2
  16. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-content-editor.ts +22 -1
  17. package/dist/assets/adapters/next/templates/init/hooks/content-editor/use-table-button.ts +85 -0
  18. package/dist/assets/adapters/next/templates/init/hooks/use-audit-log.ts +23 -0
  19. package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +63 -0
  20. package/dist/assets/adapters/next/templates/init/lib/actions/audit/get-audit-logs.ts +122 -0
  21. package/dist/assets/adapters/next/templates/init/lib/actions/audit/index.ts +19 -0
  22. package/dist/assets/adapters/next/templates/init/lib/actions/audit/record-audit-event.ts +37 -0
  23. package/dist/assets/adapters/next/templates/init/lib/actions/audit/register.ts +13 -0
  24. package/dist/assets/adapters/next/templates/init/lib/actions/audit/request-metadata.ts +37 -0
  25. package/dist/assets/adapters/next/templates/init/lib/actions/audit/types.ts +77 -0
  26. package/dist/assets/adapters/next/templates/init/lib/actions/forms/upsert-form-settings.ts +3 -0
  27. package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +19 -0
  28. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +23 -0
  29. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +21 -0
  30. package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +23 -1
  31. package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +17 -0
  32. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-files.ts +3 -0
  33. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +3 -0
  34. package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +19 -1
  35. package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +23 -1
  36. package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +23 -1
  37. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +49 -0
  38. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/index.ts +6 -1
  39. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/register.ts +3 -0
  40. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/registry.ts +36 -1
  41. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +117 -12
  42. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/types.ts +35 -1
  43. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-drawer.tsx +104 -0
  44. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-detail-row.tsx +15 -0
  45. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-content.tsx +565 -0
  46. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page-skeleton.tsx +7 -0
  47. package/dist/assets/adapters/next/templates/init/pages/audit-log/audit-log-page.tsx +14 -0
  48. package/dist/assets/adapters/next/templates/init/pages/users/users-page-content.tsx +83 -2
  49. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +126 -78
  50. package/dist/assets/adapters/next/templates/init/utils/audit/audit.ts +54 -0
  51. package/dist/assets/adapters/next/templates/init/utils/editor/tiptap.ts +7 -32
  52. package/dist/assets/adapters/next/templates/init/utils/form/list-field.ts +17 -0
  53. package/dist/assets/adapters/next/templates/init/utils/user/user.ts +70 -0
  54. package/dist/assets/shared-assets/react-admin/custom/content-editor/horizontal-rule-button.tsx +1 -1
  55. package/dist/assets/shared-assets/react-admin/custom/content-editor/index.tsx +6 -0
  56. package/dist/assets/shared-assets/react-admin/custom/content-editor/main-toolbar-content.tsx +1 -1
  57. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-button.tsx +1 -1
  58. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-editor-controls.tsx +27 -40
  59. package/dist/assets/shared-assets/react-admin/custom/content-editor/math-popover-button.tsx +0 -1
  60. package/dist/assets/shared-assets/react-admin/custom/content-editor/mobile-toolbar-content.tsx +4 -4
  61. package/dist/assets/shared-assets/react-admin/custom/content-editor/mode-toggle-button.tsx +1 -1
  62. package/dist/assets/shared-assets/react-admin/custom/content-editor/selection-bubble-menu.tsx +45 -8
  63. package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +153 -27
  64. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode-dropdown-button.tsx +1 -1
  65. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +9 -11
  66. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-bubble-menu.tsx +1 -1
  67. package/dist/assets/shared-assets/react-admin/custom/content-editor/table-button.tsx +24 -67
  68. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/blockquote-button/blockquote-button.tsx +1 -1
  69. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/code-block-button/code-block-button.tsx +1 -1
  70. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-button.tsx +1 -1
  71. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover-content.tsx +1 -1
  72. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +11 -2
  73. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/color-highlight-popover/source-color-highlight-popover.tsx +1 -1
  74. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu-item.tsx +1 -1
  75. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +9 -4
  76. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-button.tsx +1 -1
  77. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-control-popover.tsx +5 -12
  78. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-main.tsx +19 -45
  79. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/link-popover/link-popover.tsx +2 -3
  80. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu-item.tsx +1 -1
  81. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +9 -4
  82. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/mark-button/mark-button.tsx +1 -1
  83. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/undo-redo-button/undo-redo-button.tsx +1 -1
  84. package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +2 -2
  85. package/dist/assets/shared-assets/react-admin/custom/dynamic-list-field.tsx +20 -8
  86. package/dist/assets/shared-assets/react-admin/custom/media-gallery-field.tsx +1 -1
  87. package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +211 -0
  88. package/dist/assets/shared-assets/react-admin/schema.json +136 -23
  89. package/dist/assets/shared-assets/react-admin/ui/button.tsx +18 -18
  90. package/dist/assets/shared-assets/react-admin/ui/card.tsx +1 -1
  91. package/dist/assets/shared-assets/react-admin/ui/checkbox.tsx +1 -1
  92. package/dist/assets/shared-assets/react-admin/ui/combobox.tsx +27 -3
  93. package/dist/assets/shared-assets/react-admin/ui/drawer.tsx +9 -5
  94. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +27 -12
  95. package/dist/assets/shared-assets/react-admin/ui/form.tsx +1 -1
  96. package/dist/assets/shared-assets/react-admin/ui/sidebar.tsx +4 -4
  97. package/dist/cli.js +5396 -2385
  98. package/dist/cli.js.map +1 -1
  99. package/package.json +10 -2
@@ -39,6 +39,7 @@ export function ContentEditor({
39
39
 
40
40
  const {
41
41
  activeSlashIndex,
42
+ closeSlashMenu,
42
43
  editor,
43
44
  filteredSlashCommands,
44
45
  handleModeChange,
@@ -47,7 +48,9 @@ export function ContentEditor({
47
48
  mode,
48
49
  selectSlashCommand,
49
50
  setActiveSlashIndex,
51
+ setSlashSearch,
50
52
  slashMenu,
53
+ slashSearchQuery,
51
54
  sourceValue,
52
55
  sourceSelectionRequest
53
56
  } = useContentEditor({
@@ -123,8 +126,11 @@ export function ContentEditor({
123
126
  activeIndex={activeSlashIndex}
124
127
  commands={filteredSlashCommands}
125
128
  onActiveIndexChange={setActiveSlashIndex}
129
+ onClose={closeSlashMenu}
126
130
  onCommandSelect={selectSlashCommand}
131
+ onSearchChange={setSlashSearch}
127
132
  slashMenu={slashMenu}
133
+ searchQuery={slashSearchQuery}
128
134
  />
129
135
 
130
136
  <EditorContent
@@ -103,7 +103,7 @@ export function MainToolbarContent({
103
103
  mediaInsertion.insertMedia()
104
104
  }}
105
105
  >
106
- <ImagePlusIcon className="tiptap-button-icon" />
106
+ <ImagePlusIcon />
107
107
  <span className="tiptap-button-text">Media</span>
108
108
  </Button>
109
109
  ) : null}
@@ -13,7 +13,7 @@ export function MathButton({ children, text, ...props }: MathButtonProps) {
13
13
  <Button type="button" variant="ghost" size={text ? 'sm' : 'icon-sm'} {...props}>
14
14
  {children ?? (
15
15
  <>
16
- <SigmaIcon className="tiptap-button-icon" />
16
+ <SigmaIcon />
17
17
  {text ? <span className="tiptap-button-text">{text}</span> : null}
18
18
  </>
19
19
  )}
@@ -1,16 +1,13 @@
1
1
  'use client'
2
2
 
3
3
  import { Button } from '@admin/components/ui/button'
4
- import { Separator } from '@admin/components/ui/separator'
5
4
  import { Textarea } from '@admin/components/ui/textarea'
6
- import { cn } from '@admin/utils/shared/cn'
7
- import { CornerDownLeftIcon, TrashIcon } from 'lucide-react'
5
+ import { TrashIcon } from 'lucide-react'
8
6
  import * as React from 'react'
9
7
 
10
8
  export interface MathEditorControlsProps {
11
9
  disabled?: boolean
12
10
  initialValue?: string
13
- layout?: 'popover' | 'toolbar'
14
11
  onApply: (value: string) => boolean | undefined
15
12
  onComplete?: () => void
16
13
  onRemove?: () => boolean | undefined
@@ -19,14 +16,19 @@ export interface MathEditorControlsProps {
19
16
  export function MathEditorControls({
20
17
  disabled,
21
18
  initialValue = '',
22
- layout = 'toolbar',
23
19
  onApply,
24
20
  onComplete,
25
21
  onRemove
26
22
  }: MathEditorControlsProps) {
27
23
  const [value, setValue] = React.useState(initialValue)
24
+ const textareaRef = React.useRef<HTMLTextAreaElement>(null)
28
25
  const canApply = Boolean(value.trim()) && !disabled
29
- const isPopoverLayout = layout === 'popover'
26
+
27
+ React.useEffect(() => {
28
+ if (disabled) return
29
+
30
+ textareaRef.current?.focus({ preventScroll: true })
31
+ }, [disabled])
30
32
 
31
33
  const applyMath = React.useCallback(() => {
32
34
  if (!canApply) return
@@ -60,57 +62,42 @@ export function MathEditorControls({
60
62
  )
61
63
 
62
64
  return (
63
- <div
64
- className={
65
- isPopoverLayout
66
- ? 'flex w-72 max-w-[calc(100vw-2rem)] flex-col gap-3 p-3'
67
- : 'flex min-w-72 max-w-[calc(100vw-2rem)] items-start gap-2 p-2'
68
- }
69
- >
65
+ <div className="flex w-72 flex-col gap-3 p-4">
70
66
  <Textarea
67
+ ref={textareaRef}
71
68
  value={value}
72
69
  onChange={(event) => setValue(event.target.value)}
73
70
  onKeyDown={handleKeyDown}
74
71
  placeholder="Paste LaTeX..."
75
72
  disabled={disabled}
76
- autoFocus
77
73
  className="min-h-24 resize-y rounded-md text-sm"
78
74
  />
79
75
 
80
- <div
81
- className={cn({
82
- 'flex flex-col gap-2': isPopoverLayout,
83
- 'flex shrink-0 items-center gap-1': !isPopoverLayout
84
- })}
85
- >
76
+ <div className="flex items-center flex-wrap gap-1">
77
+ {onRemove ? (
78
+ <Button
79
+ type="button"
80
+ variant="destructive"
81
+ disabled={disabled}
82
+ aria-label="Remove math"
83
+ title="Remove"
84
+ onClick={removeMath}
85
+ >
86
+ <TrashIcon />
87
+ </Button>
88
+ ) : null}
89
+
86
90
  <Button
87
91
  type="button"
88
- variant={isPopoverLayout ? 'default' : 'ghost'}
89
- size={isPopoverLayout ? 'sm' : 'icon-sm'}
92
+ variant="default"
93
+ className="flex-1"
90
94
  disabled={!canApply}
91
95
  aria-label="Enter math"
92
96
  title="Enter"
93
97
  onClick={applyMath}
94
98
  >
95
- {isPopoverLayout ? 'Enter' : <CornerDownLeftIcon className="tiptap-button-icon" />}
99
+ Insert
96
100
  </Button>
97
-
98
- {onRemove ? (
99
- <>
100
- <Separator orientation="vertical" decorative className="h-6 my-auto self-center" />
101
- <Button
102
- type="button"
103
- variant="ghost"
104
- size="icon-sm"
105
- disabled={disabled}
106
- aria-label="Remove math"
107
- title="Remove"
108
- onClick={removeMath}
109
- >
110
- <TrashIcon className="tiptap-button-icon" />
111
- </Button>
112
- </>
113
- ) : null}
114
101
  </div>
115
102
  </div>
116
103
  )
@@ -49,7 +49,6 @@ export function MathPopoverButton({
49
49
  <PopoverContent align="start" className="w-auto gap-0 p-0">
50
50
  <MathEditorControls
51
51
  disabled={disabled || !canInsert}
52
- layout="popover"
53
52
  onApply={applyMath}
54
53
  onComplete={() => setIsOpen(false)}
55
54
  />
@@ -27,13 +27,13 @@ export function MobileToolbarContent({
27
27
  aria-label="Back to formatting toolbar"
28
28
  title="Back"
29
29
  >
30
- <ArrowLeftIcon className="tiptap-button-icon" />
30
+ <ArrowLeftIcon />
31
31
  {type === 'highlighter' ? (
32
- <HighlighterIcon className="tiptap-button-icon" />
32
+ <HighlighterIcon />
33
33
  ) : type === 'math' ? (
34
- <SigmaIcon className="tiptap-button-icon" />
34
+ <SigmaIcon />
35
35
  ) : (
36
- <LinkIcon className="tiptap-button-icon" />
36
+ <LinkIcon />
37
37
  )}
38
38
  </Button>
39
39
  </div>
@@ -22,7 +22,7 @@ export function ModeToggleButton({
22
22
  onClick={onClick}
23
23
  className="min-w-40"
24
24
  >
25
- <FileCode2 className="tiptap-button-icon" />
25
+ <FileCode2 />
26
26
  <span className="tiptap-button-text">{label}</span>
27
27
  </Button>
28
28
  )
@@ -10,6 +10,7 @@ import { Separator } from '@admin/components/ui/separator'
10
10
  import { cn } from '@admin/utils/shared/cn'
11
11
  import type { Editor } from '@tiptap/core'
12
12
  import { BubbleMenu } from '@tiptap/react/menus'
13
+ import * as React from 'react'
13
14
 
14
15
  const HIDDEN_SELECTION_TOOLBAR_TYPES = [
15
16
  'mediaGalleryPlaceholder',
@@ -26,6 +27,31 @@ const HIDDEN_SELECTION_TOOLBAR_TYPES = [
26
27
  ] as const
27
28
 
28
29
  export function SelectionBubbleMenu({ editor }: { editor: Editor }) {
30
+ const [hasOpenSelectionMenuChild, setHasOpenSelectionMenuChild] = React.useState(false)
31
+ const hasOpenSelectionMenuChildRef = React.useRef(false)
32
+ const setSelectionMenuChildOpen = React.useCallback((isOpen: boolean) => {
33
+ hasOpenSelectionMenuChildRef.current = isOpen
34
+ setHasOpenSelectionMenuChild(isOpen)
35
+ }, [])
36
+ const handleSelectionMenuChildOpenChange = React.useCallback(
37
+ (isOpen: boolean) => {
38
+ setSelectionMenuChildOpen(isOpen)
39
+ },
40
+ [setSelectionMenuChildOpen]
41
+ )
42
+ const handleSelectionMenuPointerDown = React.useCallback(
43
+ (event: React.PointerEvent<HTMLDivElement>) => {
44
+ const target = event.target
45
+ if (!(target instanceof Element)) return
46
+
47
+ const menuTrigger = target.closest('button[aria-haspopup], button[aria-expanded]')
48
+ if (menuTrigger) {
49
+ setSelectionMenuChildOpen(true)
50
+ }
51
+ },
52
+ [setSelectionMenuChildOpen]
53
+ )
54
+
29
55
  return (
30
56
  <>
31
57
  <BubbleMenu
@@ -46,8 +72,8 @@ export function SelectionBubbleMenu({ editor }: { editor: Editor }) {
46
72
  role="toolbar"
47
73
  aria-label="Link formatting toolbar"
48
74
  className={cn(
49
- 'flex items-center gap-1 overflow-x-auto border-border rounded-full border bg-popover p-1 text-popover-foreground shadow-menu [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
50
- 'relative z-50 p-0'
75
+ 'flex items-center gap-1 overflow-x-auto border-border border bg-popover p-4 text-foreground shadow-2xl [scrollbar-width:none] [&::-webkit-scrollbar]:hidden rounded-lg',
76
+ 'relative z-10'
51
77
  )}
52
78
  >
53
79
  <LinkContent editor={editor} />
@@ -60,7 +86,7 @@ export function SelectionBubbleMenu({ editor }: { editor: Editor }) {
60
86
  updateDelay={0}
61
87
  shouldShow={({ editor: currentEditor, from, to }) =>
62
88
  currentEditor.isEditable &&
63
- from !== to &&
89
+ (from !== to || hasOpenSelectionMenuChild || hasOpenSelectionMenuChildRef.current) &&
64
90
  HIDDEN_SELECTION_TOOLBAR_TYPES.every((type) => !currentEditor.isActive(type))
65
91
  }
66
92
  options={{
@@ -73,14 +99,22 @@ export function SelectionBubbleMenu({ editor }: { editor: Editor }) {
73
99
  <div
74
100
  role="toolbar"
75
101
  aria-label="Selection formatting toolbar"
102
+ onPointerDownCapture={handleSelectionMenuPointerDown}
76
103
  className={cn(
77
104
  'flex items-center gap-1 overflow-x-auto border-border rounded-full border bg-popover p-1 text-popover-foreground shadow-menu [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
78
- 'relative z-50'
105
+ 'relative z-10'
79
106
  )}
80
107
  >
81
108
  <div className="flex items-center gap-0.5">
82
- <HeadingDropdownMenu editor={editor} />
83
- <ListDropdownMenu editor={editor} types={['bulletList', 'orderedList', 'taskList']} />
109
+ <HeadingDropdownMenu
110
+ editor={editor}
111
+ onOpenChange={handleSelectionMenuChildOpenChange}
112
+ />
113
+ <ListDropdownMenu
114
+ editor={editor}
115
+ types={['bulletList', 'orderedList', 'taskList']}
116
+ onOpenChange={handleSelectionMenuChildOpenChange}
117
+ />
84
118
  </div>
85
119
 
86
120
  <Separator
@@ -95,8 +129,11 @@ export function SelectionBubbleMenu({ editor }: { editor: Editor }) {
95
129
  <MarkButton editor={editor} type="strike" />
96
130
  <MarkButton editor={editor} type="code" />
97
131
  <MarkButton editor={editor} type="underline" />
98
- <ColorHighlightPopover editor={editor} />
99
- <LinkPopover editor={editor} />
132
+ <ColorHighlightPopover
133
+ editor={editor}
134
+ onOpenChange={handleSelectionMenuChildOpenChange}
135
+ />
136
+ <LinkPopover editor={editor} onOpenChange={handleSelectionMenuChildOpenChange} />
100
137
  </div>
101
138
  </div>
102
139
  </BubbleMenu>
@@ -1,65 +1,191 @@
1
+ 'use client'
2
+
3
+ import {
4
+ Combobox,
5
+ ComboboxContent,
6
+ ComboboxEmpty,
7
+ ComboboxInput,
8
+ ComboboxItem,
9
+ ComboboxList
10
+ } from '@admin/components/ui/combobox'
1
11
  import type { ContentEditorSlashCommand } from '@admin/utils/editor/slash-commands'
2
- import { getSlashMenuStyle, type SlashMenuState } from '@admin/utils/editor/tiptap'
12
+ import type { SlashMenuState } from '@admin/utils/editor/tiptap'
3
13
  import { cn } from '@admin/utils/shared/cn'
14
+ import * as React from 'react'
4
15
 
5
16
  export function SlashCommandMenu({
6
17
  activeIndex,
7
18
  commands,
8
19
  onActiveIndexChange,
20
+ onClose,
9
21
  onCommandSelect,
10
- slashMenu
22
+ onSearchChange,
23
+ slashMenu,
24
+ searchQuery
11
25
  }: {
12
26
  activeIndex: number
13
27
  commands: ContentEditorSlashCommand[]
14
28
  onActiveIndexChange: (index: number) => void
29
+ onClose: () => void
15
30
  onCommandSelect: (command: ContentEditorSlashCommand) => void
31
+ onSearchChange: (query: string) => void
16
32
  slashMenu: SlashMenuState | null
33
+ searchQuery: string
17
34
  }) {
35
+ const activeItemRef = React.useRef<HTMLDivElement | null>(null)
36
+ const searchInputRef = React.useRef<HTMLInputElement | null>(null)
37
+ const activeCommandId = commands[activeIndex]?.id
38
+
39
+ const handleSearchKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
40
+ const stopComboboxKeyHandling = () => {
41
+ const baseUIEvent = event as React.KeyboardEvent<HTMLInputElement> & {
42
+ preventBaseUIHandler?: () => void
43
+ }
44
+
45
+ event.preventDefault()
46
+ event.stopPropagation()
47
+ baseUIEvent.preventBaseUIHandler?.()
48
+ }
49
+
50
+ if (event.key === 'ArrowDown') {
51
+ stopComboboxKeyHandling()
52
+ onActiveIndexChange(commands.length > 0 ? (activeIndex + 1) % commands.length : 0)
53
+ return
54
+ }
55
+
56
+ if (event.key === 'ArrowUp') {
57
+ stopComboboxKeyHandling()
58
+ onActiveIndexChange(
59
+ commands.length > 0 ? (activeIndex - 1 + commands.length) % commands.length : 0
60
+ )
61
+ return
62
+ }
63
+
64
+ if (event.key === 'Escape') {
65
+ stopComboboxKeyHandling()
66
+ onClose()
67
+ return
68
+ }
69
+
70
+ if (event.key === 'Enter' || event.key === 'Tab') {
71
+ const command = commands[activeIndex]
72
+ if (!command) return
73
+
74
+ stopComboboxKeyHandling()
75
+ onCommandSelect(command)
76
+ }
77
+ }
78
+
79
+ React.useEffect(() => {
80
+ if (!slashMenu) return
81
+
82
+ const focusSearchInput = () => {
83
+ searchInputRef.current?.focus({ preventScroll: true })
84
+ searchInputRef.current?.select()
85
+ }
86
+
87
+ focusSearchInput()
88
+ const frame = requestAnimationFrame(focusSearchInput)
89
+
90
+ return () => cancelAnimationFrame(frame)
91
+ }, [slashMenu])
92
+
93
+ React.useEffect(() => {
94
+ if (!activeCommandId) return
95
+
96
+ activeItemRef.current?.scrollIntoView({ block: 'nearest' })
97
+ }, [activeCommandId])
98
+
18
99
  if (!slashMenu) return null
19
100
 
101
+ const slashAnchor = {
102
+ getBoundingClientRect: () => slashMenu.rect
103
+ }
104
+
20
105
  return (
21
- <div
22
- className="z-50 w-72 max-w-[calc(100vw-2rem)] rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
23
- style={getSlashMenuStyle(slashMenu.rect)}
106
+ <Combobox<ContentEditorSlashCommand>
107
+ autoHighlight
108
+ filter={null}
109
+ filteredItems={commands}
110
+ inputValue={searchQuery}
111
+ itemToStringLabel={(command) => command.title}
112
+ itemToStringValue={(command) => command.id}
113
+ items={commands}
114
+ onInputValueChange={onSearchChange}
115
+ onItemHighlighted={(command) => {
116
+ if (!command) return
117
+
118
+ const nextIndex = commands.findIndex((item) => item.id === command.id)
119
+ if (nextIndex >= 0) onActiveIndexChange(nextIndex)
120
+ }}
121
+ onOpenChange={(isOpen) => {
122
+ if (!isOpen) onClose()
123
+ }}
124
+ onValueChange={(command) => {
125
+ if (command) onCommandSelect(command)
126
+ }}
127
+ open
128
+ value={null}
24
129
  >
25
- {commands.length > 0 ? (
26
- <div className="max-h-80 overflow-y-auto">
130
+ <ComboboxContent
131
+ anchor={slashAnchor}
132
+ align="start"
133
+ className="w-72 min-w-72 max-w-[calc(100vw-2rem)] p-2 gap-2 flex flex-col"
134
+ collisionPadding={8}
135
+ finalFocus={false}
136
+ initialFocus={false}
137
+ positionMethod="fixed"
138
+ side="bottom"
139
+ sideOffset={8}
140
+ data-content-editor-slash-menu
141
+ >
142
+ <ComboboxInput
143
+ aria-label="Search commands"
144
+ ref={searchInputRef}
145
+ onKeyDown={handleSearchKeyDown}
146
+ placeholder="Search commands..."
147
+ showClear
148
+ showTrigger={false}
149
+ className="border border-border"
150
+ />
151
+ <ComboboxEmpty className="py-6">No commands found</ComboboxEmpty>
152
+ <ComboboxList className="max-h-80 p-0">
27
153
  {commands.map((command, index) => {
28
154
  const Icon = command.Icon
29
155
  const isActive = index === activeIndex
30
156
 
31
157
  return (
32
- <button
158
+ <ComboboxItem
159
+ index={index}
33
160
  key={command.id}
34
- type="button"
161
+ data-active={isActive ? 'true' : undefined}
162
+ ref={(node) => {
163
+ if (isActive) activeItemRef.current = node
164
+ }}
165
+ onMouseDown={(event) => event.preventDefault()}
166
+ onMouseEnter={() => onActiveIndexChange(index)}
167
+ value={command}
35
168
  className={cn(
36
- 'flex w-full items-start gap-3 rounded-sm px-2.5 py-2 text-left text-sm outline-none transition-colors',
169
+ 'border border-transparent p-2 data-highlighted:bg-card! data-highlighted:border-border!',
37
170
  {
38
- 'bg-accent text-accent-foreground': isActive,
39
- 'text-popover-foreground hover:bg-accent hover:text-accent-foreground':
40
- !isActive
171
+ 'bg-card border-border shadow-input text-foreground': isActive
41
172
  }
42
173
  )}
43
- onMouseDown={(event) => event.preventDefault()}
44
- onMouseEnter={() => onActiveIndexChange(index)}
45
- onClick={() => onCommandSelect(command)}
46
174
  >
47
- <span className="mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-sm bg-muted">
48
- <Icon className="size-4" />
175
+ <span className="flex size-8 shrink-0 items-center justify-center rounded-sm bg-card border border-border shadow-input">
176
+ <Icon />
49
177
  </span>
50
- <span className="min-w-0">
51
- <span className="block font-medium leading-none">{command.title}</span>
52
- <span className="mt-1 block truncate text-xs text-muted-foreground">
178
+ <span className="min-w-0 space-y-1 flex flex-col w-full">
179
+ <span className="font-medium leading-none">{command.title}</span>
180
+ <span className="truncate text-xs text-muted-foreground">
53
181
  {command.description}
54
182
  </span>
55
183
  </span>
56
- </button>
184
+ </ComboboxItem>
57
185
  )
58
186
  })}
59
- </div>
60
- ) : (
61
- <div className="px-3 py-6 text-center text-sm text-muted-foreground">No commands found</div>
62
- )}
63
- </div>
187
+ </ComboboxList>
188
+ </ComboboxContent>
189
+ </Combobox>
64
190
  )
65
191
  }
@@ -40,7 +40,7 @@ export function SourceModeDropdownButton({
40
40
  onPointerDown={(event) => event.preventDefault()}
41
41
  onClick={() => setIsOpen((current) => !current)}
42
42
  >
43
- <Icon className="tiptap-button-icon" />
43
+ <Icon />
44
44
  <ChevronDownIcon className="tiptap-button-dropdown-small" />
45
45
  </Button>
46
46
  </DropdownMenuTrigger>
@@ -105,7 +105,6 @@ export function SourceMode({
105
105
  isMarkdownLinkActive,
106
106
  markdownLinkUrl,
107
107
  openMediaGalleryAtCursor,
108
- openMarkdownLink,
109
108
  redoMarkdownChange,
110
109
  removeHighlight,
111
110
  removeMarkdownLink,
@@ -140,7 +139,7 @@ export function SourceMode({
140
139
  aria-label="Undo"
141
140
  title="Undo"
142
141
  >
143
- <Undo2Icon className="tiptap-button-icon" />
142
+ <Undo2Icon />
144
143
  </Button>
145
144
  <Button
146
145
  type="button"
@@ -151,7 +150,7 @@ export function SourceMode({
151
150
  aria-label="Redo"
152
151
  title="Redo"
153
152
  >
154
- <Redo2Icon className="tiptap-button-icon" />
153
+ <Redo2Icon />
155
154
  </Button>
156
155
  </div>
157
156
 
@@ -178,7 +177,7 @@ export function SourceMode({
178
177
  title={option.label}
179
178
  onSelect={() => applyHeading(option.level)}
180
179
  >
181
- <Icon className="tiptap-button-icon" />
180
+ <Icon />
182
181
  <span className="tiptap-button-text">{option.label}</span>
183
182
  </DropdownMenuItem>
184
183
  )
@@ -196,7 +195,7 @@ export function SourceMode({
196
195
  title={option.label}
197
196
  onSelect={() => applyList(option.type)}
198
197
  >
199
- <Icon className="tiptap-button-icon" />
198
+ <Icon />
200
199
  <span className="tiptap-button-text">{option.label}</span>
201
200
  </DropdownMenuItem>
202
201
  )
@@ -211,7 +210,7 @@ export function SourceMode({
211
210
  aria-label="Blockquote"
212
211
  title="Blockquote"
213
212
  >
214
- <BlockquoteIcon className="tiptap-button-icon" />
213
+ <BlockquoteIcon />
215
214
  </Button>
216
215
  <Button
217
216
  type="button"
@@ -222,7 +221,7 @@ export function SourceMode({
222
221
  aria-label="Code Block"
223
222
  title="Code Block"
224
223
  >
225
- <CodeBlockIcon className="tiptap-button-icon" />
224
+ <CodeBlockIcon />
226
225
  </Button>
227
226
  <Button
228
227
  type="button"
@@ -233,7 +232,7 @@ export function SourceMode({
233
232
  aria-label="Insert divider"
234
233
  title="Divider"
235
234
  >
236
- <Minus className="tiptap-button-icon" />
235
+ <Minus />
237
236
  </Button>
238
237
  </div>
239
238
 
@@ -281,7 +280,7 @@ export function SourceMode({
281
280
  aria-label={mark.label}
282
281
  title={mark.label}
283
282
  >
284
- <mark.Icon className="tiptap-button-icon" />
283
+ <mark.Icon />
285
284
  </Button>
286
285
  ))}
287
286
  <SourceColorHighlightPopover
@@ -296,7 +295,6 @@ export function SourceMode({
296
295
  disabled={disabled}
297
296
  isActive={isMarkdownLinkActive}
298
297
  onOpenChange={handleMarkdownLinkPopoverOpenChange}
299
- openLink={openMarkdownLink}
300
298
  removeLink={removeMarkdownLink}
301
299
  setLink={setMarkdownLink}
302
300
  setUrl={setMarkdownLinkUrl}
@@ -322,7 +320,7 @@ export function SourceMode({
322
320
  aria-label="Add media"
323
321
  title="Add media"
324
322
  >
325
- <ImagePlusIcon className="tiptap-button-icon" />
323
+ <ImagePlusIcon />
326
324
  <span className="tiptap-button-text">Media</span>
327
325
  </Button>
328
326
  </div>
@@ -227,7 +227,7 @@ export function TableBubbleMenu({ editor }: { editor: Editor }) {
227
227
  }}
228
228
  title={label}
229
229
  >
230
- <Icon className="tiptap-button-icon" />
230
+ <Icon />
231
231
  </Button>
232
232
  )
233
233
  })}