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.
- package/README.md +1 -0
- package/dist/tiptapify.css +1 -1
- package/dist/tiptapify.mjs +15909 -15553
- package/dist/tiptapify.umd.js +55 -55
- package/index.d.ts +95 -0
- package/package.json +2 -1
- package/src/components/ImageBubbleMenu.vue +16 -7
- package/src/components/MenuBubble.vue +45 -7
- package/src/components/SkinScope.test.ts +74 -0
- package/src/components/SkinScope.vue +18 -0
- package/src/components/Tiptapify.skin.test.ts +186 -0
- package/src/components/Tiptapify.vue +115 -21
- package/src/components/Toolbar/Index.vue +43 -62
- package/src/components/Toolbar/Items.test.ts +58 -2
- package/src/components/Toolbar/Items.vue +93 -7
- package/src/components/UI/TiptapifyDialog.test.ts +132 -0
- package/src/components/UI/TiptapifyDialog.vue +90 -40
- package/src/composables/useTiptapifyConfig.test.ts +25 -2
- package/src/composables/useTiptapifyConfig.ts +21 -2
- package/src/constants/defaults.ts +2 -1
- package/src/extensions/components/alignment/menu/Button.vue +21 -17
- package/src/extensions/components/list/bullet/Button.vue +20 -16
- package/src/extensions/components/list/styles/ListStyleButton.vue +25 -25
- package/src/extensions/components/style/heading/Button.vue +33 -29
- package/src/index.test.ts +88 -0
- package/src/index.ts +15 -0
- package/src/skins/__fixtures__/skin-baseline.json +805 -0
- package/src/skins/builtins.ts +81 -0
- package/src/skins/menu.css +87 -0
- package/src/skins/menu.ts +10 -0
- package/src/skins/registry.test.ts +180 -0
- package/src/skins/registry.ts +132 -0
- package/src/skins/tokens.reachability.test.ts +43 -0
- package/src/skins/tokens.ts +92 -0
- package/src/types/editor.ts +99 -0
- package/src/utils/validateHref.test.ts +25 -0
- 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
|
|
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
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
/*
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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
|
-
|
|
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="
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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="
|
|
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
|
-
<
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|