@betterstart/cli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -0
- package/dist/cli.d.ts +1 -9
- package/dist/cli.js +13484 -354
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +24 -260
- package/dist/index.js +4 -11373
- package/dist/index.js.map +1 -1
- package/package.json +29 -42
- package/templates/schema.json +959 -0
- package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
- package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
- package/templates/tiptap/hooks/use-element-rect.ts +166 -0
- package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
- package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
- package/templates/tiptap/hooks/use-scrolling.ts +64 -0
- package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
- package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
- package/templates/tiptap/hooks/use-unmount.ts +21 -0
- package/templates/tiptap/hooks/use-window-size.ts +87 -0
- package/templates/tiptap/lib/tiptap-utils.ts +587 -0
- package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
- package/templates/tiptap/styles/_variables.scss +296 -0
- package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
- package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
- package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
- package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
- package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
- package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
- package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
- package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
- package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
- package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
- package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
- package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
- package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
- package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
- package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
- package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
- package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
- package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
- package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
- package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
- package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
- package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
- package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
- package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
- package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
- package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
- package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
- package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
- package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
- package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
- package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
- package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
- package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
- package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
- package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
- package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
- package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
- package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
- package/templates/ui/accordion.tsx +52 -0
- package/templates/ui/alert-dialog.tsx +116 -0
- package/templates/ui/alert.tsx +48 -0
- package/templates/ui/aspect-ratio.tsx +7 -0
- package/templates/ui/avatar.tsx +46 -0
- package/templates/ui/badge.tsx +32 -0
- package/templates/ui/breadcrumb.tsx +98 -0
- package/templates/ui/button-group.tsx +77 -0
- package/templates/ui/button.tsx +48 -0
- package/templates/ui/calendar.tsx +176 -0
- package/templates/ui/card.tsx +54 -0
- package/templates/ui/carousel.tsx +234 -0
- package/templates/ui/chart.tsx +349 -0
- package/templates/ui/checkbox.tsx +27 -0
- package/templates/ui/collapsible.tsx +11 -0
- package/templates/ui/command.tsx +142 -0
- package/templates/ui/context-menu.tsx +188 -0
- package/templates/ui/curriculum-editor.tsx +601 -0
- package/templates/ui/date-picker.tsx +70 -0
- package/templates/ui/dialog.tsx +103 -0
- package/templates/ui/drawer.tsx +99 -0
- package/templates/ui/dropdown-menu.tsx +185 -0
- package/templates/ui/dynamic-list-field.tsx +95 -0
- package/templates/ui/empty.tsx +90 -0
- package/templates/ui/field.tsx +231 -0
- package/templates/ui/file-upload-example.tsx +113 -0
- package/templates/ui/form.tsx +172 -0
- package/templates/ui/hover-card.tsx +28 -0
- package/templates/ui/icon-picker.tsx +435 -0
- package/templates/ui/icons-data.ts +6 -0
- package/templates/ui/image-upload-field.tsx +360 -0
- package/templates/ui/input-group.tsx +160 -0
- package/templates/ui/input-otp.tsx +70 -0
- package/templates/ui/input.tsx +21 -0
- package/templates/ui/item.tsx +171 -0
- package/templates/ui/kbd.tsx +28 -0
- package/templates/ui/label.tsx +20 -0
- package/templates/ui/logo.tsx +113 -0
- package/templates/ui/markdown-editor.tsx +303 -0
- package/templates/ui/markdown-utils.ts +128 -0
- package/templates/ui/media-upload-field.tsx +255 -0
- package/templates/ui/menubar.tsx +230 -0
- package/templates/ui/navigation-menu.tsx +119 -0
- package/templates/ui/pagination.tsx +96 -0
- package/templates/ui/placeholder.tsx +25 -0
- package/templates/ui/popover.tsx +32 -0
- package/templates/ui/progress.tsx +24 -0
- package/templates/ui/radio-group.tsx +37 -0
- package/templates/ui/resizable.tsx +41 -0
- package/templates/ui/rich-text-editor.tsx +374 -0
- package/templates/ui/scroll-area.tsx +45 -0
- package/templates/ui/select.tsx +151 -0
- package/templates/ui/separator.tsx +25 -0
- package/templates/ui/sheet.tsx +120 -0
- package/templates/ui/sidebar.tsx +684 -0
- package/templates/ui/skeleton.tsx +7 -0
- package/templates/ui/slider.tsx +24 -0
- package/templates/ui/sonner.tsx +29 -0
- package/templates/ui/spinner.tsx +15 -0
- package/templates/ui/switch.tsx +28 -0
- package/templates/ui/table.tsx +93 -0
- package/templates/ui/tabs.tsx +54 -0
- package/templates/ui/textarea.tsx +20 -0
- package/templates/ui/toast.tsx +127 -0
- package/templates/ui/toggle-group.tsx +56 -0
- package/templates/ui/toggle.tsx +43 -0
- package/templates/ui/tooltip.tsx +31 -0
- package/templates/ui/use-mobile.tsx +19 -0
- package/templates/ui/video-upload-field.tsx +368 -0
- package/dist/chunk-G4KI4DVB.js +0 -179
- package/dist/chunk-G4KI4DVB.js.map +0 -1
- package/dist/chunk-NKRQYAS6.js +0 -260
- package/dist/chunk-NKRQYAS6.js.map +0 -1
- package/dist/chunk-QLVSHP7X.js +0 -235
- package/dist/chunk-QLVSHP7X.js.map +0 -1
- package/dist/chunk-WY6BC55D.js +0 -357
- package/dist/chunk-WY6BC55D.js.map +0 -1
- package/dist/config/index.d.ts +0 -93
- package/dist/config/index.js +0 -58
- package/dist/config/index.js.map +0 -1
- package/dist/core/index.d.ts +0 -415
- package/dist/core/index.js +0 -906
- package/dist/core/index.js.map +0 -1
- package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
- package/dist/logger-awLb347n.d.ts +0 -81
- package/dist/plugins/index.d.ts +0 -213
- package/dist/plugins/index.js +0 -365
- package/dist/plugins/index.js.map +0 -1
- package/dist/types-ByX_gl6y.d.ts +0 -232
- package/dist/types-eI549DEG.d.ts +0 -331
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './list-dropdown-menu'
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Editor } from '@tiptap/react'
|
|
4
|
+
import { useCallback, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
// --- Hooks ---
|
|
7
|
+
import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
|
|
8
|
+
|
|
9
|
+
// --- Icons ---
|
|
10
|
+
import { ChevronDownIcon } from '../../tiptap-icons/chevron-down-icon'
|
|
11
|
+
// --- UI Primitives ---
|
|
12
|
+
import type { ButtonProps } from '../../tiptap-ui-primitive/button'
|
|
13
|
+
import { Button, ButtonGroup } from '../../tiptap-ui-primitive/button'
|
|
14
|
+
import { Card, CardBody } from '../../tiptap-ui-primitive/card'
|
|
15
|
+
import {
|
|
16
|
+
DropdownMenu,
|
|
17
|
+
DropdownMenuContent,
|
|
18
|
+
DropdownMenuItem,
|
|
19
|
+
DropdownMenuTrigger
|
|
20
|
+
} from '../../tiptap-ui-primitive/dropdown-menu'
|
|
21
|
+
// --- Tiptap UI ---
|
|
22
|
+
import { ListButton, type ListType } from '../list-button'
|
|
23
|
+
import { useListDropdownMenu } from './use-list-dropdown-menu'
|
|
24
|
+
|
|
25
|
+
export interface ListDropdownMenuProps extends Omit<ButtonProps, 'type'> {
|
|
26
|
+
/**
|
|
27
|
+
* The Tiptap editor instance.
|
|
28
|
+
*/
|
|
29
|
+
editor?: Editor
|
|
30
|
+
/**
|
|
31
|
+
* The list types to display in the dropdown.
|
|
32
|
+
*/
|
|
33
|
+
types?: ListType[]
|
|
34
|
+
/**
|
|
35
|
+
* Whether the dropdown should be hidden when no list types are available
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
hideWhenUnavailable?: boolean
|
|
39
|
+
/**
|
|
40
|
+
* Callback for when the dropdown opens or closes
|
|
41
|
+
*/
|
|
42
|
+
onOpenChange?: (isOpen: boolean) => void
|
|
43
|
+
/**
|
|
44
|
+
* Whether to render the dropdown menu in a portal
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
portal?: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function ListDropdownMenu({
|
|
51
|
+
editor: providedEditor,
|
|
52
|
+
types = ['bulletList', 'orderedList', 'taskList'],
|
|
53
|
+
hideWhenUnavailable = false,
|
|
54
|
+
onOpenChange,
|
|
55
|
+
portal = false,
|
|
56
|
+
...props
|
|
57
|
+
}: ListDropdownMenuProps) {
|
|
58
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
59
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
60
|
+
|
|
61
|
+
const { filteredLists, canToggle, isActive, isVisible, Icon } = useListDropdownMenu({
|
|
62
|
+
editor,
|
|
63
|
+
types,
|
|
64
|
+
hideWhenUnavailable
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const handleOnOpenChange = useCallback(
|
|
68
|
+
(open: boolean) => {
|
|
69
|
+
setIsOpen(open)
|
|
70
|
+
onOpenChange?.(open)
|
|
71
|
+
},
|
|
72
|
+
[onOpenChange]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (!isVisible) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<DropdownMenu open={isOpen} onOpenChange={handleOnOpenChange}>
|
|
81
|
+
<DropdownMenuTrigger asChild>
|
|
82
|
+
<Button
|
|
83
|
+
type="button"
|
|
84
|
+
variant="ghost"
|
|
85
|
+
data-active-state={isActive ? 'on' : 'off'}
|
|
86
|
+
role="button"
|
|
87
|
+
tabIndex={-1}
|
|
88
|
+
disabled={!canToggle}
|
|
89
|
+
data-disabled={!canToggle}
|
|
90
|
+
aria-label="List options"
|
|
91
|
+
tooltip="List"
|
|
92
|
+
{...props}
|
|
93
|
+
onPointerDown={(e) => e.preventDefault()}
|
|
94
|
+
onClick={() => handleOnOpenChange(!isOpen)}
|
|
95
|
+
>
|
|
96
|
+
<Icon className="tiptap-button-icon" />
|
|
97
|
+
<ChevronDownIcon className="tiptap-button-dropdown-small" />
|
|
98
|
+
</Button>
|
|
99
|
+
</DropdownMenuTrigger>
|
|
100
|
+
|
|
101
|
+
<DropdownMenuContent align="start" portal={portal}>
|
|
102
|
+
<Card>
|
|
103
|
+
<CardBody>
|
|
104
|
+
<ButtonGroup>
|
|
105
|
+
{filteredLists.map((option) => (
|
|
106
|
+
<DropdownMenuItem key={option.type} asChild>
|
|
107
|
+
<ListButton
|
|
108
|
+
editor={editor}
|
|
109
|
+
type={option.type}
|
|
110
|
+
text={option.label}
|
|
111
|
+
showTooltip={false}
|
|
112
|
+
/>
|
|
113
|
+
</DropdownMenuItem>
|
|
114
|
+
))}
|
|
115
|
+
</ButtonGroup>
|
|
116
|
+
</CardBody>
|
|
117
|
+
</Card>
|
|
118
|
+
</DropdownMenuContent>
|
|
119
|
+
</DropdownMenu>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default ListDropdownMenu
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Editor } from '@tiptap/react'
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
// --- Hooks ---
|
|
7
|
+
import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
|
|
8
|
+
// --- Lib ---
|
|
9
|
+
import { isNodeInSchema } from '../../lib/tiptap-utils'
|
|
10
|
+
// --- Icons ---
|
|
11
|
+
import { ListIcon } from '../../tiptap-icons/list-icon'
|
|
12
|
+
import { ListOrderedIcon } from '../../tiptap-icons/list-ordered-icon'
|
|
13
|
+
import { ListTodoIcon } from '../../tiptap-icons/list-todo-icon'
|
|
14
|
+
|
|
15
|
+
// --- Tiptap UI ---
|
|
16
|
+
import { canToggleList, isListActive, type ListType, listIcons } from '../list-button'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Configuration for the list dropdown menu functionality
|
|
20
|
+
*/
|
|
21
|
+
export interface UseListDropdownMenuConfig {
|
|
22
|
+
/**
|
|
23
|
+
* The Tiptap editor instance.
|
|
24
|
+
*/
|
|
25
|
+
editor?: Editor | null
|
|
26
|
+
/**
|
|
27
|
+
* The list types to display in the dropdown.
|
|
28
|
+
* @default ["bulletList", "orderedList", "taskList"]
|
|
29
|
+
*/
|
|
30
|
+
types?: ListType[]
|
|
31
|
+
/**
|
|
32
|
+
* Whether the dropdown should be hidden when no list types are available
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
hideWhenUnavailable?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ListOption {
|
|
39
|
+
label: string
|
|
40
|
+
type: ListType
|
|
41
|
+
icon: React.ElementType
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const listOptions: ListOption[] = [
|
|
45
|
+
{
|
|
46
|
+
label: 'Bullet List',
|
|
47
|
+
type: 'bulletList',
|
|
48
|
+
icon: ListIcon
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: 'Ordered List',
|
|
52
|
+
type: 'orderedList',
|
|
53
|
+
icon: ListOrderedIcon
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
label: 'Task List',
|
|
57
|
+
type: 'taskList',
|
|
58
|
+
icon: ListTodoIcon
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
export function canToggleAnyList(editor: Editor | null, listTypes: ListType[]): boolean {
|
|
63
|
+
if (!editor || !editor.isEditable) return false
|
|
64
|
+
return listTypes.some((type) => canToggleList(editor, type))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isAnyListActive(editor: Editor | null, listTypes: ListType[]): boolean {
|
|
68
|
+
if (!editor || !editor.isEditable) return false
|
|
69
|
+
return listTypes.some((type) => isListActive(editor, type))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getFilteredListOptions(availableTypes: ListType[]): typeof listOptions {
|
|
73
|
+
return listOptions.filter((option) => !option.type || availableTypes.includes(option.type))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function shouldShowListDropdown(params: {
|
|
77
|
+
editor: Editor | null
|
|
78
|
+
listTypes: ListType[]
|
|
79
|
+
hideWhenUnavailable: boolean
|
|
80
|
+
listInSchema: boolean
|
|
81
|
+
canToggleAny: boolean
|
|
82
|
+
}): boolean {
|
|
83
|
+
const { editor, hideWhenUnavailable, listInSchema, canToggleAny } = params
|
|
84
|
+
|
|
85
|
+
if (!editor) return false
|
|
86
|
+
|
|
87
|
+
if (!hideWhenUnavailable) {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!listInSchema) return false
|
|
92
|
+
|
|
93
|
+
if (!editor.isActive('code')) {
|
|
94
|
+
return canToggleAny
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Gets the currently active list type from the available types
|
|
102
|
+
*/
|
|
103
|
+
export function getActiveListType(
|
|
104
|
+
editor: Editor | null,
|
|
105
|
+
availableTypes: ListType[]
|
|
106
|
+
): ListType | undefined {
|
|
107
|
+
if (!editor || !editor.isEditable) return undefined
|
|
108
|
+
return availableTypes.find((type) => isListActive(editor, type))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Custom hook that provides list dropdown menu functionality for Tiptap editor
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```tsx
|
|
116
|
+
* // Simple usage
|
|
117
|
+
* function MyListDropdown() {
|
|
118
|
+
* const {
|
|
119
|
+
* isVisible,
|
|
120
|
+
* activeType,
|
|
121
|
+
* isAnyActive,
|
|
122
|
+
* canToggleAny,
|
|
123
|
+
* filteredLists,
|
|
124
|
+
* } = useListDropdownMenu()
|
|
125
|
+
*
|
|
126
|
+
* if (!isVisible) return null
|
|
127
|
+
*
|
|
128
|
+
* return (
|
|
129
|
+
* <DropdownMenu>
|
|
130
|
+
* // dropdown content
|
|
131
|
+
* </DropdownMenu>
|
|
132
|
+
* )
|
|
133
|
+
* }
|
|
134
|
+
*
|
|
135
|
+
* // Advanced usage with configuration
|
|
136
|
+
* function MyAdvancedListDropdown() {
|
|
137
|
+
* const {
|
|
138
|
+
* isVisible,
|
|
139
|
+
* activeType,
|
|
140
|
+
* } = useListDropdownMenu({
|
|
141
|
+
* editor: myEditor,
|
|
142
|
+
* types: ["bulletList", "orderedList"],
|
|
143
|
+
* hideWhenUnavailable: true,
|
|
144
|
+
* })
|
|
145
|
+
*
|
|
146
|
+
* // component implementation
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function useListDropdownMenu(config?: UseListDropdownMenuConfig) {
|
|
151
|
+
const {
|
|
152
|
+
editor: providedEditor,
|
|
153
|
+
types = ['bulletList', 'orderedList', 'taskList'],
|
|
154
|
+
hideWhenUnavailable = false
|
|
155
|
+
} = config || {}
|
|
156
|
+
|
|
157
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
158
|
+
const [isVisible, setIsVisible] = useState(true)
|
|
159
|
+
|
|
160
|
+
const listInSchema = types.some((type) => isNodeInSchema(type, editor))
|
|
161
|
+
|
|
162
|
+
const filteredLists = useMemo(() => getFilteredListOptions(types), [types])
|
|
163
|
+
|
|
164
|
+
const canToggleAny = canToggleAnyList(editor, types)
|
|
165
|
+
const isAnyActive = isAnyListActive(editor, types)
|
|
166
|
+
const activeType = getActiveListType(editor, types)
|
|
167
|
+
const activeList = filteredLists.find((option) => option.type === activeType)
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!editor) return
|
|
171
|
+
|
|
172
|
+
const handleSelectionUpdate = () => {
|
|
173
|
+
setIsVisible(
|
|
174
|
+
shouldShowListDropdown({
|
|
175
|
+
editor,
|
|
176
|
+
listTypes: types,
|
|
177
|
+
hideWhenUnavailable,
|
|
178
|
+
listInSchema,
|
|
179
|
+
canToggleAny
|
|
180
|
+
})
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
handleSelectionUpdate()
|
|
185
|
+
|
|
186
|
+
editor.on('selectionUpdate', handleSelectionUpdate)
|
|
187
|
+
|
|
188
|
+
return () => {
|
|
189
|
+
editor.off('selectionUpdate', handleSelectionUpdate)
|
|
190
|
+
}
|
|
191
|
+
}, [canToggleAny, editor, hideWhenUnavailable, listInSchema, types])
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
isVisible,
|
|
195
|
+
activeType,
|
|
196
|
+
isActive: isAnyActive,
|
|
197
|
+
canToggle: canToggleAny,
|
|
198
|
+
types,
|
|
199
|
+
filteredLists,
|
|
200
|
+
label: 'List',
|
|
201
|
+
Icon: activeList ? listIcons[activeList.type] : ListIcon
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useCallback } from 'react'
|
|
4
|
+
// --- Hooks ---
|
|
5
|
+
import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
|
|
6
|
+
// --- Lib ---
|
|
7
|
+
import { parseShortcutKeys } from '../../lib/tiptap-utils'
|
|
8
|
+
import { Badge } from '../../tiptap-ui-primitive/badge'
|
|
9
|
+
// --- UI Primitives ---
|
|
10
|
+
import type { ButtonProps } from '../../tiptap-ui-primitive/button'
|
|
11
|
+
import { Button } from '../../tiptap-ui-primitive/button'
|
|
12
|
+
// --- Tiptap UI ---
|
|
13
|
+
import type { Mark, UseMarkConfig } from '.'
|
|
14
|
+
import { MARK_SHORTCUT_KEYS, useMark } from '.'
|
|
15
|
+
|
|
16
|
+
export interface MarkButtonProps extends Omit<ButtonProps, 'type'>, UseMarkConfig {
|
|
17
|
+
/**
|
|
18
|
+
* Optional text to display alongside the icon.
|
|
19
|
+
*/
|
|
20
|
+
text?: string
|
|
21
|
+
/**
|
|
22
|
+
* Optional show shortcut keys in the button.
|
|
23
|
+
* @default false
|
|
24
|
+
*/
|
|
25
|
+
showShortcut?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function MarkShortcutBadge({
|
|
29
|
+
type,
|
|
30
|
+
shortcutKeys = MARK_SHORTCUT_KEYS[type]
|
|
31
|
+
}: {
|
|
32
|
+
type: Mark
|
|
33
|
+
shortcutKeys?: string
|
|
34
|
+
}) {
|
|
35
|
+
return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Button component for toggling marks in a Tiptap editor.
|
|
40
|
+
*
|
|
41
|
+
* For custom button implementations, use the `useMark` hook instead.
|
|
42
|
+
*/
|
|
43
|
+
export const MarkButton = forwardRef<HTMLButtonElement, MarkButtonProps>(
|
|
44
|
+
(
|
|
45
|
+
{
|
|
46
|
+
editor: providedEditor,
|
|
47
|
+
type,
|
|
48
|
+
text,
|
|
49
|
+
hideWhenUnavailable = false,
|
|
50
|
+
onToggled,
|
|
51
|
+
showShortcut = false,
|
|
52
|
+
onClick,
|
|
53
|
+
children,
|
|
54
|
+
...buttonProps
|
|
55
|
+
},
|
|
56
|
+
ref
|
|
57
|
+
) => {
|
|
58
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
59
|
+
const { isVisible, handleMark, label, canToggle, isActive, Icon, shortcutKeys } = useMark({
|
|
60
|
+
editor,
|
|
61
|
+
type,
|
|
62
|
+
hideWhenUnavailable,
|
|
63
|
+
onToggled
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const handleClick = useCallback(
|
|
67
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
68
|
+
onClick?.(event)
|
|
69
|
+
if (event.defaultPrevented) return
|
|
70
|
+
handleMark()
|
|
71
|
+
},
|
|
72
|
+
[handleMark, onClick]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (!isVisible) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Button
|
|
81
|
+
type="button"
|
|
82
|
+
disabled={!canToggle}
|
|
83
|
+
variant="ghost"
|
|
84
|
+
data-active-state={isActive ? 'on' : 'off'}
|
|
85
|
+
data-disabled={!canToggle}
|
|
86
|
+
role="button"
|
|
87
|
+
tabIndex={-1}
|
|
88
|
+
aria-label={label}
|
|
89
|
+
aria-pressed={isActive}
|
|
90
|
+
tooltip={label}
|
|
91
|
+
onClick={handleClick}
|
|
92
|
+
{...buttonProps}
|
|
93
|
+
ref={ref}
|
|
94
|
+
>
|
|
95
|
+
{children ?? (
|
|
96
|
+
<>
|
|
97
|
+
<Icon className="tiptap-button-icon" />
|
|
98
|
+
{text && <span className="tiptap-button-text">{text}</span>}
|
|
99
|
+
{showShortcut && <MarkShortcutBadge type={type} shortcutKeys={shortcutKeys} />}
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
</Button>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
MarkButton.displayName = 'MarkButton'
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Editor } from '@tiptap/react'
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
// --- Hooks ---
|
|
7
|
+
import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
|
|
8
|
+
|
|
9
|
+
// --- Lib ---
|
|
10
|
+
import { isMarkInSchema, isNodeTypeSelected } from '../../lib/tiptap-utils'
|
|
11
|
+
|
|
12
|
+
// --- Icons ---
|
|
13
|
+
import { BoldIcon } from '../../tiptap-icons/bold-icon'
|
|
14
|
+
import { Code2Icon } from '../../tiptap-icons/code2-icon'
|
|
15
|
+
import { ItalicIcon } from '../../tiptap-icons/italic-icon'
|
|
16
|
+
import { StrikeIcon } from '../../tiptap-icons/strike-icon'
|
|
17
|
+
import { SubscriptIcon } from '../../tiptap-icons/subscript-icon'
|
|
18
|
+
import { SuperscriptIcon } from '../../tiptap-icons/superscript-icon'
|
|
19
|
+
import { UnderlineIcon } from '../../tiptap-icons/underline-icon'
|
|
20
|
+
|
|
21
|
+
export type Mark = 'bold' | 'italic' | 'strike' | 'code' | 'underline' | 'superscript' | 'subscript'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for the mark functionality
|
|
25
|
+
*/
|
|
26
|
+
export interface UseMarkConfig {
|
|
27
|
+
/**
|
|
28
|
+
* The Tiptap editor instance.
|
|
29
|
+
*/
|
|
30
|
+
editor?: Editor | null
|
|
31
|
+
/**
|
|
32
|
+
* The type of mark to toggle
|
|
33
|
+
*/
|
|
34
|
+
type: Mark
|
|
35
|
+
/**
|
|
36
|
+
* Whether the button should hide when mark is not available.
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
hideWhenUnavailable?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* Callback function called after a successful mark toggle.
|
|
42
|
+
*/
|
|
43
|
+
onToggled?: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const markIcons = {
|
|
47
|
+
bold: BoldIcon,
|
|
48
|
+
italic: ItalicIcon,
|
|
49
|
+
underline: UnderlineIcon,
|
|
50
|
+
strike: StrikeIcon,
|
|
51
|
+
code: Code2Icon,
|
|
52
|
+
superscript: SuperscriptIcon,
|
|
53
|
+
subscript: SubscriptIcon
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const MARK_SHORTCUT_KEYS: Record<Mark, string> = {
|
|
57
|
+
bold: 'mod+b',
|
|
58
|
+
italic: 'mod+i',
|
|
59
|
+
underline: 'mod+u',
|
|
60
|
+
strike: 'mod+shift+s',
|
|
61
|
+
code: 'mod+e',
|
|
62
|
+
superscript: 'mod+.',
|
|
63
|
+
subscript: 'mod+,'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks if a mark can be toggled in the current editor state
|
|
68
|
+
*/
|
|
69
|
+
export function canToggleMark(editor: Editor | null, type: Mark): boolean {
|
|
70
|
+
if (!editor || !editor.isEditable) return false
|
|
71
|
+
if (!isMarkInSchema(type, editor) || isNodeTypeSelected(editor, ['image'])) return false
|
|
72
|
+
|
|
73
|
+
return editor.can().toggleMark(type)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks if a mark is currently active
|
|
78
|
+
*/
|
|
79
|
+
export function isMarkActive(editor: Editor | null, type: Mark): boolean {
|
|
80
|
+
if (!editor || !editor.isEditable) return false
|
|
81
|
+
return editor.isActive(type)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Toggles a mark in the editor
|
|
86
|
+
*/
|
|
87
|
+
export function toggleMark(editor: Editor | null, type: Mark): boolean {
|
|
88
|
+
if (!editor || !editor.isEditable) return false
|
|
89
|
+
if (!canToggleMark(editor, type)) return false
|
|
90
|
+
|
|
91
|
+
return editor.chain().focus().toggleMark(type).run()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Determines if the mark button should be shown
|
|
96
|
+
*/
|
|
97
|
+
export function shouldShowButton(props: {
|
|
98
|
+
editor: Editor | null
|
|
99
|
+
type: Mark
|
|
100
|
+
hideWhenUnavailable: boolean
|
|
101
|
+
}): boolean {
|
|
102
|
+
const { editor, type, hideWhenUnavailable } = props
|
|
103
|
+
|
|
104
|
+
if (!editor || !editor.isEditable) return false
|
|
105
|
+
|
|
106
|
+
if (!hideWhenUnavailable) {
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!isMarkInSchema(type, editor)) return false
|
|
111
|
+
|
|
112
|
+
if (!editor.isActive('code')) {
|
|
113
|
+
return canToggleMark(editor, type)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Gets the formatted mark name
|
|
121
|
+
*/
|
|
122
|
+
export function getFormattedMarkName(type: Mark): string {
|
|
123
|
+
return type.charAt(0).toUpperCase() + type.slice(1)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Custom hook that provides mark functionality for Tiptap editor
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```tsx
|
|
131
|
+
* // Simple usage
|
|
132
|
+
* function MySimpleBoldButton() {
|
|
133
|
+
* const { isVisible, handleMark } = useMark({ type: "bold" })
|
|
134
|
+
*
|
|
135
|
+
* if (!isVisible) return null
|
|
136
|
+
*
|
|
137
|
+
* return <button onClick={handleMark}>Bold</button>
|
|
138
|
+
* }
|
|
139
|
+
*
|
|
140
|
+
* // Advanced usage with configuration
|
|
141
|
+
* function MyAdvancedItalicButton() {
|
|
142
|
+
* const { isVisible, handleMark, label, isActive } = useMark({
|
|
143
|
+
* editor: myEditor,
|
|
144
|
+
* type: "italic",
|
|
145
|
+
* hideWhenUnavailable: true,
|
|
146
|
+
* onToggled: () => console.log('Mark toggled!')
|
|
147
|
+
* })
|
|
148
|
+
*
|
|
149
|
+
* if (!isVisible) return null
|
|
150
|
+
*
|
|
151
|
+
* return (
|
|
152
|
+
* <MyButton
|
|
153
|
+
* onClick={handleMark}
|
|
154
|
+
* aria-pressed={isActive}
|
|
155
|
+
* aria-label={label}
|
|
156
|
+
* >
|
|
157
|
+
* Italic
|
|
158
|
+
* </MyButton>
|
|
159
|
+
* )
|
|
160
|
+
* }
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export function useMark(config: UseMarkConfig) {
|
|
164
|
+
const { editor: providedEditor, type, hideWhenUnavailable = false, onToggled } = config
|
|
165
|
+
|
|
166
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
167
|
+
const [isVisible, setIsVisible] = useState<boolean>(true)
|
|
168
|
+
const canToggle = canToggleMark(editor, type)
|
|
169
|
+
const isActive = isMarkActive(editor, type)
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!editor) return
|
|
173
|
+
|
|
174
|
+
const handleSelectionUpdate = () => {
|
|
175
|
+
setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable }))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
handleSelectionUpdate()
|
|
179
|
+
|
|
180
|
+
editor.on('selectionUpdate', handleSelectionUpdate)
|
|
181
|
+
|
|
182
|
+
return () => {
|
|
183
|
+
editor.off('selectionUpdate', handleSelectionUpdate)
|
|
184
|
+
}
|
|
185
|
+
}, [editor, type, hideWhenUnavailable])
|
|
186
|
+
|
|
187
|
+
const handleMark = useCallback(() => {
|
|
188
|
+
if (!editor) return false
|
|
189
|
+
|
|
190
|
+
const success = toggleMark(editor, type)
|
|
191
|
+
if (success) {
|
|
192
|
+
onToggled?.()
|
|
193
|
+
}
|
|
194
|
+
return success
|
|
195
|
+
}, [editor, type, onToggled])
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
isVisible,
|
|
199
|
+
isActive,
|
|
200
|
+
handleMark,
|
|
201
|
+
canToggle,
|
|
202
|
+
label: getFormattedMarkName(type),
|
|
203
|
+
shortcutKeys: MARK_SHORTCUT_KEYS[type],
|
|
204
|
+
Icon: markIcons[type]
|
|
205
|
+
}
|
|
206
|
+
}
|