tiptapify 0.2.5 → 0.3.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 (73) hide show
  1. package/README.md +2 -2
  2. package/dist/tiptapify.css +1 -1
  3. package/dist/tiptapify.mjs +23328 -25129
  4. package/dist/tiptapify.umd.js +70 -72
  5. package/index.d.ts +19 -0
  6. package/package.json +1 -1
  7. package/src/components/Footer.vue +1 -1
  8. package/src/components/Tiptapify.vue +31 -10
  9. package/src/components/Toolbar/Index.vue +3 -4
  10. package/src/components/Toolbar/Items.vue +3 -3
  11. package/src/components/UI/HotkeyTooltip.test.ts +65 -0
  12. package/src/components/UI/HotkeyTooltip.vue +54 -0
  13. package/src/components/UI/Tooltip.vue +20 -0
  14. package/src/composables/useTiptapifyConfig.test.ts +66 -0
  15. package/src/composables/useTiptapifyConfig.ts +17 -0
  16. package/src/constants/defaults.ts +4 -2
  17. package/src/extensions/components/actions/redo/Button.vue +5 -9
  18. package/src/extensions/components/actions/undo/Button.vue +5 -9
  19. package/src/extensions/components/ai/Button.vue +7 -10
  20. package/src/extensions/components/ai/Dialog.vue +9 -13
  21. package/src/extensions/components/ai/backend.test.ts +314 -0
  22. package/src/extensions/components/ai/backend.ts +303 -0
  23. package/src/extensions/components/alignment/center/Button.vue +5 -9
  24. package/src/extensions/components/alignment/justify/Button.vue +5 -9
  25. package/src/extensions/components/alignment/left/Button.vue +5 -9
  26. package/src/extensions/components/alignment/right/Button.vue +5 -9
  27. package/src/extensions/components/format/bold/Button.vue +5 -9
  28. package/src/extensions/components/format/italic/Button.vue +5 -9
  29. package/src/extensions/components/format/strike/Button.vue +5 -9
  30. package/src/extensions/components/format/underline/Button.vue +5 -9
  31. package/src/extensions/components/formatExtra/code/Button.vue +5 -9
  32. package/src/extensions/components/formatExtra/codeBlock/Button.vue +5 -9
  33. package/src/extensions/components/formatExtra/quote/Button.vue +5 -9
  34. package/src/extensions/components/formatExtra/sub/Button.vue +5 -9
  35. package/src/extensions/components/formatExtra/sup/Button.vue +5 -9
  36. package/src/extensions/components/list/bullet/Button.vue +8 -13
  37. package/src/extensions/components/list/indent/Button.vue +5 -9
  38. package/src/extensions/components/list/numbered/Button.vue +5 -9
  39. package/src/extensions/components/list/outdent/Button.vue +5 -9
  40. package/src/extensions/components/list/task/Button.vue +5 -9
  41. package/src/extensions/components/media/charmap/Button.vue +5 -8
  42. package/src/extensions/components/media/charmap/Picker.vue +2 -3
  43. package/src/extensions/components/media/emoji/Button.vue +6 -9
  44. package/src/extensions/components/media/emoji/Picker.vue +2 -3
  45. package/src/extensions/components/media/iframe/Button.vue +6 -10
  46. package/src/extensions/components/media/iframe/IframeDialog.vue +3 -7
  47. package/src/extensions/components/media/image/Button.vue +6 -10
  48. package/src/extensions/components/media/image/ImageDialog.vue +5 -10
  49. package/src/extensions/components/media/link/Button.vue +7 -10
  50. package/src/extensions/components/media/link/LinkDialog.vue +3 -7
  51. package/src/extensions/components/media/table/Button.vue +10 -24
  52. package/src/extensions/components/media/table/ColumnActions.vue +4 -9
  53. package/src/extensions/components/media/table/RowActions.vue +4 -9
  54. package/src/extensions/components/media/video/Button.vue +6 -10
  55. package/src/extensions/components/media/video/VideoDialog.vue +3 -7
  56. package/src/extensions/components/misc/break/Button.vue +5 -9
  57. package/src/extensions/components/misc/formatClear/Button.vue +5 -9
  58. package/src/extensions/components/misc/fullscreen/Button.vue +5 -9
  59. package/src/extensions/components/misc/invisibleChar/Button.vue +5 -9
  60. package/src/extensions/components/misc/line/Button.vue +5 -9
  61. package/src/extensions/components/misc/preview/Button.vue +5 -9
  62. package/src/extensions/components/misc/source/Button.vue +6 -10
  63. package/src/extensions/components/misc/source/ShowSourceDialog.vue +4 -5
  64. package/src/extensions/components/style/color/Button.vue +5 -9
  65. package/src/extensions/components/style/fontFamily/Button.vue +5 -9
  66. package/src/extensions/components/style/fontSize/Button.vue +6 -7
  67. package/src/extensions/components/style/heading/Button.vue +6 -8
  68. package/src/extensions/components/style/highlight/Button.vue +5 -9
  69. package/src/extensions/components/style/lineHeight/Button.vue +5 -9
  70. package/src/index.ts +7 -0
  71. package/src/types/editor.ts +18 -1
  72. package/src/utils/hotkeys.test.ts +146 -0
  73. package/src/utils/hotkeys.ts +65 -0
package/index.d.ts CHANGED
@@ -135,6 +135,23 @@ export type TiptapifyAiProvider = (
135
135
 
136
136
  export type TiptapifyAiTokenProvider = () => Promise<string | null> | string | null
137
137
 
138
+ export interface TiptapifyAiBackendRequest {
139
+ prompt: string
140
+ instruction: string
141
+ stream?: boolean
142
+ thinking?: boolean
143
+ reasoning_effort?: TiptapifyAiReasoningEffort
144
+ model?: string
145
+ }
146
+
147
+ export interface TiptapifyAiBackendProviderOptions {
148
+ endpoint: string
149
+ headers?: Record<string, string>
150
+ tokenProvider?: TiptapifyAiTokenProvider
151
+ }
152
+
153
+ export declare function createAiBackendProvider(options: TiptapifyAiBackendProviderOptions): TiptapifyAiProvider
154
+
138
155
  export interface TiptapifyAiStorage {
139
156
  getItem: (key: string) => string | null | Promise<string | null>
140
157
  setItem: (key: string, value: string) => void | Promise<void>
@@ -143,6 +160,8 @@ export interface TiptapifyAiStorage {
143
160
 
144
161
  export interface TiptapifyAiOptions {
145
162
  aiProvider?: TiptapifyAiProvider
163
+ aiEndpoint?: string
164
+ aiHeaders?: Record<string, string>
146
165
  model?: string
147
166
  promptExamples?: TiptapifyAiPromptExample[]
148
167
  mode?: TiptapifyAiMode
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tiptapify",
3
3
  "types": "./index.d.ts",
4
- "version": "0.2.5",
4
+ "version": "0.3.0",
5
5
  "description": "Tiptap3 editor with Vuetify3 menu implementation",
6
6
  "exports": {
7
7
  ".": {
@@ -57,7 +57,7 @@ function showFooter() {
57
57
  <VRow>
58
58
  <VCol class="d-flex align-center" :class="`justify-${alignment}`">
59
59
  <template v-if="limit > 0">
60
- <VTooltip>
60
+ <VTooltip :open-delay="1500">
61
61
  <template #default>
62
62
  <span v-html="limitText" />
63
63
  </template>
@@ -3,23 +3,26 @@
3
3
  import defaults from '@tiptapify/constants/defaults'
4
4
  import { itemsPropType, toolbarSections } from '@tiptapify/types/toolbarTypes'
5
5
  import { SlashCommandsConfig } from '@tiptapify/types/slashCommandsTypes'
6
- import { computed, onBeforeUnmount, PropType, provide, ref, ShallowRef } from 'vue'
6
+ import { computed, onBeforeUnmount, PropType, provide, ref, ShallowRef, toRef } 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 { TiptapifyAiOptions, TiptapifyEditor, TiptapifyFooterAlignment, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
9
+ import { TiptapifyAiOptions, TiptapifyAiResolvedOptions, TiptapifyEditor, TiptapifyFooterAlignment, 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
 
13
13
  import { useI18n } from 'vue-i18n'
14
14
 
15
15
  import { getTiptapEditor } from '@tiptapify/components/index'
16
+ import { TIPTAPIFY_CONFIG_KEY } from '@tiptapify/composables/useTiptapifyConfig'
17
+ import { createAiBackendProvider } from '@tiptapify/extensions/components/ai/backend'
16
18
 
17
19
  import Footer from '@tiptapify/components/Footer.vue'
18
20
  import { useTheme } from 'vuetify/framework'
19
21
 
20
22
  const props = defineProps({
21
23
  locale: { type: String, default () { return 'en' } },
22
- content: { type: [String, Object] as PropType<string | Record<string, never>>, required: true },
24
+ content: { type: [String, Object] as PropType<string | Record<string, never>>, default () { return '' } },
25
+ modelValue: { type: [String, Object] as PropType<string | Record<string, never>>, default () { return '' } },
23
26
  height: { type: [Number, String], default () { return null } },
24
27
  variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
25
28
  variantField: { type: String as PropType<variantFieldTypes>, default() { return defaults.variantField } },
@@ -48,12 +51,14 @@ const props = defineProps({
48
51
  loadingHeight: { type: String, default() { return '1px' } },
49
52
  })
50
53
 
54
+ const emit = defineEmits(['update:modelValue', 'editor-ready', 'content-changed'])
55
+
51
56
  const loadingProgress = ref(0)
52
57
 
53
58
  const { t } = useI18n()
54
59
 
55
60
  const appTheme = useTheme()
56
- const currentTheme = ref(appTheme.global.name)
61
+ const currentTheme = computed(() => appTheme.name.value)
57
62
 
58
63
  const defaultAiPromptExamples = computed(() => [
59
64
  {
@@ -81,15 +86,30 @@ const tiptapifyAi = computed(() => {
81
86
 
82
87
  const options = props.ai === true ? {} : props.ai
83
88
 
84
- return {
89
+ const resolved: TiptapifyAiResolvedOptions = {
85
90
  ...options,
86
91
  enabled: true,
87
92
  promptExamples: options.promptExamples?.length ? options.promptExamples : defaultAiPromptExamples.value,
88
93
  }
94
+
95
+ // Backend generation: when no `aiProvider` is given but an `aiEndpoint`
96
+ // is set, synthesize a provider that posts the request to the endpoint.
97
+ // An explicit `aiProvider` always wins.
98
+ if (!resolved.aiProvider && resolved.aiEndpoint) {
99
+ resolved.aiProvider = createAiBackendProvider({
100
+ endpoint: resolved.aiEndpoint,
101
+ headers: resolved.aiHeaders,
102
+ tokenProvider: resolved.tokenProvider,
103
+ })
104
+ }
105
+
106
+ return resolved
89
107
  })
90
108
 
91
109
  function contentChanged() {
92
- emit('content-changed', { html: editor.value?.getHTML(), json: editor.value?.getJSON() })
110
+ const html = editor.value?.getHTML() ?? ''
111
+ emit('update:modelValue', html)
112
+ emit('content-changed', { html, json: editor.value?.getJSON() })
93
113
  }
94
114
 
95
115
  function onContainerClick(event: MouseEvent) {
@@ -101,7 +121,7 @@ function onContainerClick(event: MouseEvent) {
101
121
  }
102
122
 
103
123
  const editor: ShallowRef<Editor | undefined> = getTiptapEditor(
104
- props.content,
124
+ props.content || props.modelValue,
105
125
  computed(() => props.placeholder || t('content.placeholder')).value,
106
126
  props.slashCommands,
107
127
  props.customExtensions,
@@ -117,11 +137,13 @@ const editor: ShallowRef<Editor | undefined> = getTiptapEditor(
117
137
  },
118
138
  )
119
139
 
120
- const emit = defineEmits(['update:modelValue', 'editor-ready', 'content-changed'])
121
-
122
140
  provide('tiptapifyEditor', editor)
123
141
  provide('tiptapifyI18n', { t })
124
142
  provide('tiptapifyAi', tiptapifyAi)
143
+ provide(TIPTAPIFY_CONFIG_KEY, {
144
+ variantBtn: toRef(props, 'variantBtn'),
145
+ variantField: toRef(props, 'variantField'),
146
+ })
125
147
 
126
148
  editor.value?.chain().setFontFamily(props.defaultFontFamily).run()
127
149
 
@@ -145,7 +167,6 @@ onBeforeUnmount(() => {
145
167
  <template v-if="toolbar">
146
168
  <Toolbar
147
169
  v-if="editor"
148
- :variant-btn="variantBtn"
149
170
  :variant-field="variantField"
150
171
  :font-measure="fontMeasure"
151
172
  :items="items"
@@ -2,14 +2,13 @@
2
2
  import { Editor } from '@tiptap/vue-3'
3
3
  import Items from '@tiptapify/components/Toolbar/Items.vue'
4
4
  import defaults from '@tiptapify/constants/defaults'
5
- import { variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
5
+ import { variantFieldTypes } from '@tiptapify/types/editor'
6
6
  import { computed, inject, PropType, Ref } from 'vue'
7
7
  import { itemsPropType, toolbarSections } from '@tiptapify/types/toolbarTypes'
8
8
 
9
9
  import { default as toolbarItemsGroups, availableItems } from '@tiptapify/components/Toolbar/items'
10
10
 
11
11
  const props = defineProps({
12
- variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
13
12
  variantField: { type: String as PropType<variantFieldTypes>, default() { return defaults.variantField } },
14
13
  items: { type: [Array, Object] as PropType<itemsPropType>, default() { return [] } },
15
14
  itemsExclude: { type: Boolean, default() { return false } },
@@ -178,10 +177,10 @@ Object.keys(props.customExtensions).forEach((key: any) => {
178
177
  <div v-if="editor" class="tiptapify-toolbar">
179
178
  <VToolbar elevation="1" :theme="theme" height="auto" :class="`ps-1 pr-1 rounded-t-${rounded}`">
180
179
  <VSlideGroup v-if="toolbarScrollable">
181
- <Items :items="toolbarItems" :variant-btn="variantBtn" />
180
+ <Items :items="toolbarItems" />
182
181
  </VSlideGroup>
183
182
 
184
- <Items v-else :items="toolbarItems" :variant-btn="variantBtn" />
183
+ <Items v-else :items="toolbarItems" />
185
184
  </VToolbar>
186
185
  </div>
187
186
  </template>
@@ -1,16 +1,16 @@
1
1
  <script setup lang="ts">
2
2
 
3
- import defaults from '@tiptapify/constants/defaults'
4
- import { variantBtnTypes } from '@tiptapify/types/editor'
3
+ import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
5
4
  import { PropType } from 'vue'
6
5
 
7
6
  import { toolbarSections } from '@tiptapify/types/toolbarTypes'
8
7
 
9
8
  defineProps({
10
- variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
11
9
  items: { type: Array as PropType<toolbarSections>, default() { return {} } },
12
10
  })
13
11
 
12
+ const { variantBtn } = useTiptapifyConfig()
13
+
14
14
  </script>
15
15
 
16
16
  <template>
@@ -0,0 +1,65 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { createSSRApp, h } from 'vue'
3
+ import { renderToString } from 'vue/server-renderer'
4
+ import { createVuetify } from 'vuetify'
5
+ import HotkeyTooltip from './HotkeyTooltip.vue'
6
+
7
+ const MAC_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
8
+ const WIN_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
9
+
10
+ function mockUserAgent (userAgent: string) {
11
+ Object.defineProperty(window.navigator, 'userAgent', {
12
+ value: userAgent,
13
+ configurable: true,
14
+ })
15
+ }
16
+
17
+ function renderTooltip (label: string, hotkey: string): Promise<string> {
18
+ const app = createSSRApp({
19
+ render: () => h(HotkeyTooltip, { label, hotkey })
20
+ })
21
+ app.use(createVuetify())
22
+
23
+ return renderToString(app)
24
+ }
25
+
26
+ describe('HotkeyTooltip', () => {
27
+ afterEach(() => {
28
+ vi.unstubAllGlobals()
29
+ })
30
+
31
+ it('renders label and spelled-out hotkey on two lines on Windows', async () => {
32
+ mockUserAgent(WIN_UA)
33
+ const html = await renderTooltip('Bold', 'Mod-Shift-b')
34
+
35
+ expect(html).toContain('t-hotkey-tooltip__label')
36
+ expect(html).toContain('t-hotkey-tooltip__hotkey')
37
+ expect(html).toContain('Bold')
38
+ expect(html).toContain('Ctrl')
39
+ expect(html).toContain('Shift')
40
+ expect(html).toContain('>B</span>')
41
+ expect(html).not.toContain('mdiSvg:')
42
+ })
43
+
44
+ it('renders the hotkey with icons on macOS without spelled-out names', async () => {
45
+ mockUserAgent(MAC_UA)
46
+ const html = await renderTooltip('Heading', 'Mod-Alt-1..6')
47
+
48
+ expect(html).toContain('t-hotkey-tooltip__label')
49
+ expect(html).toContain('t-hotkey-tooltip__hotkey')
50
+ expect(html).toContain('Heading')
51
+ expect(html).toContain('mdiSvg:')
52
+ expect(html).toContain('1–6')
53
+ expect(html).not.toContain('Ctrl')
54
+ expect(html).not.toContain('>Command<')
55
+ expect(html).not.toContain('>Option<')
56
+ })
57
+
58
+ it('renders Enter as an icon on macOS', async () => {
59
+ mockUserAgent(MAC_UA)
60
+ const html = await renderTooltip('Hard break', 'Mod-Enter')
61
+
62
+ expect(html).toContain('mdiSvg:')
63
+ expect(html).not.toContain('>Enter<')
64
+ })
65
+ })
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+
3
+ import { computed } from 'vue'
4
+ import { hotkeyParts } from '@tiptapify/utils/hotkeys'
5
+
6
+ const props = defineProps({
7
+ label: { type: String, required: true },
8
+ hotkey: { type: String, required: true },
9
+ })
10
+
11
+ const parts = computed(() => hotkeyParts(props.hotkey))
12
+
13
+ </script>
14
+
15
+ <template>
16
+ <span class="t-hotkey-tooltip">
17
+ <span class="t-hotkey-tooltip__label">{{ label }}</span>
18
+ <span class="t-hotkey-tooltip__hotkey">
19
+ <template v-for="(part, index) in parts" :key="index">
20
+ <span v-if="index" class="t-hotkey-sep">+</span>
21
+ <VIcon v-if="part.icon" :icon="part.icon" size="12" class="t-hotkey-icon" />
22
+ <span v-else>{{ part.label }}</span>
23
+ </template>
24
+ </span>
25
+ </span>
26
+ </template>
27
+
28
+ <style scoped lang="scss">
29
+ .t-hotkey-tooltip {
30
+ align-items: center;
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: 3px;
34
+ padding: 4px 0;
35
+ white-space: nowrap;
36
+ }
37
+
38
+ .t-hotkey-tooltip__hotkey {
39
+ align-items: center;
40
+ background: rgba(127, 127, 127, .2);
41
+ background: color-mix(in srgb, currentColor 14%, transparent);
42
+ border-radius: 4px;
43
+ display: inline-flex;
44
+ font-family: 'JetBrainsMono', ui-monospace, Menlo, Consolas, monospace;
45
+ font-size: 11px;
46
+ gap: 4px;
47
+ letter-spacing: .03em;
48
+ padding: 3px 6px;
49
+
50
+ .t-hotkey-icon {
51
+ font-size: 12px;
52
+ }
53
+ }
54
+ </style>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+
3
+ import HotkeyTooltip from '@tiptapify/components/UI/HotkeyTooltip.vue'
4
+
5
+ defineProps({
6
+ label: { type: String, default: '' },
7
+ hotkey: { type: String, default: '' },
8
+ openDelay: { type: Number, default: 500 },
9
+ })
10
+
11
+ </script>
12
+
13
+ <template>
14
+ <VTooltip activator="parent" :open-delay="openDelay">
15
+ <HotkeyTooltip v-if="hotkey" :label="label" :hotkey="hotkey" />
16
+ <slot v-else>
17
+ {{ label }}
18
+ </slot>
19
+ </VTooltip>
20
+ </template>
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createSSRApp, h, provide, ref } from 'vue'
3
+ import { renderToString } from 'vue/server-renderer'
4
+ import { createVuetify } from 'vuetify'
5
+ import BoldButton from '@tiptapify/extensions/components/format/bold/Button.vue'
6
+ import { TIPTAPIFY_CONFIG_KEY, useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
7
+
8
+ const editor = {
9
+ isActive: () => false,
10
+ can: () => ({ chain: () => ({ focus: () => ({ toggleBold: () => ({ run: () => false }) }) }) }),
11
+ commands: { toggleBold: () => {} },
12
+ }
13
+
14
+ const Consumer = {
15
+ setup () {
16
+ const { variantBtn, variantField } = useTiptapifyConfig()
17
+ return () => h('span', [
18
+ h('span', { id: 'variant-btn' }, variantBtn.value),
19
+ h('span', { id: 'variant-field' }, variantField.value),
20
+ ])
21
+ },
22
+ }
23
+
24
+ function render (root: () => any, withConfig: boolean) {
25
+ const app = createSSRApp({
26
+ setup () {
27
+ if (withConfig) {
28
+ provide(TIPTAPIFY_CONFIG_KEY, {
29
+ variantBtn: ref('tonal'),
30
+ variantField: ref('outlined'),
31
+ })
32
+ }
33
+ provide('tiptapifyEditor', ref(editor))
34
+ provide('tiptapifyI18n', { t: (key: string) => key })
35
+ return root
36
+ },
37
+ })
38
+ app.use(createVuetify())
39
+
40
+ return renderToString(app)
41
+ }
42
+
43
+ describe('useTiptapifyConfig', () => {
44
+ it('exposes provided config values', async () => {
45
+ const html = await render(() => h(Consumer), true)
46
+
47
+ expect(html).toContain('<span id="variant-btn">tonal</span>')
48
+ expect(html).toContain('<span id="variant-field">outlined</span>')
49
+ })
50
+
51
+ it('falls back to defaults when no provider is present', async () => {
52
+ const html = await render(() => h(Consumer), false)
53
+
54
+ expect(html).toContain('<span id="variant-btn">flat</span>')
55
+ expect(html).toContain('<span id="variant-field">solo</span>')
56
+ })
57
+
58
+ it('is consumed by toolbar extension buttons', async () => {
59
+ const custom = await render(() => h(BoldButton), true)
60
+ const fallback = await render(() => h(BoldButton), false)
61
+
62
+ expect(custom).toContain('tonal')
63
+ expect(fallback).toContain('flat')
64
+ expect(custom).not.toEqual(fallback)
65
+ })
66
+ })
@@ -0,0 +1,17 @@
1
+ import { computed, inject, InjectionKey, Ref } from 'vue'
2
+ import defaults from '@tiptapify/constants/defaults'
3
+ import { variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
4
+
5
+ export interface TiptapifyConfig {
6
+ variantBtn: Ref<variantBtnTypes>
7
+ variantField: Ref<variantFieldTypes>
8
+ }
9
+
10
+ export const TIPTAPIFY_CONFIG_KEY: InjectionKey<TiptapifyConfig> = Symbol('tiptapifyConfig')
11
+
12
+ export function useTiptapifyConfig(): TiptapifyConfig {
13
+ return inject(TIPTAPIFY_CONFIG_KEY, {
14
+ variantBtn: computed(() => defaults.variantBtn),
15
+ variantField: computed(() => defaults.variantField),
16
+ })
17
+ }
@@ -1,4 +1,6 @@
1
+ import { variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
2
+
1
3
  export default {
2
- variantBtn: 'flat',
3
- variantField: 'solo',
4
+ variantBtn: 'flat' as variantBtnTypes,
5
+ variantField: 'solo' as variantFieldTypes,
4
6
  }
@@ -3,15 +3,13 @@
3
3
  import * as mdi from '@mdi/js'
4
4
  import { Editor } from '@tiptap/vue-3'
5
5
  import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
6
- import { variantBtnTypes } from '@tiptapify/types/editor'
7
- import { inject, PropType, Ref } from 'vue'
6
+ import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
7
+ import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
8
+ import { inject, Ref } from 'vue'
8
9
 
9
- import defaults from '@tiptapify/constants/defaults'
10
10
  import { ComposerTranslation } from 'vue-i18n'
11
11
 
12
- defineProps({
13
- variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
14
- })
12
+ const { variantBtn } = useTiptapifyConfig()
15
13
 
16
14
  const editor = inject('tiptapifyEditor') as Ref<Editor>
17
15
 
@@ -27,9 +25,7 @@ const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
27
25
  :variant="variantBtn"
28
26
  @click="editor.chain().focus().redo().run()"
29
27
  >
30
- <VTooltip activator="parent">
31
- {{ t('action.redo') }}
32
- </VTooltip>
28
+ <Tooltip :label="t('action.redo')" hotkey="Mod-Shift-z" />
33
29
  <BtnIcon :icon="`mdiSvg:${mdi.mdiRedo}`" />
34
30
  </VBtn>
35
31
  </template>
@@ -3,15 +3,13 @@
3
3
  import * as mdi from '@mdi/js'
4
4
  import { Editor } from '@tiptap/vue-3'
5
5
  import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
6
- import { variantBtnTypes } from '@tiptapify/types/editor'
7
- import { inject, PropType, Ref } from 'vue'
6
+ import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
7
+ import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
8
+ import { inject, Ref } from 'vue'
8
9
 
9
- import defaults from '@tiptapify/constants/defaults'
10
10
  import { ComposerTranslation } from 'vue-i18n'
11
11
 
12
- defineProps({
13
- variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
14
- })
12
+ const { variantBtn } = useTiptapifyConfig()
15
13
 
16
14
  const editor = inject('tiptapifyEditor') as Ref<Editor>
17
15
 
@@ -27,9 +25,7 @@ const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
27
25
  :variant="variantBtn"
28
26
  @click="editor.chain().focus().undo().run()"
29
27
  >
30
- <VTooltip activator="parent">
31
- {{ t('action.undo') }}
32
- </VTooltip>
28
+ <Tooltip :label="t('action.undo')" hotkey="Mod-z" />
33
29
  <BtnIcon :icon="`mdiSvg:${mdi.mdiUndo}`" />
34
30
  </VBtn>
35
31
  </template>
@@ -2,15 +2,14 @@
2
2
 
3
3
  import * as mdi from '@mdi/js'
4
4
  import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
5
+ import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
5
6
  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'
7
+ import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
8
+ import { TiptapifyAiResolvedOptions } from '@tiptapify/types/editor'
9
+ import { computed, ComputedRef, inject, useTemplateRef } from 'vue'
9
10
  import { ComposerTranslation } from 'vue-i18n'
10
11
 
11
- defineProps({
12
- variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
13
- })
12
+ const { variantBtn } = useTiptapifyConfig()
14
13
 
15
14
  const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
16
15
  const ai = inject('tiptapifyAi') as ComputedRef<TiptapifyAiResolvedOptions | false>
@@ -32,11 +31,9 @@ function showDialog() {
32
31
 
33
32
  <template>
34
33
  <VBtn color="" :variant="variantBtn" size="32" :disabled="!isAvailable" @click="showDialog">
35
- <VTooltip activator="parent">
36
- {{ isAvailable ? t('ai.title') : t('ai.unavailable') }}
37
- </VTooltip>
34
+ <Tooltip :label="isAvailable ? t('ai.title') : t('ai.unavailable')" />
38
35
  <BtnIcon :icon="icon" />
39
36
  </VBtn>
40
37
 
41
- <AiDialog ref="dialog" :variant-btn="variantBtn" />
38
+ <AiDialog ref="dialog" />
42
39
  </template>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
 
3
3
  import TiptapifyDialog from '@tiptapify/components/UI/TiptapifyDialog.vue'
4
- import defaults from '@tiptapify/constants/defaults'
4
+ import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
5
5
  import {
6
6
  TiptapifyAiEditorContext,
7
7
  TiptapifyAiMode,
@@ -11,19 +11,15 @@ import {
11
11
  TiptapifyAiResponse,
12
12
  TiptapifyAiResolvedOptions,
13
13
  TiptapifyAiStream,
14
- TiptapifyEditor,
15
- variantBtnTypes,
16
- variantFieldTypes
14
+ TiptapifyEditor
17
15
  } from '@tiptapify/types/editor'
18
- import { computed, ComputedRef, inject, nextTick, PropType, Ref, ref, useTemplateRef, watch } from 'vue'
16
+ import { computed, ComputedRef, inject, nextTick, Ref, ref, useTemplateRef, watch } from 'vue'
19
17
  import * as mdi from '@mdi/js'
20
18
  import { ComposerTranslation } from 'vue-i18n'
21
19
  import { exceedsLimit } from './charLimit'
20
+ import { DEFAULT_AI_INSTRUCTION } from './backend'
22
21
 
23
- defineProps({
24
- variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
25
- variantField: { type: String as PropType<variantFieldTypes>, default() { return defaults.variantField } },
26
- })
22
+ const { variantBtn, variantField } = useTiptapifyConfig()
27
23
 
28
24
  const editor = inject('tiptapifyEditor') as Ref<TiptapifyEditor>
29
25
  const ai = inject('tiptapifyAi') as ComputedRef<TiptapifyAiResolvedOptions | false>
@@ -180,7 +176,7 @@ function getSelectedText(from: number, to: number) {
180
176
  function buildDefaultMessages(context: TiptapifyAiEditorContext) {
181
177
  const systemPrompt = ai.value && ai.value.systemPrompt
182
178
  ? ai.value.systemPrompt
183
- : 'You are an AI writing assistant inside a rich text editor. Return only the final text to insert.'
179
+ : DEFAULT_AI_INSTRUCTION
184
180
  const contextText = context.selectedText || context.text
185
181
  const contextLabel = context.selectedText ? 'Selected text' : 'Editor text'
186
182
 
@@ -532,19 +528,19 @@ function apply() {
532
528
 
533
529
  .ai-reasoning {
534
530
  :deep(.v-expansion-panel-title) {
535
- color: var(--gray-5);
531
+ color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
536
532
  font-size: .8125rem;
537
533
  font-weight: 500;
538
534
  padding: .625rem .75rem;
539
535
  }
540
536
 
541
537
  :deep(.v-expansion-panel-title__icon) {
542
- color: var(--gray-4);
538
+ color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
543
539
  }
544
540
  }
545
541
 
546
542
  .ai-reasoning__text {
547
- color: var(--gray-5);
543
+ color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
548
544
  font-family: 'JetBrainsMono', monospace;
549
545
  font-size: .75rem;
550
546
  line-height: 1.4;