tiptapify 0.3.3 → 0.3.5

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 (37) hide show
  1. package/README.md +1 -0
  2. package/dist/tiptapify.css +1 -1
  3. package/dist/tiptapify.mjs +15909 -15553
  4. package/dist/tiptapify.umd.js +55 -55
  5. package/index.d.ts +95 -0
  6. package/package.json +2 -1
  7. package/src/components/ImageBubbleMenu.vue +16 -7
  8. package/src/components/MenuBubble.vue +45 -7
  9. package/src/components/SkinScope.test.ts +74 -0
  10. package/src/components/SkinScope.vue +18 -0
  11. package/src/components/Tiptapify.skin.test.ts +186 -0
  12. package/src/components/Tiptapify.vue +115 -21
  13. package/src/components/Toolbar/Index.vue +43 -62
  14. package/src/components/Toolbar/Items.test.ts +58 -2
  15. package/src/components/Toolbar/Items.vue +93 -7
  16. package/src/components/UI/TiptapifyDialog.test.ts +132 -0
  17. package/src/components/UI/TiptapifyDialog.vue +90 -40
  18. package/src/composables/useTiptapifyConfig.test.ts +25 -2
  19. package/src/composables/useTiptapifyConfig.ts +21 -2
  20. package/src/constants/defaults.ts +2 -1
  21. package/src/extensions/components/alignment/menu/Button.vue +21 -17
  22. package/src/extensions/components/list/bullet/Button.vue +20 -16
  23. package/src/extensions/components/list/styles/ListStyleButton.vue +25 -25
  24. package/src/extensions/components/style/heading/Button.vue +33 -29
  25. package/src/index.test.ts +88 -0
  26. package/src/index.ts +15 -0
  27. package/src/skins/__fixtures__/skin-baseline.json +805 -0
  28. package/src/skins/builtins.ts +81 -0
  29. package/src/skins/menu.css +87 -0
  30. package/src/skins/menu.ts +10 -0
  31. package/src/skins/registry.test.ts +180 -0
  32. package/src/skins/registry.ts +132 -0
  33. package/src/skins/tokens.reachability.test.ts +43 -0
  34. package/src/skins/tokens.ts +92 -0
  35. package/src/types/editor.ts +99 -0
  36. package/src/utils/validateHref.test.ts +25 -0
  37. package/src/utils/validateHref.ts +17 -13
@@ -0,0 +1,132 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { App, computed, createApp, h, nextTick, provide, Ref, ref } from 'vue'
3
+ import { createVuetify } from 'vuetify'
4
+ import { VTextField } from 'vuetify/components'
5
+ import TiptapifyDialog from '@tiptapify/components/UI/TiptapifyDialog.vue'
6
+ import { TIPTAPIFY_CONFIG_KEY } from '@tiptapify/composables/useTiptapifyConfig'
7
+ import { resolveSkin } from '@tiptapify/skins/registry'
8
+
9
+ // jsdom has no `visualViewport` global; Vuetify's overlay positioning reads it,
10
+ // and the `visualViewport?.` guards do not protect against an undeclared identifier
11
+ if (!('visualViewport' in globalThis)) {
12
+ ;(globalThis as Record<string, unknown>).visualViewport = {
13
+ scale: 1,
14
+ offsetLeft: 0,
15
+ offsetTop: 0,
16
+ addEventListener: () => {},
17
+ removeEventListener: () => {},
18
+ }
19
+ }
20
+
21
+ // The dialog teleports to the overlay root, so its skin markup rides the shape:
22
+ // the `tiptapify-dialog--soft` carrier, the divider under the title, the close
23
+ // variant and the field defaults all key off `shape.dialog`, not the skin name.
24
+ const openApps: { app: App, host: HTMLElement }[] = []
25
+
26
+ async function openDialog (skinInput: unknown): Promise<HTMLElement> {
27
+ const config = resolveSkin(skinInput)
28
+ const dialog: Ref<{ open: () => void } | null> = ref(null)
29
+
30
+ const app = createApp({
31
+ setup () {
32
+ provide(TIPTAPIFY_CONFIG_KEY, {
33
+ variantBtn: ref('flat'),
34
+ variantField: ref('outlined'),
35
+ skin: ref(skinInput),
36
+ skinName: computed(() => config.name),
37
+ tokens: computed(() => config.tokens),
38
+ shape: computed(() => config.shape),
39
+ })
40
+ provide('tiptapifyI18n', { t: (key: string) => key })
41
+
42
+ return () => h(TiptapifyDialog, { ref: dialog, title: 'Probe dialog', module: 'link' }, {
43
+ content: () => h('div', { class: 'probe-content' }, [h(VTextField, { label: 'Field' })]),
44
+ })
45
+ },
46
+ })
47
+ app.use(createVuetify())
48
+
49
+ const host = document.createElement('div')
50
+ document.body.appendChild(host)
51
+ app.mount(host)
52
+ openApps.push({ app, host })
53
+ await nextTick()
54
+
55
+ dialog.value?.open()
56
+ for (let i = 0; i < 10; i++) {
57
+ await nextTick()
58
+ if (document.querySelector('.tiptapify-dialog')) {
59
+ break
60
+ }
61
+ }
62
+
63
+ const card = document.querySelector<HTMLElement>('.tiptapify-dialog')
64
+ if (!card) {
65
+ throw new Error('dialog card did not render')
66
+ }
67
+
68
+ return card
69
+ }
70
+
71
+ function disposeDialogs () {
72
+ for (const { app, host } of openApps.splice(0)) {
73
+ app.unmount()
74
+ host.remove()
75
+ }
76
+ document.body.innerHTML = ''
77
+ }
78
+
79
+ afterEach(disposeDialogs)
80
+
81
+ describe('TiptapifyDialog shape wiring', () => {
82
+ it('the classic dialog keeps the divider, the elevated close and no soft carrier', async () => {
83
+ const card = await openDialog('default')
84
+
85
+ expect(card.classList.contains('tiptapify-dialog--soft')).toBe(false)
86
+ expect(card.classList.contains('tiptapify-dialog--pastel')).toBe(false)
87
+
88
+ const header = card.querySelector('.tiptapify-dialog-header')
89
+ expect(header?.nextElementSibling?.classList.contains('v-divider')).toBe(true)
90
+
91
+ const close = card.querySelector('.tiptapify-dialog-close')
92
+ expect([...close?.classList ?? []].join(' ')).toContain('v-btn--variant-elevated')
93
+
94
+ const input = card.querySelector('.v-input')
95
+ expect([...input?.classList ?? []].join(' ')).toContain('v-input--density-default')
96
+ })
97
+
98
+ it('the pastel dialog carries tiptapify-dialog--soft, no divider, a text close and soft field defaults', async () => {
99
+ const card = await openDialog('pastel')
100
+
101
+ expect(card.classList.contains('tiptapify-dialog--soft')).toBe(true)
102
+
103
+ const header = card.querySelector('.tiptapify-dialog-header')
104
+ expect(header?.nextElementSibling?.classList.contains('v-divider')).toBe(false)
105
+
106
+ const close = card.querySelector('.tiptapify-dialog-close')
107
+ expect([...close?.classList ?? []].join(' ')).toContain('v-btn--variant-text')
108
+
109
+ // VDefaultsProvider: the soft dialog gives unstyled fields the comfortable outlined defaults
110
+ const input = card.querySelector('.v-input')
111
+ expect([...input?.classList ?? []].join(' ')).toContain('v-input--density-comfortable')
112
+ })
113
+
114
+ it('the soft carrier rides the dialog flag, not the name: a custom skin extending pastel keeps it', async () => {
115
+ const card = await openDialog({ extends: 'pastel' })
116
+
117
+ expect(card.classList.contains('tiptapify-dialog--soft')).toBe(true)
118
+ expect(card.classList.contains('tiptapify-dialog--pastel')).toBe(false)
119
+ })
120
+
121
+ it('an inline dialog flag alone drives the carrier and the divider', async () => {
122
+ const soft = await openDialog({ dialog: 'soft' })
123
+ expect(soft.classList.contains('tiptapify-dialog--soft')).toBe(true)
124
+ expect(soft.querySelector('.tiptapify-dialog-header')?.nextElementSibling?.classList.contains('v-divider')).toBe(false)
125
+
126
+ disposeDialogs()
127
+
128
+ const classic = await openDialog({ dialog: 'classic' })
129
+ expect(classic.classList.contains('tiptapify-dialog--soft')).toBe(false)
130
+ expect(classic.querySelector('.tiptapify-dialog-header')?.nextElementSibling?.classList.contains('v-divider')).toBe(true)
131
+ })
132
+ })
@@ -1,8 +1,9 @@
1
1
  <script lang="ts" setup>
2
2
 
3
3
  import * as mdi from '@mdi/js'
4
- import { inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
4
+ import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
5
5
  import { ComposerTranslation } from 'vue-i18n'
6
+ import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
6
7
 
7
8
  const props = defineProps({
8
9
  module: String,
@@ -13,6 +14,14 @@ const props = defineProps({
13
14
 
14
15
  const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
15
16
 
17
+ // The dialog is teleported to the overlay root, so the skin reaches it by injection,
18
+ // not by the editor's DOM
19
+ const { shape, tokens } = useTiptapifyConfig()
20
+
21
+ // The soft dialog shape drives every dialog site (carrier class, close variant, field defaults):
22
+ // one computed keeps the template readable (task-7 cleanup)
23
+ const dialogSoft = computed(() => shape.value.dialog === 'soft')
24
+
16
25
  defineExpose({ open, close })
17
26
  const emits = defineEmits(['closeDialog'])
18
27
 
@@ -150,7 +159,11 @@ onBeforeUnmount(() => {
150
159
 
151
160
  <template>
152
161
  <VDialog v-model="showDialog" :max-width="maxWidth" :fullscreen="fullscreen" @click:outside="emitClose">
153
- <VCard class="tiptapify-dialog" :class="{ 'tiptapify-dialog--fullscreen': fullscreen }">
162
+ <VCard
163
+ class="tiptapify-dialog"
164
+ :class="{ 'tiptapify-dialog--soft': dialogSoft, 'tiptapify-dialog--fullscreen': fullscreen }"
165
+ :style="tokens"
166
+ >
154
167
  <VCardTitle
155
168
  ref="movableHandler"
156
169
  class="tiptapify-dialog-header"
@@ -163,13 +176,23 @@ onBeforeUnmount(() => {
163
176
 
164
177
  <VSpacer />
165
178
 
166
- <VBtn class="tiptapify-dialog-close" variant="text" size="32" icon @click="emitClose">
167
- <VIcon size="20" :icon="`mdiSvg:${mdi.mdiClose}`" />
179
+ <VBtn
180
+ class="tiptapify-dialog-close"
181
+ :variant="dialogSoft ? 'text' : 'elevated'"
182
+ :elevation="dialogSoft ? undefined : 4"
183
+ :density="dialogSoft ? 'default' : 'compact'"
184
+ :size="dialogSoft ? '32' : undefined"
185
+ icon
186
+ @click="emitClose"
187
+ >
188
+ <VIcon :size="dialogSoft ? 20 : 'x-small'" :icon="`mdiSvg:${mdi.mdiClose}`" />
168
189
  </VBtn>
169
190
  </VCardTitle>
170
191
 
171
- <!-- Fields a dialog does not style itself get the light outlined look -->
172
- <VDefaultsProvider :defaults="fieldDefaults">
192
+ <VDivider v-if="!dialogSoft" />
193
+
194
+ <!-- In the soft dialog shape, fields a dialog does not style itself get the light outlined look -->
195
+ <VDefaultsProvider :defaults="dialogSoft ? fieldDefaults : {}">
173
196
  <slot name="content" />
174
197
 
175
198
  <slot name="actions" />
@@ -179,76 +202,103 @@ onBeforeUnmount(() => {
179
202
  </template>
180
203
 
181
204
  <style lang="scss" scoped>
182
- /* A light card in the spirit of Flux modals: generous padding, soft corners and shadow, no rules */
183
- .tiptapify-dialog {
184
- border-radius: 12px !important;
185
- box-shadow: 0 20px 48px rgba(0, 0, 0, 0.16), 0 2px 6px rgba(0, 0, 0, 0.06) !important;
205
+ /*
206
+ * Dialog skin rules: the base block reads the dialog tokens with
207
+ * the classic fallbacks; the soft block (carrier `tiptapify-dialog--soft`, teleported
208
+ * surfaces cannot ride the root's shape classes) reads the same tokens with the soft
209
+ * shape's base as fallback. The VCard itself carries the token `:style` (SkinScope step),
210
+ * so the tokens reach every rule here.
211
+ * The `!important`s stay exactly where Vuetify paints the pixel with an INLINE
212
+ * style (the card radius/shadow via the elevation/rounded composables, the content
213
+ * padding via density) — no selector can outrank an inline style, and these are the
214
+ * base-commit set, no additions. The header and actions padding keep the base-commit
215
+ * !importants dropped: Vuetify paints them as plain class rules, and the doubled
216
+ * scoped carrier outranks them (proven by the fixture gate).
217
+ */
218
+ .tiptapify-dialog-header {
219
+ display: flex;
220
+ align-items: center;
221
+ }
222
+
223
+ /* The classic title paints like a VLabel: a light, roomy label */
224
+ .tiptapify-dialog-title {
225
+ color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
226
+ font-size: var(--tf-dialog-title-size, 1.25rem);
227
+ font-weight: var(--tf-dialog-title-weight, 400);
228
+ line-height: 1.75;
229
+ white-space: normal;
230
+ }
231
+
232
+ :deep(.v-overlay__content) {
233
+ position: fixed;
234
+ }
235
+
236
+ .tiptapify-movable-handler {
237
+ cursor: move;
238
+ }
239
+
240
+ .tiptapify-dialog-close:hover {
241
+ cursor: pointer;
242
+ }
243
+
244
+ /**
245
+ * Soft dialog shape: a light card in the spirit of Flux modals: generous padding,
246
+ * soft corners and shadow, no rules
247
+ */
248
+ .tiptapify-dialog--soft {
249
+ border-radius: var(--tf-dialog-radius, 12px) !important;
250
+ box-shadow: var(--tf-dialog-shadow, 0 20px 48px rgba(0, 0, 0, 0.16), 0 2px 6px rgba(0, 0, 0, 0.06)) !important;
186
251
 
187
252
  &.tiptapify-dialog--fullscreen {
188
253
  border-radius: 0 !important;
189
254
  }
190
255
 
191
256
  :deep(.v-card-text) {
192
- padding: 4px 20px !important;
257
+ padding: var(--tf-dialog-content-padding, 4px 20px) !important;
193
258
  }
194
259
 
195
260
  :deep(.v-field--variant-outlined) {
196
- border-radius: 8px;
261
+ border-radius: var(--tf-dialog-field-radius, 8px);
197
262
 
198
263
  .v-field__outline__start {
199
- border-start-start-radius: 8px;
200
- border-end-start-radius: 8px;
264
+ border-start-start-radius: var(--tf-dialog-field-radius, 8px);
265
+ border-end-start-radius: var(--tf-dialog-field-radius, 8px);
201
266
  }
202
267
 
203
268
  .v-field__outline__end {
204
- border-start-end-radius: 8px;
205
- border-end-end-radius: 8px;
269
+ border-start-end-radius: var(--tf-dialog-field-radius, 8px);
270
+ border-end-end-radius: var(--tf-dialog-field-radius, 8px);
206
271
  }
207
272
  }
208
273
 
209
274
  :deep(.v-card-actions) {
210
275
  gap: 8px;
211
- padding: 16px 20px 20px !important;
276
+ padding: var(--tf-dialog-actions-padding, 16px 20px 20px);
212
277
  }
213
278
 
214
279
  :deep(.v-card-actions .v-btn),
215
280
  :deep(.tiptapify-dialog-actions .v-btn) {
216
- min-height: 38px;
281
+ min-height: var(--tf-dialog-action-btn-min-height, 38px);
217
282
  padding-inline: 16px;
218
- border-radius: 8px;
283
+ border-radius: var(--tf-dialog-action-btn-radius, 8px);
219
284
  font-weight: 500;
220
285
  letter-spacing: normal;
221
286
  text-transform: none;
222
287
  }
223
288
  }
224
289
 
225
- .tiptapify-dialog-header {
226
- display: flex;
227
- align-items: center;
228
- padding: 16px 12px 12px 20px !important;
290
+ .tiptapify-dialog--soft .tiptapify-dialog-header {
291
+ padding: var(--tf-dialog-header-padding, 16px 12px 12px 20px);
229
292
  }
230
293
 
231
- .tiptapify-dialog-title {
294
+ .tiptapify-dialog--soft .tiptapify-dialog-title {
232
295
  color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
233
- font-size: 1.125rem;
234
- font-weight: 600;
296
+ font-size: var(--tf-dialog-title-size, 1.125rem);
297
+ font-weight: var(--tf-dialog-title-weight, 600);
235
298
  line-height: 1.4;
236
- white-space: normal;
237
299
  }
238
300
 
239
- .tiptapify-dialog-close {
301
+ .tiptapify-dialog--soft .tiptapify-dialog-close {
240
302
  color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
241
303
  }
242
-
243
- :deep(.v-overlay__content) {
244
- position: fixed;
245
- }
246
-
247
- .tiptapify-movable-handler {
248
- cursor: move;
249
- }
250
-
251
- .tiptapify-dialog-close:hover {
252
- cursor: pointer;
253
- }
254
304
  </style>
@@ -1,9 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { createSSRApp, h, provide, ref } from 'vue'
2
+ import { computed, createSSRApp, h, provide, ref } from 'vue'
3
3
  import { renderToString } from 'vue/server-renderer'
4
4
  import { createVuetify } from 'vuetify'
5
5
  import BoldButton from '@tiptapify/extensions/components/format/bold/Button.vue'
6
6
  import { TIPTAPIFY_CONFIG_KEY, useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
7
+ import { resolveSkin } from '@tiptapify/skins/registry'
7
8
 
8
9
  const editor = {
9
10
  isActive: () => false,
@@ -13,10 +14,16 @@ const editor = {
13
14
 
14
15
  const Consumer = {
15
16
  setup () {
16
- const { variantBtn, variantField } = useTiptapifyConfig()
17
+ const { variantBtn, variantField, skin, skinName, tokens, shape } = useTiptapifyConfig()
17
18
  return () => h('span', [
18
19
  h('span', { id: 'variant-btn' }, variantBtn.value),
19
20
  h('span', { id: 'variant-field' }, variantField.value),
21
+ h('span', { id: 'skin' }, typeof skin.value === 'string' ? skin.value : ''),
22
+ h('span', { id: 'skin-name' }, skinName.value),
23
+ h('span', { id: 'token-count' }, String(Object.keys(tokens.value).length)),
24
+ h('span', { id: 'token-button-radius' }, tokens.value['--tf-button-radius'] ?? ''),
25
+ h('span', { id: 'shape-toolbar' }, shape.value.toolbar),
26
+ h('span', { id: 'shape-buttons' }, shape.value.buttons),
20
27
  ])
21
28
  },
22
29
  }
@@ -25,9 +32,14 @@ function render (root: () => any, withConfig: boolean) {
25
32
  const app = createSSRApp({
26
33
  setup () {
27
34
  if (withConfig) {
35
+ const pastel = resolveSkin('pastel')
28
36
  provide(TIPTAPIFY_CONFIG_KEY, {
29
37
  variantBtn: ref('tonal'),
30
38
  variantField: ref('outlined'),
39
+ skin: ref('pastel'),
40
+ skinName: computed(() => pastel.name),
41
+ tokens: computed(() => pastel.tokens),
42
+ shape: computed(() => pastel.shape),
31
43
  })
32
44
  }
33
45
  provide('tiptapifyEditor', ref(editor))
@@ -46,6 +58,11 @@ describe('useTiptapifyConfig', () => {
46
58
 
47
59
  expect(html).toContain('<span id="variant-btn">tonal</span>')
48
60
  expect(html).toContain('<span id="variant-field">outlined</span>')
61
+ expect(html).toContain('<span id="skin">pastel</span>')
62
+ expect(html).toContain('<span id="skin-name">pastel</span>')
63
+ expect(html).toContain('<span id="token-button-radius">6px</span>')
64
+ expect(html).toContain('<span id="shape-toolbar">flat</span>')
65
+ expect(html).toContain('<span id="shape-buttons">quiet</span>')
49
66
  })
50
67
 
51
68
  it('falls back to defaults when no provider is present', async () => {
@@ -53,6 +70,12 @@ describe('useTiptapifyConfig', () => {
53
70
 
54
71
  expect(html).toContain('<span id="variant-btn">flat</span>')
55
72
  expect(html).toContain('<span id="variant-field">solo</span>')
73
+ expect(html).toContain('<span id="skin">default</span>')
74
+ // The fallback resolves the default skin: classic shape, no emitted tokens
75
+ expect(html).toContain('<span id="skin-name">default</span>')
76
+ expect(html).toContain('<span id="token-count">0</span>')
77
+ expect(html).toContain('<span id="shape-toolbar">raised</span>')
78
+ expect(html).toContain('<span id="shape-buttons">vuetify</span>')
56
79
  })
57
80
 
58
81
  it('is consumed by toolbar extension buttons', async () => {
@@ -1,17 +1,36 @@
1
- import { computed, inject, InjectionKey, Ref } from 'vue'
1
+ import { computed, ComputedRef, inject, InjectionKey, Ref } from 'vue'
2
2
  import defaults from '@tiptapify/constants/defaults'
3
- import { variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
3
+ import { TiptapifyShape, TiptapifySkinInput, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
4
+ // Importing the builtins registers them on load, so resolution works without consumer setup
5
+ import '@tiptapify/skins/builtins'
6
+ import { resolveSkin } from '@tiptapify/skins/registry'
4
7
 
5
8
  export interface TiptapifyConfig {
6
9
  variantBtn: Ref<variantBtnTypes>
7
10
  variantField: Ref<variantFieldTypes>
11
+ // the raw `skin` prop: a registered name (string) or an inline skin object
12
+ skin: Ref<TiptapifySkinInput>
13
+ // the resolved skin name (inline objects without a name resolve to 'inline')
14
+ skinName: ComputedRef<string>
15
+ // css variable name -> value; only overrides, the css fallbacks carry the base
16
+ tokens: ComputedRef<Record<string, string>>
17
+ shape: ComputedRef<Required<TiptapifyShape>>
8
18
  }
9
19
 
10
20
  export const TIPTAPIFY_CONFIG_KEY: InjectionKey<TiptapifyConfig> = Symbol('tiptapifyConfig')
11
21
 
22
+ // Components outside an editor (docs playground) resolve the default skin:
23
+ // classic shape, and no emitted tokens — the css fallbacks are the base values.
24
+ // Resolved once at module load: the fallback never changes at runtime
25
+ const defaultSkin = resolveSkin(defaults.skin)
26
+
12
27
  export function useTiptapifyConfig(): TiptapifyConfig {
13
28
  return inject(TIPTAPIFY_CONFIG_KEY, {
14
29
  variantBtn: computed(() => defaults.variantBtn),
15
30
  variantField: computed(() => defaults.variantField),
31
+ skin: computed(() => defaults.skin),
32
+ skinName: computed(() => defaultSkin.name),
33
+ tokens: computed(() => defaultSkin.tokens),
34
+ shape: computed(() => defaultSkin.shape),
16
35
  })
17
36
  }
@@ -1,6 +1,7 @@
1
- import { variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
1
+ import { TiptapifySkin, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
2
2
 
3
3
  export default {
4
4
  variantBtn: 'flat' as variantBtnTypes,
5
5
  variantField: 'solo' as variantFieldTypes,
6
+ skin: 'default' as TiptapifySkin,
6
7
  }
@@ -3,8 +3,10 @@
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 SkinScope from '@tiptapify/components/SkinScope.vue'
6
7
  import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
7
8
  import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
9
+ import { menuContentClass } from '@tiptapify/skins/menu'
8
10
  import { inject, PropType, Ref } from 'vue'
9
11
 
10
12
  import { ComposerTranslation } from 'vue-i18n'
@@ -15,7 +17,7 @@ const props = defineProps({
15
17
  alignments: { type: Array as PropType<Alignment[]>, default: () => ['left', 'center', 'right', 'justify'] },
16
18
  })
17
19
 
18
- const { variantBtn } = useTiptapifyConfig()
20
+ const { variantBtn, shape } = useTiptapifyConfig()
19
21
 
20
22
  const editor = inject('tiptapifyEditor') as Ref<Editor>
21
23
 
@@ -48,21 +50,23 @@ function currentAlignmentIcon(): string {
48
50
  <BtnIcon :icon="`mdiSvg:${mdi.mdiChevronDown}`" class="tiptapify-dropdown-button__chevron" />
49
51
  </VBtn>
50
52
 
51
- <VMenu :activator="`#tiptapify-align-button-${editor.instanceId}`" content-class="tiptapify-menu">
52
- <VList density="compact">
53
- <VListItem
54
- v-for="alignment in alignments"
55
- :key="alignment"
56
- link
57
- :active="editor.isActive({ textAlign: alignment })"
58
- @click="editor.chain().focus().toggleTextAlign(alignment).run()"
59
- >
60
- <Tooltip :label="t(`alignments.${alignment}`)" :hotkey="alignmentOptions[alignment].hotkey" />
61
- <template #prepend>
62
- <BtnIcon :icon="`mdiSvg:${alignmentOptions[alignment].icon}`" />
63
- </template>
64
- <VListItemTitle>{{ t(`alignments.${alignment}`) }}</VListItemTitle>
65
- </VListItem>
66
- </VList>
53
+ <VMenu :activator="`#tiptapify-align-button-${editor.instanceId}`" :content-class="menuContentClass(shape.menu === 'soft')">
54
+ <SkinScope>
55
+ <VList density="compact">
56
+ <VListItem
57
+ v-for="alignment in alignments"
58
+ :key="alignment"
59
+ link
60
+ :active="editor.isActive({ textAlign: alignment })"
61
+ @click="editor.chain().focus().toggleTextAlign(alignment).run()"
62
+ >
63
+ <Tooltip :label="t(`alignments.${alignment}`)" :hotkey="alignmentOptions[alignment].hotkey" />
64
+ <template #prepend>
65
+ <BtnIcon :icon="`mdiSvg:${alignmentOptions[alignment].icon}`" />
66
+ </template>
67
+ <VListItemTitle>{{ t(`alignments.${alignment}`) }}</VListItemTitle>
68
+ </VListItem>
69
+ </VList>
70
+ </SkinScope>
67
71
  </VMenu>
68
72
  </template>
@@ -3,8 +3,10 @@
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 SkinScope from '@tiptapify/components/SkinScope.vue'
6
7
  import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
7
8
  import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
9
+ import { menuContentClass } from '@tiptapify/skins/menu'
8
10
  import { computed, inject, ref, Ref, watch } from 'vue'
9
11
 
10
12
  import { ComposerTranslation } from 'vue-i18n'
@@ -19,7 +21,7 @@ const props = defineProps({
19
21
  withSquare: { type: Boolean, default: true }
20
22
  })
21
23
 
22
- const { variantBtn } = useTiptapifyConfig()
24
+ const { variantBtn, shape } = useTiptapifyConfig()
23
25
 
24
26
  const editor = inject('tiptapifyEditor') as Ref<Editor>
25
27
 
@@ -98,7 +100,7 @@ watch(() => bulletLists.value, () => {
98
100
  @toggle="editor.commands.toggleBulletList()"
99
101
  />
100
102
 
101
- <VMenu v-else-if="isMenu" content-class="tiptapify-menu">
103
+ <VMenu v-else-if="isMenu" :content-class="menuContentClass(shape.menu === 'soft')">
102
104
  <template #activator="{ props: menuProps }">
103
105
  <VBtn
104
106
  :color="buttonActive ? 'primary' : ''"
@@ -112,20 +114,22 @@ watch(() => bulletLists.value, () => {
112
114
  </VBtn>
113
115
  </template>
114
116
 
115
- <VList>
116
- <VListItem
117
- v-for="(bulletList, key) in menuItems"
118
- :key="key"
119
- link
120
- :active="editor.isActive(bulletList.name)"
121
- @click="toggleList(bulletList.name)"
122
- >
123
- <Tooltip :label="t('lists.bullet')" />
124
- <VListItemTitle class="d-flex justify-center align-center">
125
- <BtnIcon :icon="bulletList.icon" />
126
- </VListItemTitle>
127
- </VListItem>
128
- </VList>
117
+ <SkinScope>
118
+ <VList>
119
+ <VListItem
120
+ v-for="(bulletList, key) in menuItems"
121
+ :key="key"
122
+ link
123
+ :active="editor.isActive(bulletList.name)"
124
+ @click="toggleList(bulletList.name)"
125
+ >
126
+ <Tooltip :label="t('lists.bullet')" />
127
+ <VListItemTitle class="d-flex justify-center align-center">
128
+ <BtnIcon :icon="bulletList.icon" />
129
+ </VListItemTitle>
130
+ </VListItem>
131
+ </VList>
132
+ </SkinScope>
129
133
  </VMenu>
130
134
 
131
135
  <VBtn