tiptapify 0.1.5 → 0.2.0

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 (43) hide show
  1. package/README.md +1 -1
  2. package/dist/tiptapify.css +1 -1
  3. package/dist/tiptapify.mjs +30351 -26471
  4. package/dist/tiptapify.umd.js +57 -52
  5. package/index.d.ts +71 -1
  6. package/package.json +52 -45
  7. package/src/components/Tiptapify.vue +38 -1
  8. package/src/components/Toolbar/ai.ts +13 -0
  9. package/src/components/Toolbar/items.ts +2 -0
  10. package/src/components/editorExtensions.ts +9 -3
  11. package/src/components/index.ts +4 -3
  12. package/src/constants/style.test.ts +49 -0
  13. package/src/extensions/PickerEventBus.test.ts +38 -0
  14. package/src/extensions/components/ai/Button.vue +42 -0
  15. package/src/extensions/components/ai/Dialog.vue +262 -0
  16. package/src/extensions/components/media/link/LinkDialog.vue +5 -5
  17. package/src/i18n/locales/ar.json +29 -0
  18. package/src/i18n/locales/ch.json +29 -0
  19. package/src/i18n/locales/cz.json +29 -0
  20. package/src/i18n/locales/de.json +29 -0
  21. package/src/i18n/locales/en.json +29 -0
  22. package/src/i18n/locales/es.json +29 -0
  23. package/src/i18n/locales/fi.json +29 -0
  24. package/src/i18n/locales/fr.json +29 -0
  25. package/src/i18n/locales/hu.json +29 -0
  26. package/src/i18n/locales/it.json +29 -0
  27. package/src/i18n/locales/ja.json +29 -0
  28. package/src/i18n/locales/ko.json +29 -0
  29. package/src/i18n/locales/la.json +29 -0
  30. package/src/i18n/locales/lt.json +29 -0
  31. package/src/i18n/locales/nl.json +29 -0
  32. package/src/i18n/locales/pl.json +29 -0
  33. package/src/i18n/locales/pt.json +29 -0
  34. package/src/i18n/locales/ru.json +29 -0
  35. package/src/i18n/locales/se.json +29 -0
  36. package/src/i18n/locales/th.json +29 -0
  37. package/src/i18n/locales/tr.json +29 -0
  38. package/src/i18n/locales/uk.json +29 -0
  39. package/src/i18n/locales/vi.json +29 -0
  40. package/src/index.ts +15 -1
  41. package/src/types/editor.ts +74 -1
  42. package/src/types/toolbarTypes.ts +2 -1
  43. package/src/utils/helpers.test.ts +32 -0
package/index.d.ts CHANGED
@@ -32,6 +32,7 @@ import type {
32
32
  SingleCommands,
33
33
  } from '@tiptap/core'
34
34
 
35
+ import '@tiptap/extensions'
35
36
  import '@tiptap/extension-text-style'
36
37
  import '@tiptap/extension-list'
37
38
  import '@tiptap/extensions'
@@ -42,6 +43,7 @@ import '@tiptap/extension-heading'
42
43
  import '@tiptap/extension-bold'
43
44
  import '@tiptap/extension-italic'
44
45
  import '@tiptap/extension-strike'
46
+ import '@tiptap/extension-link'
45
47
  import '@tiptap/extension-code'
46
48
  import '@tiptap/extension-blockquote'
47
49
  import '@tiptap/extension-hard-break'
@@ -66,6 +68,74 @@ export interface TiptapifyOptions {
66
68
  i18n?: string
67
69
  }
68
70
 
71
+ export type TiptapifyAiMode = 'insert' | 'replace' | 'append'
72
+
73
+ export interface TiptapifyAiPromptExample {
74
+ title: string
75
+ prompt: string
76
+ }
77
+
78
+ export type TiptapifyAiChatRole = 'system' | 'user' | 'assistant' | 'developer' | 'tool'
79
+
80
+ export interface TiptapifyAiChatMessage {
81
+ role: TiptapifyAiChatRole
82
+ content: string
83
+ }
84
+
85
+ export interface TiptapifyAiEditorContext {
86
+ prompt: string
87
+ selectedText: string
88
+ text: string
89
+ html: string
90
+ json: object
91
+ mode: TiptapifyAiMode
92
+ }
93
+
94
+ export interface TiptapifyAiRequest {
95
+ model?: string
96
+ messages: TiptapifyAiChatMessage[]
97
+ temperature?: number
98
+ stream?: false
99
+ [key: string]: unknown
100
+ }
101
+
102
+ export interface TiptapifyAiResponse {
103
+ content: string
104
+ }
105
+
106
+ export interface TiptapifyAiOpenAiResponse {
107
+ choices?: Array<{
108
+ message?: {
109
+ content?: string
110
+ }
111
+ text?: string
112
+ }>
113
+ }
114
+
115
+ export type TiptapifyAiProvider = (request: TiptapifyAiRequest, context: TiptapifyAiEditorContext) => Promise<TiptapifyAiResponse | TiptapifyAiOpenAiResponse | string>
116
+
117
+ export type TiptapifyAiTokenProvider = () => Promise<string | null> | string | null
118
+
119
+ export interface TiptapifyAiStorage {
120
+ getItem: (key: string) => string | null | Promise<string | null>
121
+ setItem: (key: string, value: string) => void | Promise<void>
122
+ removeItem: (key: string) => void | Promise<void>
123
+ }
124
+
125
+ export interface TiptapifyAiOptions {
126
+ aiProvider?: TiptapifyAiProvider
127
+ model?: string
128
+ promptExamples?: TiptapifyAiPromptExample[]
129
+ mode?: TiptapifyAiMode
130
+ defaultPrompt?: string
131
+ systemPrompt?: string
132
+ temperature?: number
133
+ chatCompletionOptions?: Record<string, unknown>
134
+ createMessages?: (context: TiptapifyAiEditorContext) => TiptapifyAiChatMessage[]
135
+ tokenProvider?: TiptapifyAiTokenProvider
136
+ storage?: TiptapifyAiStorage
137
+ }
138
+
69
139
  declare const TiptapifyPlugin: {
70
140
  install: (app: never, options?: TiptapifyOptions) => void
71
141
  }
@@ -114,4 +184,4 @@ declare module '@vue/runtime-core' {
114
184
  interface GlobalComponents {
115
185
  Tiptapify: typeof Tiptapify
116
186
  }
117
- }
187
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tiptapify",
3
3
  "types": "./index.d.ts",
4
- "version": "0.1.5",
4
+ "version": "0.2.0",
5
5
  "description": "Tiptap3 editor with Vuetify3 menu implementation",
6
6
  "exports": {
7
7
  ".": {
@@ -30,7 +30,8 @@
30
30
  "dev": "vite",
31
31
  "build": "vite build",
32
32
  "lint": "eslint resources/ts --fix --ext .ts,.js,.cjs,.vue,.tsx,.jsx",
33
- "test": "echo \"Error: no test specified\" && exit 1",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest",
34
35
  "build:charmap": "tsx build-charmap.ts",
35
36
  "build:emojis": "tsx build-emojis.ts",
36
37
  "docs:dev": "pnpm --filter tiptapify-docs docs:dev",
@@ -55,6 +56,10 @@
55
56
  "material design icons",
56
57
  "wysiwyg",
57
58
  "wysiwyg toolbar",
59
+ "rich text editor",
60
+ "vue editor",
61
+ "vue rich text",
62
+ "vue wysiwyg",
58
63
  "markdown",
59
64
  "emoji",
60
65
  "emoticons"
@@ -65,45 +70,45 @@
65
70
  "packageManager": "pnpm@11.8.0",
66
71
  "dependencies": {
67
72
  "@floating-ui/dom": "^1.7.6",
68
- "@tiptap/core": "^3.26.1",
69
- "@tiptap/extension-blockquote": "^3.26.1",
70
- "@tiptap/extension-bold": "^3.26.1",
71
- "@tiptap/extension-bubble-menu": "^3.26.1",
72
- "@tiptap/extension-code": "^3.26.1",
73
- "@tiptap/extension-code-block": "^3.26.1",
74
- "@tiptap/extension-code-block-lowlight": "^3.26.1",
75
- "@tiptap/extension-color": "^3.26.1",
76
- "@tiptap/extension-document": "^3.26.1",
77
- "@tiptap/extension-floating-menu": "^3.26.1",
78
- "@tiptap/extension-font-family": "^3.26.1",
79
- "@tiptap/extension-hard-break": "^3.26.1",
80
- "@tiptap/extension-heading": "^3.26.1",
81
- "@tiptap/extension-highlight": "^3.26.1",
82
- "@tiptap/extension-horizontal-rule": "^3.26.1",
83
- "@tiptap/extension-image": "^3.26.1",
84
- "@tiptap/extension-invisible-characters": "^3.26.1",
85
- "@tiptap/extension-italic": "^3.26.1",
86
- "@tiptap/extension-link": "^3.26.1",
87
- "@tiptap/extension-list": "^3.26.1",
88
- "@tiptap/extension-list-item": "^3.26.1",
89
- "@tiptap/extension-paragraph": "^3.26.1",
90
- "@tiptap/extension-placeholder": "^3.26.1",
91
- "@tiptap/extension-strike": "^3.26.1",
92
- "@tiptap/extension-subscript": "^3.26.1",
93
- "@tiptap/extension-superscript": "^3.26.1",
94
- "@tiptap/extension-table": "^3.26.1",
95
- "@tiptap/extension-task-item": "^3.26.1",
96
- "@tiptap/extension-task-list": "^3.26.1",
97
- "@tiptap/extension-text": "^3.26.1",
98
- "@tiptap/extension-text-align": "^3.26.1",
99
- "@tiptap/extension-text-style": "^3.26.1",
100
- "@tiptap/extension-typography": "^3.26.1",
101
- "@tiptap/extension-underline": "^3.26.1",
102
- "@tiptap/extension-youtube": "^3.26.1",
103
- "@tiptap/extensions": "^3.26.1",
104
- "@tiptap/pm": "^3.26.1",
105
- "@tiptap/suggestion": "^3.26.1",
106
- "@tiptap/vue-3": "^3.26.1",
73
+ "@tiptap/core": "^3.27.1",
74
+ "@tiptap/extension-blockquote": "^3.27.1",
75
+ "@tiptap/extension-bold": "^3.27.1",
76
+ "@tiptap/extension-bubble-menu": "^3.27.1",
77
+ "@tiptap/extension-code": "^3.27.1",
78
+ "@tiptap/extension-code-block": "^3.27.1",
79
+ "@tiptap/extension-code-block-lowlight": "^3.27.1",
80
+ "@tiptap/extension-color": "^3.27.1",
81
+ "@tiptap/extension-document": "^3.27.1",
82
+ "@tiptap/extension-floating-menu": "^3.27.1",
83
+ "@tiptap/extension-font-family": "^3.27.1",
84
+ "@tiptap/extension-hard-break": "^3.27.1",
85
+ "@tiptap/extension-heading": "^3.27.1",
86
+ "@tiptap/extension-highlight": "^3.27.1",
87
+ "@tiptap/extension-horizontal-rule": "^3.27.1",
88
+ "@tiptap/extension-image": "^3.27.1",
89
+ "@tiptap/extension-invisible-characters": "^3.27.1",
90
+ "@tiptap/extension-italic": "^3.27.1",
91
+ "@tiptap/extension-link": "^3.27.1",
92
+ "@tiptap/extension-list": "^3.27.1",
93
+ "@tiptap/extension-list-item": "^3.27.1",
94
+ "@tiptap/extension-paragraph": "^3.27.1",
95
+ "@tiptap/extension-placeholder": "^3.27.1",
96
+ "@tiptap/extension-strike": "^3.27.1",
97
+ "@tiptap/extension-subscript": "^3.27.1",
98
+ "@tiptap/extension-superscript": "^3.27.1",
99
+ "@tiptap/extension-table": "^3.27.1",
100
+ "@tiptap/extension-task-item": "^3.27.1",
101
+ "@tiptap/extension-task-list": "^3.27.1",
102
+ "@tiptap/extension-text": "^3.27.1",
103
+ "@tiptap/extension-text-align": "^3.27.1",
104
+ "@tiptap/extension-text-style": "^3.27.1",
105
+ "@tiptap/extension-typography": "^3.27.1",
106
+ "@tiptap/extension-underline": "^3.27.1",
107
+ "@tiptap/extension-youtube": "^3.27.1",
108
+ "@tiptap/extensions": "^3.27.1",
109
+ "@tiptap/pm": "^3.27.1",
110
+ "@tiptap/suggestion": "^3.27.1",
111
+ "@tiptap/vue-3": "^3.27.1",
107
112
  "emoji.json": "^16.0.0",
108
113
  "highlight.js": "^11.11.1",
109
114
  "linkifyjs": "^4.3.3",
@@ -116,8 +121,8 @@
116
121
  "vuetify": "^3.8.5 || ^4.0.0"
117
122
  },
118
123
  "devDependencies": {
119
- "@intlify/unplugin-vue-i18n": "^11.2.3",
120
- "@types/node": "^25.9.3",
124
+ "@intlify/unplugin-vue-i18n": "^11.2.4",
125
+ "@types/node": "^26.0.0",
121
126
  "@vitejs/plugin-vue": "^6.0.7",
122
127
  "@vitejs/plugin-vue-jsx": "^5.1.5",
123
128
  "eslint": "^10.5.0",
@@ -127,8 +132,9 @@
127
132
  "eslint-plugin-promise": "^7.3.0",
128
133
  "eslint-plugin-regexp": "3.1.0",
129
134
  "eslint-plugin-sonarjs": "2.0.4",
130
- "eslint-plugin-unicorn": "^67.0.0",
135
+ "eslint-plugin-unicorn": "^68.0.0",
131
136
  "eslint-plugin-vue": "^10.9.2",
137
+ "jsdom": "^29.1.1",
132
138
  "sass-embedded": "^1.100.0",
133
139
  "stylelint": "^17.13.0",
134
140
  "stylelint-config-standard-scss": "17.0.0",
@@ -139,7 +145,8 @@
139
145
  "vite": "^8.0.16",
140
146
  "vite-plugin-vuetify": "^2.1.3",
141
147
  "vite-svg-loader": "^5.1.1",
148
+ "vitest": "^4.1.9",
142
149
  "vue-eslint-parser": "^10.4.1",
143
150
  "vue-tsc": "^3.3.5"
144
151
  }
145
- }
152
+ }
@@ -6,7 +6,7 @@ import { SlashCommandsConfig } from '@tiptapify/types/slashCommandsTypes'
6
6
  import { computed, onBeforeUnmount, PropType, provide, ref, ShallowRef } from 'vue'
7
7
  import { default as Toolbar } from '@tiptapify/components/Toolbar/Index.vue'
8
8
  import { Editor, EditorContent } from '@tiptap/vue-3'
9
- import { TiptapifyEditor, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
9
+ import { TiptapifyAiOptions, TiptapifyEditor, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
10
10
  import MenuBubble from '@tiptapify/components/MenuBubble.vue'
11
11
  import MenuFloating from '@tiptapify/components/MenuFloating.vue'
12
12
 
@@ -32,10 +32,12 @@ const props = defineProps({
32
32
  placeholder: { type: String, default () { return '' } },
33
33
  showWordsCount: { type: Boolean, default () { return true } },
34
34
  showCharactersCount: { type: Boolean, default () { return true } },
35
+ limit: { type: Number as PropType<number | null>, default () { return null } },
35
36
  defaultFontFamily: { type: String, default () { return 'Inter' } },
36
37
  fontMeasure: { type: String, default () { return 'px' } },
37
38
  rounded: { type: String, default () { return '0' } },
38
39
  customExtensions: { type: Array as PropType<toolbarSections>, default() { return [] } },
40
+ ai: { type: [Boolean, Object] as PropType<boolean | TiptapifyAiOptions>, default() { return false } },
39
41
  interactiveStyles: { type: Boolean, default() { return true } },
40
42
  loading: { type: Boolean, default() { return false } },
41
43
  loadingColor: { type: String, default() { return 'default' } },
@@ -49,6 +51,39 @@ const { t } = useI18n()
49
51
  const appTheme = useTheme()
50
52
  const currentTheme = ref(appTheme.global.name)
51
53
 
54
+ const defaultAiPromptExamples = computed(() => [
55
+ {
56
+ title: t('ai.defaultExamples.improve.title'),
57
+ prompt: t('ai.defaultExamples.improve.prompt'),
58
+ },
59
+ {
60
+ title: t('ai.defaultExamples.proofReading.title'),
61
+ prompt: t('ai.defaultExamples.proofReading.prompt'),
62
+ },
63
+ {
64
+ title: t('ai.defaultExamples.summarize.title'),
65
+ prompt: t('ai.defaultExamples.summarize.prompt'),
66
+ },
67
+ {
68
+ title: t('ai.defaultExamples.expand.title'),
69
+ prompt: t('ai.defaultExamples.expand.prompt'),
70
+ },
71
+ ])
72
+
73
+ const tiptapifyAi = computed(() => {
74
+ if (props.ai === false) {
75
+ return false
76
+ }
77
+
78
+ const options = props.ai === true ? {} : props.ai
79
+
80
+ return {
81
+ ...options,
82
+ enabled: true,
83
+ promptExamples: options.promptExamples?.length ? options.promptExamples : defaultAiPromptExamples.value,
84
+ }
85
+ })
86
+
52
87
  function contentChanged() {
53
88
  emit('content-changed', { html: editor.value?.getHTML(), json: editor.value?.getJSON() })
54
89
  }
@@ -58,6 +93,7 @@ const editor: ShallowRef<Editor | undefined> = getTiptapEditor(
58
93
  computed(() => props.placeholder || t('content.placeholder')).value,
59
94
  props.slashCommands,
60
95
  props.customExtensions,
96
+ props.limit,
61
97
  contentChanged,
62
98
  (editorInstance: Editor) => {
63
99
  (<TiptapifyEditor>editorInstance).interactiveStyles = props.interactiveStyles
@@ -73,6 +109,7 @@ const emit = defineEmits(['update:modelValue', 'editor-ready', 'content-changed'
73
109
 
74
110
  provide('tiptapifyEditor', editor)
75
111
  provide('tiptapifyI18n', { t })
112
+ provide('tiptapifyAi', tiptapifyAi)
76
113
 
77
114
  editor.value?.chain().setFontFamily(props.defaultFontFamily).run()
78
115
 
@@ -0,0 +1,13 @@
1
+ import { default as AiButton } from '@tiptapify/extensions/components/ai/Button.vue'
2
+ import { markRaw } from 'vue'
3
+
4
+ export default {
5
+ section: 'ai',
6
+ group: true,
7
+ components: [
8
+ {
9
+ name: 'ai',
10
+ component: markRaw(AiButton),
11
+ }
12
+ ]
13
+ }
@@ -1,4 +1,5 @@
1
1
  import actions from '@tiptapify/components/Toolbar/actions'
2
+ import ai from '@tiptapify/components/Toolbar/ai'
2
3
  import alignment from '@tiptapify/components/Toolbar/alignment'
3
4
  import format from '@tiptapify/components/Toolbar/format'
4
5
  import formatExtra from '@tiptapify/components/Toolbar/formatExtra'
@@ -15,6 +16,7 @@ const items = {
15
16
  alignment,
16
17
  list,
17
18
  actions,
19
+ ai,
18
20
  misc,
19
21
  }
20
22
 
@@ -1,4 +1,5 @@
1
1
  import { VueNodeViewRenderer } from '@tiptap/vue-3'
2
+ import { Gapcursor } from '@tiptap/extensions'
2
3
  import { TextStyleKit } from '@tiptap/extension-text-style'
3
4
  import { BulletList, OrderedList, ListItem, ListKeymap, TaskList, TaskItem } from '@tiptap/extension-list'
4
5
  import { Selection, Focus, Placeholder, UndoRedo, Dropcursor, CharacterCount } from '@tiptap/extensions'
@@ -9,6 +10,7 @@ import { Heading } from '@tiptap/extension-heading'
9
10
  import { Bold } from '@tiptap/extension-bold'
10
11
  import { Italic } from '@tiptap/extension-italic'
11
12
  import { Strike } from '@tiptap/extension-strike'
13
+ import { Link } from '@tiptap/extension-link'
12
14
  import { Code } from '@tiptap/extension-code'
13
15
  import { Blockquote } from '@tiptap/extension-blockquote'
14
16
  import { HardBreak } from '@tiptap/extension-hard-break'
@@ -51,8 +53,11 @@ const lowlight = createLowlight(common)
51
53
  // register language example
52
54
  // lowlight.register('ts', ts)
53
55
 
54
- export function editorExtensions (placeholder: string, slashCommands: SlashCommandsConfig, customExtensions: toolbarSections) {
56
+ export function editorExtensions (placeholder: string, slashCommands: SlashCommandsConfig, customExtensions: toolbarSections, limit: number | null = null) {
57
+ const characterCount = limit && limit > 0 ? CharacterCount.configure({ limit }) : CharacterCount
58
+
55
59
  const extensions = [
60
+ Gapcursor,
56
61
  TextStyleKit,
57
62
  Document,
58
63
  Text,
@@ -61,6 +66,7 @@ export function editorExtensions (placeholder: string, slashCommands: SlashComma
61
66
  Bold,
62
67
  Italic,
63
68
  Strike,
69
+ Link,
64
70
  Blockquote,
65
71
  OrderedList,
66
72
  BulletList,
@@ -107,7 +113,7 @@ export function editorExtensions (placeholder: string, slashCommands: SlashComma
107
113
  Selection.configure({ className: 'selection' }),
108
114
  TextAlign.configure({ types: ['heading', 'paragraph'] }),
109
115
  Placeholder.configure({ placeholder }),
110
- CharacterCount,
116
+ characterCount,
111
117
  InvisibleCharacters.configure({
112
118
  visible: false,
113
119
  }),
@@ -135,4 +141,4 @@ export function editorExtensions (placeholder: string, slashCommands: SlashComma
135
141
  }
136
142
 
137
143
  return extensions
138
- }
144
+ }
@@ -9,15 +9,16 @@ export function getTiptapEditor (
9
9
  placeholder: string,
10
10
  slashCommands: SlashCommandsConfig = true,
11
11
  customExtensions: toolbarSections,
12
+ limit: number | null = null,
12
13
  onUpdate: Function = () => {},
13
14
  onCreate?: (editor: Editor) => void,
14
15
  ): ShallowRef<Editor | undefined> {
15
- const extensions = editorExtensions(placeholder, slashCommands, customExtensions)
16
+ const extensions = editorExtensions(placeholder, slashCommands, customExtensions, limit)
16
17
 
17
18
  return useEditor({
18
19
  content,
19
20
  extensions,
20
21
  onUpdate: ({ editor }) => onUpdate(),
21
- onCreate: onCreate ? ({ editor }) => onCreate(editor) : undefined,
22
+ onCreate: onCreate ? ({ editor }) => onCreate(editor as Editor) : undefined,
22
23
  })
23
- }
24
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { setHeadingLevels, headingLevels, fontSizes, defaultFontSize, lineHeights, defaultLineHeight } from './style'
3
+
4
+ describe('style constants', () => {
5
+ beforeEach(() => {
6
+ headingLevels.value = [1, 2, 3, 4, 5, 6]
7
+ })
8
+
9
+ it('sets custom heading levels', () => {
10
+ setHeadingLevels([2, 3, 4])
11
+ expect(headingLevels.value).toEqual([2, 3, 4])
12
+ })
13
+
14
+ it('does not change heading levels on empty array', () => {
15
+ setHeadingLevels([])
16
+ expect(headingLevels.value).toEqual([1, 2, 3, 4, 5, 6])
17
+ })
18
+
19
+ it('rejects levels below 1', () => {
20
+ expect(() => setHeadingLevels([0])).toThrow('customHeadingLevels must be between 1 and 6')
21
+ })
22
+
23
+ it('rejects levels above 6', () => {
24
+ expect(() => setHeadingLevels([7])).toThrow('customHeadingLevels must be between 1 and 6')
25
+ })
26
+
27
+ it('rejects any invalid level in a mixed array', () => {
28
+ expect(() => setHeadingLevels([1, 2, 7])).toThrow('customHeadingLevels must be between 1 and 6')
29
+ })
30
+
31
+ it('has valid fontSizes constant', () => {
32
+ expect(fontSizes).toContain(12)
33
+ expect(fontSizes).toContain(6)
34
+ expect(fontSizes).toContain(96)
35
+ })
36
+
37
+ it('has valid defaultFontSize', () => {
38
+ expect(defaultFontSize).toBe(12)
39
+ })
40
+
41
+ it('has valid lineHeights constant', () => {
42
+ expect(lineHeights).toContain(1)
43
+ expect(lineHeights).toContain(4)
44
+ })
45
+
46
+ it('has valid defaultLineHeight', () => {
47
+ expect(defaultLineHeight).toBe(1)
48
+ })
49
+ })
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { PickerEventBus } from './PickerEventBus'
3
+
4
+ describe('PickerEventBus', () => {
5
+ it('calls registered callback on emit', () => {
6
+ const callback = vi.fn()
7
+ PickerEventBus.on('close', callback)
8
+ PickerEventBus.emit('close', { type: 'emoji' })
9
+ expect(callback).toHaveBeenCalledWith({ type: 'emoji' })
10
+ })
11
+
12
+ it('does not call callback after off', () => {
13
+ const callback = vi.fn()
14
+ PickerEventBus.on('close', callback)
15
+ PickerEventBus.off('close', callback)
16
+ PickerEventBus.emit('close', { type: 'charmap' })
17
+ expect(callback).not.toHaveBeenCalled()
18
+ })
19
+
20
+ it('supports multiple listeners', () => {
21
+ const callback1 = vi.fn()
22
+ const callback2 = vi.fn()
23
+ PickerEventBus.on('close', callback1)
24
+ PickerEventBus.on('close', callback2)
25
+ PickerEventBus.emit('close', { type: 'emoji' })
26
+ expect(callback1).toHaveBeenCalledTimes(1)
27
+ expect(callback2).toHaveBeenCalledTimes(1)
28
+ })
29
+
30
+ it('does not throw when emitting with no listeners', () => {
31
+ expect(() => PickerEventBus.emit('close', { type: 'emoji' })).not.toThrow()
32
+ })
33
+
34
+ it('does not throw when removing unregistered listener', () => {
35
+ const callback = vi.fn()
36
+ expect(() => PickerEventBus.off('close', callback)).not.toThrow()
37
+ })
38
+ })
@@ -0,0 +1,42 @@
1
+ <script lang="ts" setup>
2
+
3
+ import * as mdi from '@mdi/js'
4
+ import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
5
+ import AiDialog from '@tiptapify/extensions/components/ai/Dialog.vue'
6
+ import defaults from '@tiptapify/constants/defaults'
7
+ import { TiptapifyAiResolvedOptions, variantBtnTypes } from '@tiptapify/types/editor'
8
+ import { computed, ComputedRef, inject, PropType, useTemplateRef } from 'vue'
9
+ import { ComposerTranslation } from 'vue-i18n'
10
+
11
+ defineProps({
12
+ variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
13
+ })
14
+
15
+ const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
16
+ const ai = inject('tiptapifyAi') as ComputedRef<TiptapifyAiResolvedOptions | false>
17
+ const dialog = useTemplateRef('dialog')
18
+
19
+ const isAvailable = computed(() => Boolean(ai?.value && ai.value.aiProvider))
20
+ const mdiIcons = mdi as Record<string, string>
21
+ const icon = computed(() => `mdiSvg:${mdiIcons.mdiSparklesOutline ?? mdiIcons.mdiSparkles ?? mdi.mdiCreationOutline}`)
22
+
23
+ function showDialog() {
24
+ if (!isAvailable.value) {
25
+ return
26
+ }
27
+
28
+ dialog.value?.showDialog()
29
+ }
30
+
31
+ </script>
32
+
33
+ <template>
34
+ <VBtn color="" :variant="variantBtn" size="32" :disabled="!isAvailable" @click="showDialog">
35
+ <VTooltip activator="parent">
36
+ {{ isAvailable ? t('ai.title') : t('ai.unavailable') }}
37
+ </VTooltip>
38
+ <BtnIcon :icon="icon" />
39
+ </VBtn>
40
+
41
+ <AiDialog ref="dialog" />
42
+ </template>