tiptapify 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/tiptapify.css +1 -1
- package/dist/tiptapify.mjs +54477 -48112
- package/dist/tiptapify.umd.js +68 -68
- package/index.d.ts +96 -0
- package/package.json +2 -1
- package/src/components/Footer.vue +12 -2
- package/src/components/ImageBubbleMenu.vue +139 -0
- package/src/components/MenuBubble.vue +60 -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 +285 -74
- package/src/components/Toolbar/Index.test.ts +96 -0
- package/src/components/Toolbar/Index.vue +199 -14
- package/src/components/Toolbar/Items.test.ts +82 -2
- package/src/components/Toolbar/Items.vue +104 -19
- package/src/components/Toolbar/alignment.ts +5 -0
- package/src/components/UI/TiptapifyDialog.test.ts +132 -0
- package/src/components/UI/TiptapifyDialog.vue +128 -12
- package/src/components/editorExtensions.test.ts +10 -0
- package/src/components/editorExtensions.ts +34 -5
- package/src/components/index.ts +3 -2
- 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/actions/redo/Button.vue +1 -1
- package/src/extensions/components/actions/undo/Button.vue +1 -1
- package/src/extensions/components/alignment/menu/Button.vue +72 -0
- package/src/extensions/components/list/bullet/Button.vue +35 -16
- package/src/extensions/components/list/numbered/Button.vue +19 -0
- package/src/extensions/components/list/styles/ListStyleButton.vue +156 -0
- package/src/extensions/components/list/styles/index.ts +76 -0
- package/src/extensions/components/list/styles/listStyle.test.ts +44 -0
- package/src/extensions/components/media/image/ImageDialog.vue +1 -1
- package/src/extensions/components/media/image/caption.test.ts +122 -0
- package/src/extensions/components/media/image/caption.ts +268 -0
- package/src/extensions/components/media/image/slider.test.ts +106 -0
- package/src/extensions/components/media/image/slider.ts +87 -0
- package/src/extensions/components/style/heading/Button.vue +45 -30
- package/src/i18n/locales/ar.json +22 -0
- package/src/i18n/locales/bg.json +22 -0
- package/src/i18n/locales/ch.json +22 -0
- package/src/i18n/locales/cz.json +22 -0
- package/src/i18n/locales/de.json +22 -0
- package/src/i18n/locales/el.json +22 -0
- package/src/i18n/locales/en.json +23 -1
- package/src/i18n/locales/es.json +22 -0
- package/src/i18n/locales/et.json +22 -0
- package/src/i18n/locales/fi.json +22 -0
- package/src/i18n/locales/fr.json +22 -0
- package/src/i18n/locales/he.json +22 -0
- package/src/i18n/locales/hi.json +22 -0
- package/src/i18n/locales/hr.json +22 -0
- package/src/i18n/locales/hu.json +22 -0
- package/src/i18n/locales/id.json +22 -0
- package/src/i18n/locales/it.json +22 -0
- package/src/i18n/locales/ja.json +22 -0
- package/src/i18n/locales/ko.json +22 -0
- package/src/i18n/locales/la.json +22 -0
- package/src/i18n/locales/lt.json +22 -0
- package/src/i18n/locales/nl.json +22 -0
- package/src/i18n/locales/no.json +22 -0
- package/src/i18n/locales/pl.json +22 -0
- package/src/i18n/locales/pt-BR.json +22 -0
- package/src/i18n/locales/pt.json +22 -0
- package/src/i18n/locales/ro.json +22 -0
- package/src/i18n/locales/ru.json +23 -1
- package/src/i18n/locales/se.json +22 -0
- package/src/i18n/locales/sk.json +22 -0
- package/src/i18n/locales/sl.json +22 -0
- package/src/i18n/locales/sr.json +22 -0
- package/src/i18n/locales/th.json +22 -0
- package/src/i18n/locales/tr.json +22 -0
- package/src/i18n/locales/uk.json +23 -1
- package/src/i18n/locales/vi.json +22 -0
- 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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createApp, createSSRApp, h, nextTick, ref, Ref } from 'vue'
|
|
3
|
+
import { renderToString } from 'vue/server-renderer'
|
|
4
|
+
import { createVuetify } from 'vuetify'
|
|
5
|
+
import { createI18n } from 'vue-i18n'
|
|
6
|
+
import Tiptapify from '@tiptapify/components/Tiptapify.vue'
|
|
7
|
+
import { messages } from '@tiptapify/i18n'
|
|
8
|
+
|
|
9
|
+
function withPlugins (app: { use: (plugin: never) => unknown }) {
|
|
10
|
+
app.use(createVuetify() as never)
|
|
11
|
+
app.use(createI18n({ legacy: false, locale: 'en', fallbackLocale: 'en', messages }) as never)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function renderShell (skin: unknown): Promise<string> {
|
|
15
|
+
const app = createSSRApp({ setup: () => () => h(Tiptapify, { skin }) })
|
|
16
|
+
withPlugins(app)
|
|
17
|
+
|
|
18
|
+
return renderToString(app)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The opening tag of the editor root div, so assertions see the skin class and the token style
|
|
22
|
+
function rootTag (html: string): string {
|
|
23
|
+
const start = html.indexOf('tiptapify-editor-')
|
|
24
|
+
|
|
25
|
+
return html.slice(0, html.indexOf('>', start) + 1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function mountEditor (skinRef: Ref<unknown>) {
|
|
29
|
+
const app = createApp({ setup: () => () => h(Tiptapify, { skin: skinRef.value }) })
|
|
30
|
+
withPlugins(app)
|
|
31
|
+
const host = document.createElement('div')
|
|
32
|
+
document.body.appendChild(host)
|
|
33
|
+
app.mount(host)
|
|
34
|
+
await nextTick()
|
|
35
|
+
await nextTick()
|
|
36
|
+
|
|
37
|
+
return { app, host }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('Tiptapify skin resolution', () => {
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
vi.restoreAllMocks()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('renders the default skin class and emits no token vars', async () => {
|
|
46
|
+
const html = await renderShell('default')
|
|
47
|
+
|
|
48
|
+
expect(rootTag(html)).toContain('tiptapify-skin-default')
|
|
49
|
+
expect(rootTag(html)).not.toContain('--tf-')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('renders the pastel skin class and emits its token overrides', async () => {
|
|
53
|
+
const html = await renderShell('pastel')
|
|
54
|
+
|
|
55
|
+
expect(rootTag(html)).toContain('tiptapify-skin-pastel')
|
|
56
|
+
expect(rootTag(html)).toContain('--tf-toolbar-bg:rgba(var(--v-theme-on-surface), 0.03)')
|
|
57
|
+
expect(rootTag(html)).toContain('--tf-group-shadow:none')
|
|
58
|
+
expect(rootTag(html)).toContain('--tf-button-radius:6px')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('renders an inline skin object under tiptapify-skin-inline with merged tokens', async () => {
|
|
62
|
+
const html = await renderShell({ colors: { toolbarBg: 'rgb(1, 2, 3)' }, metrics: { buttonRadius: '9px' } })
|
|
63
|
+
|
|
64
|
+
expect(rootTag(html)).toContain('tiptapify-skin-inline')
|
|
65
|
+
expect(rootTag(html)).toContain('--tf-toolbar-bg:rgb(1, 2, 3)')
|
|
66
|
+
expect(rootTag(html)).toContain('--tf-button-radius:9px')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('a named inline object renders under tiptapify-skin-{name}, not the inline name', async () => {
|
|
70
|
+
const html = await renderShell({
|
|
71
|
+
name: 'mint',
|
|
72
|
+
extends: 'pastel',
|
|
73
|
+
colors: { toolbarBg: 'rgba(0, 128, 0, 0.05)' },
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expect(rootTag(html)).toContain('tiptapify-skin-mint')
|
|
77
|
+
expect(rootTag(html)).not.toContain('tiptapify-skin-inline')
|
|
78
|
+
expect(rootTag(html)).toContain('--tf-toolbar-bg:rgba(0, 128, 0, 0.05)')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('falls back to the default skin for an unknown name', async () => {
|
|
82
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
83
|
+
const html = await renderShell('nope')
|
|
84
|
+
|
|
85
|
+
expect(rootTag(html)).toContain('tiptapify-skin-default')
|
|
86
|
+
expect(rootTag(html)).not.toContain('--tf-')
|
|
87
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('nope'))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('re-resolves the skin when the prop changes, without re-rendering the editor', async () => {
|
|
91
|
+
const skin = ref<unknown>('default')
|
|
92
|
+
const { app, host } = await mountEditor(skin)
|
|
93
|
+
|
|
94
|
+
const root = host.querySelector('[id^=tiptapify-editor-]') as HTMLElement
|
|
95
|
+
expect(root.className).toContain('tiptapify-skin-default')
|
|
96
|
+
|
|
97
|
+
const proseMirror = host.querySelector('.ProseMirror')
|
|
98
|
+
expect(proseMirror).toBeTruthy()
|
|
99
|
+
|
|
100
|
+
skin.value = { colors: { toolbarBg: 'rgb(9, 9, 9)' } }
|
|
101
|
+
await nextTick()
|
|
102
|
+
|
|
103
|
+
expect(root.className).toContain('tiptapify-skin-inline')
|
|
104
|
+
expect(root.style.getPropertyValue('--tf-toolbar-bg')).toBe('rgb(9, 9, 9)')
|
|
105
|
+
// The editor DOM is the same node: a skin change never recreates the editor
|
|
106
|
+
expect(host.querySelector('.ProseMirror')).toBe(proseMirror)
|
|
107
|
+
|
|
108
|
+
app.unmount()
|
|
109
|
+
host.remove()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('keeps the pastel toolbar quiet through the resolved skin name', async () => {
|
|
113
|
+
const skin = ref<unknown>('pastel')
|
|
114
|
+
const { app, host } = await mountEditor(skin)
|
|
115
|
+
|
|
116
|
+
const toolbar = host.querySelector('.v-toolbar')
|
|
117
|
+
expect(toolbar?.classList.contains('v-toolbar--flat')).toBe(true)
|
|
118
|
+
|
|
119
|
+
app.unmount()
|
|
120
|
+
host.remove()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('the root carries all six shape classes: classic values for the default skin', async () => {
|
|
124
|
+
const tag = rootTag(await renderShell('default'))
|
|
125
|
+
|
|
126
|
+
expect(tag).toContain('tiptapify-shape-toolbar-raised')
|
|
127
|
+
expect(tag).toContain('tiptapify-shape-groups-box')
|
|
128
|
+
expect(tag).toContain('tiptapify-shape-buttons-vuetify')
|
|
129
|
+
expect(tag).toContain('tiptapify-shape-bubble-card')
|
|
130
|
+
expect(tag).toContain('tiptapify-shape-dialog-classic')
|
|
131
|
+
expect(tag).toContain('tiptapify-shape-menu-vuetify')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('the pastel root carries the quiet shape classes and keeps the skin name class', async () => {
|
|
135
|
+
const tag = rootTag(await renderShell('pastel'))
|
|
136
|
+
|
|
137
|
+
expect(tag).toContain('tiptapify-skin-pastel')
|
|
138
|
+
expect(tag).toContain('tiptapify-shape-toolbar-flat')
|
|
139
|
+
expect(tag).toContain('tiptapify-shape-groups-flat')
|
|
140
|
+
expect(tag).toContain('tiptapify-shape-buttons-quiet')
|
|
141
|
+
expect(tag).toContain('tiptapify-shape-bubble-flat')
|
|
142
|
+
expect(tag).toContain('tiptapify-shape-dialog-soft')
|
|
143
|
+
expect(tag).toContain('tiptapify-shape-menu-soft')
|
|
144
|
+
expect(tag).not.toContain('tiptapify-shape-dialog-classic')
|
|
145
|
+
expect(tag).not.toContain('tiptapify-shape-buttons-vuetify')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('an inline shape object lands on the root classes; the flags it omits stay classic', async () => {
|
|
149
|
+
const tag = rootTag(await renderShell({ buttons: 'quiet', dialog: 'soft' }))
|
|
150
|
+
|
|
151
|
+
expect(tag).toContain('tiptapify-skin-inline')
|
|
152
|
+
expect(tag).toContain('tiptapify-shape-buttons-quiet')
|
|
153
|
+
expect(tag).toContain('tiptapify-shape-dialog-soft')
|
|
154
|
+
expect(tag).toContain('tiptapify-shape-toolbar-raised')
|
|
155
|
+
expect(tag).toContain('tiptapify-shape-groups-box')
|
|
156
|
+
expect(tag).toContain('tiptapify-shape-bubble-card')
|
|
157
|
+
expect(tag).toContain('tiptapify-shape-menu-vuetify')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('a custom skin extending pastel keeps the pastel shape classes under the inline name', async () => {
|
|
161
|
+
const tag = rootTag(await renderShell({ extends: 'pastel' }))
|
|
162
|
+
|
|
163
|
+
expect(tag).toContain('tiptapify-skin-inline')
|
|
164
|
+
expect(tag).toContain('tiptapify-shape-toolbar-flat')
|
|
165
|
+
expect(tag).toContain('tiptapify-shape-groups-flat')
|
|
166
|
+
expect(tag).toContain('tiptapify-shape-buttons-quiet')
|
|
167
|
+
expect(tag).toContain('tiptapify-shape-bubble-flat')
|
|
168
|
+
expect(tag).toContain('tiptapify-shape-dialog-soft')
|
|
169
|
+
expect(tag).toContain('tiptapify-shape-menu-soft')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('the toolbar elevation follows the toolbar shape flag, not the skin name', async () => {
|
|
173
|
+
const skin = ref<unknown>({ toolbar: 'flat' })
|
|
174
|
+
const { app, host } = await mountEditor(skin)
|
|
175
|
+
|
|
176
|
+
expect(host.querySelector('.v-toolbar')?.classList.contains('v-toolbar--flat')).toBe(true)
|
|
177
|
+
|
|
178
|
+
skin.value = { toolbar: 'raised' }
|
|
179
|
+
await nextTick()
|
|
180
|
+
|
|
181
|
+
expect(host.querySelector('.v-toolbar')?.classList.contains('v-toolbar--flat')).toBe(false)
|
|
182
|
+
|
|
183
|
+
app.unmount()
|
|
184
|
+
host.remove()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -6,14 +6,24 @@ import { SlashCommandsConfig } from '@tiptapify/types/slashCommandsTypes'
|
|
|
6
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 {
|
|
9
|
+
import {
|
|
10
|
+
TiptapifyAiOptions,
|
|
11
|
+
TiptapifyAiResolvedOptions,
|
|
12
|
+
TiptapifyEditor,
|
|
13
|
+
TiptapifyFooterAlignment,
|
|
14
|
+
TiptapifySkinInput,
|
|
15
|
+
variantBtnTypes,
|
|
16
|
+
variantFieldTypes
|
|
17
|
+
} from '@tiptapify/types/editor'
|
|
10
18
|
import MenuBubble from '@tiptapify/components/MenuBubble.vue'
|
|
11
19
|
import MenuFloating from '@tiptapify/components/MenuFloating.vue'
|
|
20
|
+
import ImageBubbleMenu from '@tiptapify/components/ImageBubbleMenu.vue'
|
|
12
21
|
|
|
13
22
|
import { useI18n } from 'vue-i18n'
|
|
14
23
|
|
|
15
24
|
import { getTiptapEditor } from '@tiptapify/components/index'
|
|
16
25
|
import { TIPTAPIFY_CONFIG_KEY } from '@tiptapify/composables/useTiptapifyConfig'
|
|
26
|
+
import { resolveSkin } from '@tiptapify/skins/registry'
|
|
17
27
|
import { createAiBackendProvider } from '@tiptapify/extensions/components/ai/backend'
|
|
18
28
|
|
|
19
29
|
import Footer from '@tiptapify/components/Footer.vue'
|
|
@@ -49,6 +59,10 @@ const props = defineProps({
|
|
|
49
59
|
loading: { type: Boolean, default() { return false } },
|
|
50
60
|
loadingColor: { type: String, default() { return 'default' } },
|
|
51
61
|
loadingHeight: { type: String, default() { return '1px' } },
|
|
62
|
+
// From this many images in a row the editor shows them as a slider grid; 0 turns it off
|
|
63
|
+
imageSlider: { type: Number, default() { return 0 } },
|
|
64
|
+
// The skin of the editor chrome: a registered name ('default', 'pastel') or an inline skin object
|
|
65
|
+
skin: { type: [String, Object] as PropType<TiptapifySkinInput>, default() { return defaults.skin } },
|
|
52
66
|
})
|
|
53
67
|
|
|
54
68
|
const emit = defineEmits(['update:modelValue', 'editor-ready', 'content-changed'])
|
|
@@ -135,14 +149,46 @@ const editor: ShallowRef<Editor | undefined> = getTiptapEditor(
|
|
|
135
149
|
getJSON: () => editorInstance.getJSON(),
|
|
136
150
|
})
|
|
137
151
|
},
|
|
152
|
+
{
|
|
153
|
+
captionPlaceholder: t('dialog.image.caption_placeholder'),
|
|
154
|
+
sliderMinImages: props.imageSlider,
|
|
155
|
+
sliderLabel: t('media.slider'),
|
|
156
|
+
},
|
|
138
157
|
)
|
|
139
158
|
|
|
140
159
|
provide('tiptapifyEditor', editor)
|
|
160
|
+
|
|
161
|
+
const scrollContainer = ref<HTMLElement | null>(null)
|
|
162
|
+
provide('tiptapifyScrollContainer', scrollContainer)
|
|
141
163
|
provide('tiptapifyI18n', { t })
|
|
142
164
|
provide('tiptapifyAi', tiptapifyAi)
|
|
165
|
+
// The skin prop resolves through the registry; computed refs so a prop change
|
|
166
|
+
// re-resolves tokens and shape without an editor re-render
|
|
167
|
+
const resolvedSkin = computed(() => resolveSkin(props.skin))
|
|
168
|
+
|
|
169
|
+
// The shape classes: `.tiptapify-shape-{flag}-{value}` for all six flags, so CSS gates on
|
|
170
|
+
// shape (not on the skin name); templates read the computed refs. The name class stays
|
|
171
|
+
// as the escape hatch for consumer CSS.
|
|
172
|
+
const shapeClasses = computed(() => {
|
|
173
|
+
const shape = resolvedSkin.value.shape
|
|
174
|
+
|
|
175
|
+
return [
|
|
176
|
+
`tiptapify-shape-toolbar-${shape.toolbar}`,
|
|
177
|
+
`tiptapify-shape-groups-${shape.groups}`,
|
|
178
|
+
`tiptapify-shape-buttons-${shape.buttons}`,
|
|
179
|
+
`tiptapify-shape-bubble-${shape.bubble}`,
|
|
180
|
+
`tiptapify-shape-dialog-${shape.dialog}`,
|
|
181
|
+
`tiptapify-shape-menu-${shape.menu}`,
|
|
182
|
+
].join(' ')
|
|
183
|
+
})
|
|
184
|
+
|
|
143
185
|
provide(TIPTAPIFY_CONFIG_KEY, {
|
|
144
186
|
variantBtn: toRef(props, 'variantBtn'),
|
|
145
187
|
variantField: toRef(props, 'variantField'),
|
|
188
|
+
skin: toRef(props, 'skin'),
|
|
189
|
+
skinName: computed(() => resolvedSkin.value.name),
|
|
190
|
+
tokens: computed(() => resolvedSkin.value.tokens),
|
|
191
|
+
shape: computed(() => resolvedSkin.value.shape),
|
|
146
192
|
})
|
|
147
193
|
|
|
148
194
|
editor.value?.chain().setFontFamily(props.defaultFontFamily).run()
|
|
@@ -162,8 +208,12 @@ onBeforeUnmount(() => {
|
|
|
162
208
|
</script>
|
|
163
209
|
|
|
164
210
|
<template>
|
|
165
|
-
<div
|
|
166
|
-
|
|
211
|
+
<div
|
|
212
|
+
:id="`tiptapify-editor-${editor?.instanceId}`"
|
|
213
|
+
:class="[resolvedSkin.className, shapeClasses]"
|
|
214
|
+
:style="resolvedSkin.tokens"
|
|
215
|
+
>
|
|
216
|
+
<div :class="`tiptapify-frame rounded-${rounded}`">
|
|
167
217
|
<template v-if="toolbar">
|
|
168
218
|
<Toolbar
|
|
169
219
|
v-if="editor"
|
|
@@ -179,11 +229,13 @@ onBeforeUnmount(() => {
|
|
|
179
229
|
|
|
180
230
|
<VProgressLinear v-model="loadingProgress" :color="loadingColor" :height="loadingHeight" :indeterminate="loading" />
|
|
181
231
|
|
|
182
|
-
<div class="pa-2 tiptapify-container resizable" :style="computeResizableHeight" @click="onContainerClick">
|
|
232
|
+
<div ref="scrollContainer" class="pa-2 tiptapify-container resizable" :style="computeResizableHeight" @click="onContainerClick">
|
|
183
233
|
<MenuFloating v-if="floatingMenu" :variant="variantBtn" :theme="currentTheme" />
|
|
184
234
|
|
|
185
235
|
<MenuBubble v-if="bubbleMenu" :variant="variantBtn" :theme="currentTheme" />
|
|
186
236
|
|
|
237
|
+
<ImageBubbleMenu />
|
|
238
|
+
|
|
187
239
|
<EditorContent :editor="editor" class="tiptapify-editor" />
|
|
188
240
|
</div>
|
|
189
241
|
|
|
@@ -232,12 +284,38 @@ onBeforeUnmount(() => {
|
|
|
232
284
|
--border: 1px solid var(--gray-2);
|
|
233
285
|
}
|
|
234
286
|
|
|
287
|
+
/*
|
|
288
|
+
* The frame: one rule, skin-agnostic, reading the frame tokens (classic fallbacks).
|
|
289
|
+
* The classic shape keeps `border-top: 0`: the raised toolbar sits on the frame's top
|
|
290
|
+
* edge, so no line is needed there. The per-side deltas of the flat shape (full border,
|
|
291
|
+
* bottom edge at 1.6x border opacity) are a property of that shape, not of any one
|
|
292
|
+
* skin, so the gate is the toolbar shape class. `--tf-frame-border` is the shorthand
|
|
293
|
+
* for all sides; the bottom emphasis composes from Vuetify's border-opacity (it is a
|
|
294
|
+
* shading rule, not a catalog token).
|
|
295
|
+
*/
|
|
296
|
+
.tiptapify-frame {
|
|
297
|
+
border: var(--tf-frame-border, 1px solid rgba(var(--v-border-color), var(--v-border-opacity)));
|
|
298
|
+
border-top: 0;
|
|
299
|
+
background: var(--tf-frame-bg, transparent);
|
|
300
|
+
box-shadow: var(--tf-frame-shadow, none);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.tiptapify-shape-toolbar-flat .tiptapify-frame {
|
|
304
|
+
/* A flat toolbar lives inside the frame: the top edge closes like the other sides... */
|
|
305
|
+
border-top: var(--tf-frame-border, 1px solid rgba(var(--v-border-color), var(--v-border-opacity)));
|
|
306
|
+
|
|
307
|
+
/* ...and the bottom edge carries the frame's visual weight (1.6x border opacity) */
|
|
308
|
+
border-bottom-color: rgba(var(--v-border-color), calc(var(--v-border-opacity) * 1.6));
|
|
309
|
+
}
|
|
310
|
+
|
|
235
311
|
.resizable {
|
|
236
312
|
resize: vertical;
|
|
237
313
|
overflow: auto;
|
|
238
314
|
}
|
|
239
315
|
|
|
240
316
|
.tiptapify-container {
|
|
317
|
+
/* The containing block of the bubble menus, so they scroll with the text they belong to */
|
|
318
|
+
position: relative;
|
|
241
319
|
display: flex;
|
|
242
320
|
flex-direction: column;
|
|
243
321
|
}
|
|
@@ -281,6 +359,8 @@ onBeforeUnmount(() => {
|
|
|
281
359
|
|
|
282
360
|
/* List styles */
|
|
283
361
|
ul, ol {
|
|
362
|
+
/* Next to a floated image the markers stay out of it */
|
|
363
|
+
display: flow-root;
|
|
284
364
|
padding: 0 1rem;
|
|
285
365
|
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
|
286
366
|
|
|
@@ -518,91 +598,211 @@ onBeforeUnmount(() => {
|
|
|
518
598
|
display: block;
|
|
519
599
|
}
|
|
520
600
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
background: rgba(0, 0, 0, 0.5);
|
|
525
|
-
border: 1px solid rgba(255, 255, 255, 0.8);
|
|
526
|
-
border-radius: 2px;
|
|
527
|
-
z-index: 10;
|
|
601
|
+
/* Image with the caption under it (see extensions/components/media/image/caption.ts) */
|
|
602
|
+
.tiptapify-image {
|
|
603
|
+
--tiptapify-image-max-height: 400px;
|
|
528
604
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
605
|
+
flex-direction: column;
|
|
606
|
+
gap: 4px;
|
|
607
|
+
width: fit-content;
|
|
608
|
+
max-width: 100%;
|
|
609
|
+
margin: 1rem 0;
|
|
532
610
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
height: 8px;
|
|
540
|
-
}
|
|
611
|
+
/* Never wider than the text, never taller than the max height: big photos stay out of the way */
|
|
612
|
+
img {
|
|
613
|
+
max-width: 100%;
|
|
614
|
+
height: auto !important;
|
|
615
|
+
margin: 0;
|
|
616
|
+
}
|
|
541
617
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
618
|
+
[data-resize-wrapper] {
|
|
619
|
+
max-width: min(100%, calc(var(--tiptapify-image-max-height) * var(--tiptapify-image-ratio, 100)));
|
|
620
|
+
border-radius: var(--tf-image-radius, 0.125rem);
|
|
621
|
+
outline: 1px solid transparent;
|
|
622
|
+
transition: outline-color 0.15s;
|
|
623
|
+
}
|
|
547
624
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
625
|
+
/* One block: hovering outlines it, a click selects it and shows its toolbar */
|
|
626
|
+
img {
|
|
627
|
+
cursor: pointer;
|
|
628
|
+
border-radius: var(--tf-image-radius, 0px);
|
|
629
|
+
background: var(--tf-image-bg, transparent);
|
|
630
|
+
}
|
|
553
631
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
cursor: nesw-resize;
|
|
558
|
-
}
|
|
632
|
+
&.ProseMirror-selectednode [data-resize-wrapper] {
|
|
633
|
+
outline: var(--tf-image-outline, 1px solid rgba(0, 0, 0, 0.25));
|
|
634
|
+
}
|
|
559
635
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
636
|
+
/* Centered unless floated; a floated image lets the text wrap around it */
|
|
637
|
+
&:not([data-align]):not(.tiptapify-slide) {
|
|
638
|
+
align-items: center;
|
|
639
|
+
margin-inline: auto;
|
|
565
640
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
&[data-resize-handle='bottom'] {
|
|
569
|
-
height: 6px;
|
|
570
|
-
left: 8px;
|
|
571
|
-
right: 8px;
|
|
641
|
+
.tiptapify-image-caption {
|
|
642
|
+
text-align: center;
|
|
572
643
|
}
|
|
644
|
+
}
|
|
573
645
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
646
|
+
&[data-align='left'],
|
|
647
|
+
&[data-align='right'] {
|
|
648
|
+
max-width: 50%;
|
|
649
|
+
margin-top: 0.25rem;
|
|
650
|
+
}
|
|
578
651
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
652
|
+
&[data-align='left'] {
|
|
653
|
+
float: left;
|
|
654
|
+
margin-right: 1.25rem;
|
|
655
|
+
}
|
|
583
656
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
657
|
+
&[data-align='right'] {
|
|
658
|
+
float: right;
|
|
659
|
+
margin-left: 1.25rem;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
590
662
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
663
|
+
/* Images in a row shown as a slider (extensions/components/media/image/slider.ts): a numbered grid */
|
|
664
|
+
.tiptapify-image.tiptapify-slide {
|
|
665
|
+
position: relative;
|
|
666
|
+
display: inline-flex !important;
|
|
667
|
+
box-sizing: border-box;
|
|
668
|
+
width: calc(100% / 3);
|
|
669
|
+
margin: 0;
|
|
670
|
+
padding: 8px 6px;
|
|
671
|
+
vertical-align: top;
|
|
672
|
+
background: rgba(var(--v-theme-primary), 0.06);
|
|
595
673
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
674
|
+
[data-resize-wrapper] {
|
|
675
|
+
width: 100%;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
img {
|
|
679
|
+
width: 100% !important;
|
|
680
|
+
max-width: none;
|
|
681
|
+
aspect-ratio: 4 / 3;
|
|
682
|
+
object-fit: cover;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
[data-resize-handle] {
|
|
686
|
+
display: none !important;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
&::before {
|
|
690
|
+
position: absolute;
|
|
691
|
+
top: 14px;
|
|
692
|
+
left: 12px;
|
|
693
|
+
z-index: 1;
|
|
694
|
+
padding: 0 6px;
|
|
695
|
+
border-radius: 4px;
|
|
696
|
+
background: rgba(0, 0, 0, 0.6);
|
|
697
|
+
color: #fff;
|
|
698
|
+
content: attr(data-slide-label);
|
|
699
|
+
font-size: 12px;
|
|
700
|
+
font-weight: 600;
|
|
701
|
+
line-height: 20px;
|
|
702
|
+
pointer-events: none;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
&:has(.tiptapify-slide) {
|
|
707
|
+
container-type: inline-size;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/* The caret between two slides: a vertical line at the edge of the slide before it */
|
|
711
|
+
.tiptapify-slide + .ProseMirror-gapcursor:has(+ .tiptapify-slide) {
|
|
712
|
+
display: inline-block;
|
|
713
|
+
|
|
714
|
+
&::after {
|
|
715
|
+
top: 8px;
|
|
716
|
+
width: 0;
|
|
717
|
+
height: calc((100cqw / 3 - 12px) * 0.75);
|
|
718
|
+
border-top: 0;
|
|
719
|
+
border-left: 2px solid currentColor;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.tiptapify-image-caption {
|
|
724
|
+
width: 100%;
|
|
725
|
+
min-width: 0;
|
|
726
|
+
padding: 2px 0 3px;
|
|
727
|
+
border: 0;
|
|
728
|
+
border-bottom: 1px dashed transparent;
|
|
729
|
+
background: transparent;
|
|
730
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
731
|
+
font: inherit;
|
|
732
|
+
font-size: 0.875em;
|
|
733
|
+
outline: none;
|
|
734
|
+
transition: border-color 0.15s;
|
|
735
|
+
|
|
736
|
+
&::placeholder {
|
|
737
|
+
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
&:hover:not(:disabled),
|
|
741
|
+
&:focus {
|
|
742
|
+
border-bottom-color: rgba(var(--v-border-color), 0.4);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
&:disabled:placeholder-shown {
|
|
746
|
+
display: none;
|
|
600
747
|
}
|
|
748
|
+
}
|
|
601
749
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
750
|
+
/* Resize handles of a selected image: dark squares at the corners and edge middles */
|
|
751
|
+
[data-node='image'] [data-resize-handle] {
|
|
752
|
+
display: none;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
[data-node='image'].ProseMirror-selectednode [data-resize-handle] {
|
|
756
|
+
position: absolute;
|
|
757
|
+
z-index: 10;
|
|
758
|
+
display: block;
|
|
759
|
+
width: 10px;
|
|
760
|
+
height: 10px;
|
|
761
|
+
border: var(--tf-handle-border, none);
|
|
762
|
+
border-radius: var(--tf-handle-radius, 2px);
|
|
763
|
+
background: var(--tf-handle-bg, rgba(0, 0, 0, 0.5));
|
|
764
|
+
box-shadow: var(--tf-handle-shadow, none);
|
|
765
|
+
transition: background-color 0.15s;
|
|
766
|
+
|
|
767
|
+
/* Longhand on purpose: the hover needle in the measurement bundle reads
|
|
768
|
+
* `background-color` from CSSOM, and a shorthand carrying var() has no
|
|
769
|
+
* retrievable longhand (pending-substitution value).
|
|
770
|
+
*/
|
|
771
|
+
&:hover {
|
|
772
|
+
background-color: var(--tf-handle-bg-hover, rgba(0, 0, 0, 0.8));
|
|
605
773
|
}
|
|
774
|
+
|
|
775
|
+
&[data-resize-handle='top-left'] { top: -5px !important; left: -5px !important; cursor: nwse-resize; }
|
|
776
|
+
&[data-resize-handle='top-right'] { top: -5px !important; right: -5px !important; left: auto !important; cursor: nesw-resize; }
|
|
777
|
+
&[data-resize-handle='bottom-left'] { bottom: -5px !important; left: -5px !important; top: auto !important; cursor: nesw-resize; }
|
|
778
|
+
&[data-resize-handle='bottom-right'] { right: -5px !important; bottom: -5px !important; top: auto !important; left: auto !important; cursor: nwse-resize; }
|
|
779
|
+
&[data-resize-handle='top'] { top: -5px !important; left: calc(50% - 5px) !important; right: auto !important; cursor: ns-resize; }
|
|
780
|
+
&[data-resize-handle='bottom'] { bottom: -5px !important; top: auto !important; left: calc(50% - 5px) !important; right: auto !important; cursor: ns-resize; }
|
|
781
|
+
&[data-resize-handle='left'] { left: -5px !important; top: calc(50% - 5px) !important; bottom: auto !important; cursor: ew-resize; }
|
|
782
|
+
&[data-resize-handle='right'] { right: -5px !important; left: auto !important; top: calc(50% - 5px) !important; bottom: auto !important; cursor: ew-resize; }
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/*
|
|
787
|
+
* The quiet image chrome (shape: `buttons: quiet`): the outline around an image becomes an
|
|
788
|
+
* inset 2px ring (the classic flush 1px hairline is the base rule's fallback) and hovering
|
|
789
|
+
* tints it. Gated on the buttons shape because that is the flag of the 0.3.3 quiet look this
|
|
790
|
+
* structure belongs to; the colors stay tokens (`--tf-image-outline*`), the structural
|
|
791
|
+
* width/offset pair is the shape's base.
|
|
792
|
+
*/
|
|
793
|
+
.tiptapify-shape-buttons-quiet .tiptap .tiptapify-image {
|
|
794
|
+
[data-resize-wrapper] {
|
|
795
|
+
outline: 2px solid transparent;
|
|
796
|
+
outline-offset: 2px;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
&:hover [data-resize-wrapper] {
|
|
800
|
+
outline-color: var(--tf-image-outline-hover, rgba(var(--v-theme-primary), 0.35));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
&.ProseMirror-selectednode [data-resize-wrapper] {
|
|
804
|
+
outline: var(--tf-image-outline, 2px solid rgb(var(--v-theme-primary)));
|
|
805
|
+
outline-offset: 2px;
|
|
606
806
|
}
|
|
607
807
|
}
|
|
608
808
|
|
|
@@ -622,7 +822,7 @@ onBeforeUnmount(() => {
|
|
|
622
822
|
*/
|
|
623
823
|
.tiptapify-dialog-grid {
|
|
624
824
|
display: grid;
|
|
625
|
-
gap: 24px;
|
|
825
|
+
gap: var(--tf-dialog-grid-gap, 24px);
|
|
626
826
|
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
627
827
|
|
|
628
828
|
> .tiptapify-dialog-col {
|
|
@@ -649,6 +849,17 @@ onBeforeUnmount(() => {
|
|
|
649
849
|
}
|
|
650
850
|
}
|
|
651
851
|
|
|
852
|
+
/*
|
|
853
|
+
* The soft dialog grid.
|
|
854
|
+
*
|
|
855
|
+
* The dialog is teleported to the overlay root, so it has no shape class
|
|
856
|
+
* ancestor: the shape reaches it through the dialog's own modifier class,
|
|
857
|
+
* the same way every other soft dialog style applies.
|
|
858
|
+
*/
|
|
859
|
+
.tiptapify-dialog--soft .tiptapify-dialog-grid {
|
|
860
|
+
gap: var(--tf-dialog-grid-gap, 16px);
|
|
861
|
+
}
|
|
862
|
+
|
|
652
863
|
.tiptapify-dialog-actions {
|
|
653
864
|
display: flex;
|
|
654
865
|
flex-wrap: wrap;
|