@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,118 @@
|
|
|
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 { TextAlign, UseTextAlignConfig } from '.'
|
|
14
|
+
import { TEXT_ALIGN_SHORTCUT_KEYS, useTextAlign } from '.'
|
|
15
|
+
|
|
16
|
+
type IconProps = React.SVGProps<SVGSVGElement>
|
|
17
|
+
type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement
|
|
18
|
+
|
|
19
|
+
export interface TextAlignButtonProps extends Omit<ButtonProps, 'type'>, UseTextAlignConfig {
|
|
20
|
+
/**
|
|
21
|
+
* Optional text to display alongside the icon.
|
|
22
|
+
*/
|
|
23
|
+
text?: string
|
|
24
|
+
/**
|
|
25
|
+
* Optional show shortcut keys in the button.
|
|
26
|
+
* @default false
|
|
27
|
+
*/
|
|
28
|
+
showShortcut?: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Optional custom icon component to render instead of the default.
|
|
31
|
+
*/
|
|
32
|
+
icon?: React.MemoExoticComponent<IconComponent> | React.FC<IconProps>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function TextAlignShortcutBadge({
|
|
36
|
+
align,
|
|
37
|
+
shortcutKeys = TEXT_ALIGN_SHORTCUT_KEYS[align]
|
|
38
|
+
}: {
|
|
39
|
+
align: TextAlign
|
|
40
|
+
shortcutKeys?: string
|
|
41
|
+
}) {
|
|
42
|
+
return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Button component for setting text alignment in a Tiptap editor.
|
|
47
|
+
*
|
|
48
|
+
* For custom button implementations, use the `useTextAlign` hook instead.
|
|
49
|
+
*/
|
|
50
|
+
export const TextAlignButton = forwardRef<HTMLButtonElement, TextAlignButtonProps>(
|
|
51
|
+
(
|
|
52
|
+
{
|
|
53
|
+
editor: providedEditor,
|
|
54
|
+
align,
|
|
55
|
+
text,
|
|
56
|
+
hideWhenUnavailable = false,
|
|
57
|
+
onAligned,
|
|
58
|
+
showShortcut = false,
|
|
59
|
+
onClick,
|
|
60
|
+
icon: CustomIcon,
|
|
61
|
+
children,
|
|
62
|
+
...buttonProps
|
|
63
|
+
},
|
|
64
|
+
ref
|
|
65
|
+
) => {
|
|
66
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
67
|
+
const { isVisible, handleTextAlign, label, canAlign, isActive, Icon, shortcutKeys } =
|
|
68
|
+
useTextAlign({
|
|
69
|
+
editor,
|
|
70
|
+
align,
|
|
71
|
+
hideWhenUnavailable,
|
|
72
|
+
onAligned
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const handleClick = useCallback(
|
|
76
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
77
|
+
onClick?.(event)
|
|
78
|
+
if (event.defaultPrevented) return
|
|
79
|
+
handleTextAlign()
|
|
80
|
+
},
|
|
81
|
+
[handleTextAlign, onClick]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if (!isVisible) {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const RenderIcon = CustomIcon ?? Icon
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Button
|
|
92
|
+
type="button"
|
|
93
|
+
disabled={!canAlign}
|
|
94
|
+
variant="ghost"
|
|
95
|
+
data-active-state={isActive ? 'on' : 'off'}
|
|
96
|
+
data-disabled={!canAlign}
|
|
97
|
+
role="button"
|
|
98
|
+
tabIndex={-1}
|
|
99
|
+
aria-label={label}
|
|
100
|
+
aria-pressed={isActive}
|
|
101
|
+
tooltip={label}
|
|
102
|
+
onClick={handleClick}
|
|
103
|
+
{...buttonProps}
|
|
104
|
+
ref={ref}
|
|
105
|
+
>
|
|
106
|
+
{children ?? (
|
|
107
|
+
<>
|
|
108
|
+
<RenderIcon className="tiptap-button-icon" />
|
|
109
|
+
{text && <span className="tiptap-button-text">{text}</span>}
|
|
110
|
+
{showShortcut && <TextAlignShortcutBadge align={align} shortcutKeys={shortcutKeys} />}
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
113
|
+
</Button>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
TextAlignButton.displayName = 'TextAlignButton'
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ChainedCommands, 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 { isExtensionAvailable, isNodeTypeSelected } from '../../lib/tiptap-utils'
|
|
11
|
+
|
|
12
|
+
// --- Icons ---
|
|
13
|
+
import { AlignCenterIcon } from '../../tiptap-icons/align-center-icon'
|
|
14
|
+
import { AlignJustifyIcon } from '../../tiptap-icons/align-justify-icon'
|
|
15
|
+
import { AlignLeftIcon } from '../../tiptap-icons/align-left-icon'
|
|
16
|
+
import { AlignRightIcon } from '../../tiptap-icons/align-right-icon'
|
|
17
|
+
|
|
18
|
+
export type TextAlign = 'left' | 'center' | 'right' | 'justify'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for the text align functionality
|
|
22
|
+
*/
|
|
23
|
+
export interface UseTextAlignConfig {
|
|
24
|
+
/**
|
|
25
|
+
* The Tiptap editor instance.
|
|
26
|
+
*/
|
|
27
|
+
editor?: Editor | null
|
|
28
|
+
/**
|
|
29
|
+
* The text alignment to apply.
|
|
30
|
+
*/
|
|
31
|
+
align: TextAlign
|
|
32
|
+
/**
|
|
33
|
+
* Whether the button should hide when alignment is not available.
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
hideWhenUnavailable?: boolean
|
|
37
|
+
/**
|
|
38
|
+
* Callback function called after a successful alignment change.
|
|
39
|
+
*/
|
|
40
|
+
onAligned?: () => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const TEXT_ALIGN_SHORTCUT_KEYS: Record<TextAlign, string> = {
|
|
44
|
+
left: 'mod+shift+l',
|
|
45
|
+
center: 'mod+shift+e',
|
|
46
|
+
right: 'mod+shift+r',
|
|
47
|
+
justify: 'mod+shift+j'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const textAlignIcons = {
|
|
51
|
+
left: AlignLeftIcon,
|
|
52
|
+
center: AlignCenterIcon,
|
|
53
|
+
right: AlignRightIcon,
|
|
54
|
+
justify: AlignJustifyIcon
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const textAlignLabels: Record<TextAlign, string> = {
|
|
58
|
+
left: 'Align left',
|
|
59
|
+
center: 'Align center',
|
|
60
|
+
right: 'Align right',
|
|
61
|
+
justify: 'Align justify'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Checks if text alignment can be performed in the current editor state
|
|
66
|
+
*/
|
|
67
|
+
export function canSetTextAlign(editor: Editor | null, align: TextAlign): boolean {
|
|
68
|
+
if (!editor || !editor.isEditable) return false
|
|
69
|
+
if (
|
|
70
|
+
!isExtensionAvailable(editor, 'textAlign') ||
|
|
71
|
+
isNodeTypeSelected(editor, ['image', 'horizontalRule'])
|
|
72
|
+
)
|
|
73
|
+
return false
|
|
74
|
+
|
|
75
|
+
return editor.can().setTextAlign(align)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function hasSetTextAlign(commands: ChainedCommands): commands is ChainedCommands & {
|
|
79
|
+
setTextAlign: (align: TextAlign) => ChainedCommands
|
|
80
|
+
} {
|
|
81
|
+
return 'setTextAlign' in commands
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Checks if the text alignment is currently active
|
|
86
|
+
*/
|
|
87
|
+
export function isTextAlignActive(editor: Editor | null, align: TextAlign): boolean {
|
|
88
|
+
if (!editor || !editor.isEditable) return false
|
|
89
|
+
return editor.isActive({ textAlign: align })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sets text alignment in the editor
|
|
94
|
+
*/
|
|
95
|
+
export function setTextAlign(editor: Editor | null, align: TextAlign): boolean {
|
|
96
|
+
if (!editor || !editor.isEditable) return false
|
|
97
|
+
if (!canSetTextAlign(editor, align)) return false
|
|
98
|
+
|
|
99
|
+
const chain = editor.chain().focus()
|
|
100
|
+
if (hasSetTextAlign(chain)) {
|
|
101
|
+
return chain.setTextAlign(align).run()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Determines if the text align button should be shown
|
|
109
|
+
*/
|
|
110
|
+
export function shouldShowButton(props: {
|
|
111
|
+
editor: Editor | null
|
|
112
|
+
hideWhenUnavailable: boolean
|
|
113
|
+
align: TextAlign
|
|
114
|
+
}): boolean {
|
|
115
|
+
const { editor, hideWhenUnavailable, align } = props
|
|
116
|
+
|
|
117
|
+
if (!editor || !editor.isEditable) return false
|
|
118
|
+
|
|
119
|
+
if (!hideWhenUnavailable) {
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!isExtensionAvailable(editor, 'textAlign')) return false
|
|
124
|
+
|
|
125
|
+
if (!editor.isActive('code')) {
|
|
126
|
+
return canSetTextAlign(editor, align)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Custom hook that provides text align functionality for Tiptap editor
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```tsx
|
|
137
|
+
* // Simple usage
|
|
138
|
+
* function MySimpleAlignButton() {
|
|
139
|
+
* const { isVisible, handleTextAlign } = useTextAlign({ align: "center" })
|
|
140
|
+
*
|
|
141
|
+
* if (!isVisible) return null
|
|
142
|
+
*
|
|
143
|
+
* return <button onClick={handleTextAlign}>Align Center</button>
|
|
144
|
+
* }
|
|
145
|
+
*
|
|
146
|
+
* // Advanced usage with configuration
|
|
147
|
+
* function MyAdvancedAlignButton() {
|
|
148
|
+
* const { isVisible, handleTextAlign, label, isActive } = useTextAlign({
|
|
149
|
+
* editor: myEditor,
|
|
150
|
+
* align: "right",
|
|
151
|
+
* hideWhenUnavailable: true,
|
|
152
|
+
* onAligned: () => console.log('Text aligned!')
|
|
153
|
+
* })
|
|
154
|
+
*
|
|
155
|
+
* if (!isVisible) return null
|
|
156
|
+
*
|
|
157
|
+
* return (
|
|
158
|
+
* <MyButton
|
|
159
|
+
* onClick={handleTextAlign}
|
|
160
|
+
* aria-pressed={isActive}
|
|
161
|
+
* aria-label={label}
|
|
162
|
+
* >
|
|
163
|
+
* Align Right
|
|
164
|
+
* </MyButton>
|
|
165
|
+
* )
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export function useTextAlign(config: UseTextAlignConfig) {
|
|
170
|
+
const { editor: providedEditor, align, hideWhenUnavailable = false, onAligned } = config
|
|
171
|
+
|
|
172
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
173
|
+
const [isVisible, setIsVisible] = useState<boolean>(true)
|
|
174
|
+
const canAlign = canSetTextAlign(editor, align)
|
|
175
|
+
const isActive = isTextAlignActive(editor, align)
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!editor) return
|
|
179
|
+
|
|
180
|
+
const handleSelectionUpdate = () => {
|
|
181
|
+
setIsVisible(shouldShowButton({ editor, align, hideWhenUnavailable }))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
handleSelectionUpdate()
|
|
185
|
+
|
|
186
|
+
editor.on('selectionUpdate', handleSelectionUpdate)
|
|
187
|
+
|
|
188
|
+
return () => {
|
|
189
|
+
editor.off('selectionUpdate', handleSelectionUpdate)
|
|
190
|
+
}
|
|
191
|
+
}, [editor, hideWhenUnavailable, align])
|
|
192
|
+
|
|
193
|
+
const handleTextAlign = useCallback(() => {
|
|
194
|
+
if (!editor) return false
|
|
195
|
+
|
|
196
|
+
const success = setTextAlign(editor, align)
|
|
197
|
+
if (success) {
|
|
198
|
+
onAligned?.()
|
|
199
|
+
}
|
|
200
|
+
return success
|
|
201
|
+
}, [editor, align, onAligned])
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
isVisible,
|
|
205
|
+
isActive,
|
|
206
|
+
handleTextAlign,
|
|
207
|
+
canAlign,
|
|
208
|
+
label: textAlignLabels[align],
|
|
209
|
+
shortcutKeys: TEXT_ALIGN_SHORTCUT_KEYS[align],
|
|
210
|
+
Icon: textAlignIcons[align]
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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 { UndoRedoAction, UseUndoRedoConfig } from '.'
|
|
14
|
+
import { UNDO_REDO_SHORTCUT_KEYS, useUndoRedo } from '.'
|
|
15
|
+
|
|
16
|
+
export interface UndoRedoButtonProps extends Omit<ButtonProps, 'type'>, UseUndoRedoConfig {
|
|
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 HistoryShortcutBadge({
|
|
29
|
+
action,
|
|
30
|
+
shortcutKeys = UNDO_REDO_SHORTCUT_KEYS[action]
|
|
31
|
+
}: {
|
|
32
|
+
action: UndoRedoAction
|
|
33
|
+
shortcutKeys?: string
|
|
34
|
+
}) {
|
|
35
|
+
return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Button component for triggering undo/redo actions in a Tiptap editor.
|
|
40
|
+
*
|
|
41
|
+
* For custom button implementations, use the `useHistory` hook instead.
|
|
42
|
+
*/
|
|
43
|
+
export const UndoRedoButton = forwardRef<HTMLButtonElement, UndoRedoButtonProps>(
|
|
44
|
+
(
|
|
45
|
+
{
|
|
46
|
+
editor: providedEditor,
|
|
47
|
+
action,
|
|
48
|
+
text,
|
|
49
|
+
hideWhenUnavailable = false,
|
|
50
|
+
onExecuted,
|
|
51
|
+
showShortcut = false,
|
|
52
|
+
onClick,
|
|
53
|
+
children,
|
|
54
|
+
...buttonProps
|
|
55
|
+
},
|
|
56
|
+
ref
|
|
57
|
+
) => {
|
|
58
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
59
|
+
const { isVisible, handleAction, label, canExecute, Icon, shortcutKeys } = useUndoRedo({
|
|
60
|
+
editor,
|
|
61
|
+
action,
|
|
62
|
+
hideWhenUnavailable,
|
|
63
|
+
onExecuted
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const handleClick = useCallback(
|
|
67
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
68
|
+
onClick?.(event)
|
|
69
|
+
if (event.defaultPrevented) return
|
|
70
|
+
handleAction()
|
|
71
|
+
},
|
|
72
|
+
[handleAction, onClick]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (!isVisible) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Button
|
|
81
|
+
type="button"
|
|
82
|
+
disabled={!canExecute}
|
|
83
|
+
variant="ghost"
|
|
84
|
+
data-disabled={!canExecute}
|
|
85
|
+
role="button"
|
|
86
|
+
tabIndex={-1}
|
|
87
|
+
aria-label={label}
|
|
88
|
+
tooltip={label}
|
|
89
|
+
onClick={handleClick}
|
|
90
|
+
{...buttonProps}
|
|
91
|
+
ref={ref}
|
|
92
|
+
>
|
|
93
|
+
{children ?? (
|
|
94
|
+
<>
|
|
95
|
+
<Icon className="tiptap-button-icon" />
|
|
96
|
+
{text && <span className="tiptap-button-text">{text}</span>}
|
|
97
|
+
{showShortcut && <HistoryShortcutBadge action={action} shortcutKeys={shortcutKeys} />}
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
100
|
+
</Button>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
UndoRedoButton.displayName = 'UndoRedoButton'
|
|
@@ -0,0 +1,173 @@
|
|
|
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 { isNodeTypeSelected } from '../../lib/tiptap-utils'
|
|
11
|
+
|
|
12
|
+
// --- Icons ---
|
|
13
|
+
import { Redo2Icon } from '../../tiptap-icons/redo2-icon'
|
|
14
|
+
import { Undo2Icon } from '../../tiptap-icons/undo2-icon'
|
|
15
|
+
|
|
16
|
+
export type UndoRedoAction = 'undo' | 'redo'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Configuration for the history functionality
|
|
20
|
+
*/
|
|
21
|
+
export interface UseUndoRedoConfig {
|
|
22
|
+
/**
|
|
23
|
+
* The Tiptap editor instance.
|
|
24
|
+
*/
|
|
25
|
+
editor?: Editor | null
|
|
26
|
+
/**
|
|
27
|
+
* The history action to perform (undo or redo).
|
|
28
|
+
*/
|
|
29
|
+
action: UndoRedoAction
|
|
30
|
+
/**
|
|
31
|
+
* Whether the button should hide when action is not available.
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
hideWhenUnavailable?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Callback function called after a successful action execution.
|
|
37
|
+
*/
|
|
38
|
+
onExecuted?: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const UNDO_REDO_SHORTCUT_KEYS: Record<UndoRedoAction, string> = {
|
|
42
|
+
undo: 'mod+z',
|
|
43
|
+
redo: 'mod+shift+z'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const historyActionLabels: Record<UndoRedoAction, string> = {
|
|
47
|
+
undo: 'Undo',
|
|
48
|
+
redo: 'Redo'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const historyIcons = {
|
|
52
|
+
undo: Undo2Icon,
|
|
53
|
+
redo: Redo2Icon
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Checks if a history action can be executed
|
|
58
|
+
*/
|
|
59
|
+
export function canExecuteUndoRedoAction(editor: Editor | null, action: UndoRedoAction): boolean {
|
|
60
|
+
if (!editor || !editor.isEditable) return false
|
|
61
|
+
if (isNodeTypeSelected(editor, ['image'])) return false
|
|
62
|
+
|
|
63
|
+
return action === 'undo' ? editor.can().undo() : editor.can().redo()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Executes a history action on the editor
|
|
68
|
+
*/
|
|
69
|
+
export function executeUndoRedoAction(editor: Editor | null, action: UndoRedoAction): boolean {
|
|
70
|
+
if (!editor || !editor.isEditable) return false
|
|
71
|
+
if (!canExecuteUndoRedoAction(editor, action)) return false
|
|
72
|
+
|
|
73
|
+
const chain = editor.chain().focus()
|
|
74
|
+
return action === 'undo' ? chain.undo().run() : chain.redo().run()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Determines if the history button should be shown
|
|
79
|
+
*/
|
|
80
|
+
export function shouldShowButton(props: {
|
|
81
|
+
editor: Editor | null
|
|
82
|
+
hideWhenUnavailable: boolean
|
|
83
|
+
action: UndoRedoAction
|
|
84
|
+
}): boolean {
|
|
85
|
+
const { editor, hideWhenUnavailable, action } = props
|
|
86
|
+
|
|
87
|
+
if (!editor || !editor.isEditable) return false
|
|
88
|
+
|
|
89
|
+
if (hideWhenUnavailable && !editor.isActive('code')) {
|
|
90
|
+
return canExecuteUndoRedoAction(editor, action)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Custom hook that provides history functionality for Tiptap editor
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```tsx
|
|
101
|
+
* // Simple usage
|
|
102
|
+
* function MySimpleUndoButton() {
|
|
103
|
+
* const { isVisible, handleAction } = useHistory({ action: "undo" })
|
|
104
|
+
*
|
|
105
|
+
* if (!isVisible) return null
|
|
106
|
+
*
|
|
107
|
+
* return <button onClick={handleAction}>Undo</button>
|
|
108
|
+
* }
|
|
109
|
+
*
|
|
110
|
+
* // Advanced usage with configuration
|
|
111
|
+
* function MyAdvancedRedoButton() {
|
|
112
|
+
* const { isVisible, handleAction, label } = useHistory({
|
|
113
|
+
* editor: myEditor,
|
|
114
|
+
* action: "redo",
|
|
115
|
+
* hideWhenUnavailable: true,
|
|
116
|
+
* onExecuted: () => console.log('Action executed!')
|
|
117
|
+
* })
|
|
118
|
+
*
|
|
119
|
+
* if (!isVisible) return null
|
|
120
|
+
*
|
|
121
|
+
* return (
|
|
122
|
+
* <MyButton
|
|
123
|
+
* onClick={handleAction}
|
|
124
|
+
* aria-label={label}
|
|
125
|
+
* >
|
|
126
|
+
* Redo
|
|
127
|
+
* </MyButton>
|
|
128
|
+
* )
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function useUndoRedo(config: UseUndoRedoConfig) {
|
|
133
|
+
const { editor: providedEditor, action, hideWhenUnavailable = false, onExecuted } = config
|
|
134
|
+
|
|
135
|
+
const { editor } = useTiptapEditor(providedEditor)
|
|
136
|
+
const [isVisible, setIsVisible] = useState<boolean>(true)
|
|
137
|
+
const canExecute = canExecuteUndoRedoAction(editor, action)
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!editor) return
|
|
141
|
+
|
|
142
|
+
const handleUpdate = () => {
|
|
143
|
+
setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, action }))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
handleUpdate()
|
|
147
|
+
|
|
148
|
+
editor.on('transaction', handleUpdate)
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
editor.off('transaction', handleUpdate)
|
|
152
|
+
}
|
|
153
|
+
}, [editor, hideWhenUnavailable, action])
|
|
154
|
+
|
|
155
|
+
const handleAction = useCallback(() => {
|
|
156
|
+
if (!editor) return false
|
|
157
|
+
|
|
158
|
+
const success = executeUndoRedoAction(editor, action)
|
|
159
|
+
if (success) {
|
|
160
|
+
onExecuted?.()
|
|
161
|
+
}
|
|
162
|
+
return success
|
|
163
|
+
}, [editor, action, onExecuted])
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
isVisible,
|
|
167
|
+
handleAction,
|
|
168
|
+
canExecute,
|
|
169
|
+
label: historyActionLabels[action],
|
|
170
|
+
shortcutKeys: UNDO_REDO_SHORTCUT_KEYS[action],
|
|
171
|
+
Icon: historyIcons[action]
|
|
172
|
+
}
|
|
173
|
+
}
|