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
|
@@ -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 { ListStylePreset, ListType } from '@tiptapify/extensions/components/list/styles'
|
|
9
11
|
import { inject, PropType, Ref } from 'vue'
|
|
10
12
|
import { ComposerTranslation } from 'vue-i18n'
|
|
@@ -21,7 +23,7 @@ const props = defineProps({
|
|
|
21
23
|
|
|
22
24
|
const emit = defineEmits(['toggle'])
|
|
23
25
|
|
|
24
|
-
const { variantBtn } = useTiptapifyConfig()
|
|
26
|
+
const { variantBtn, shape } = useTiptapifyConfig()
|
|
25
27
|
|
|
26
28
|
const editor = inject('tiptapifyEditor') as Ref<Editor>
|
|
27
29
|
|
|
@@ -49,7 +51,7 @@ function presetLabel(preset: ListStylePreset): string {
|
|
|
49
51
|
<BtnIcon :icon="`mdiSvg:${icon}`" />
|
|
50
52
|
</VBtn>
|
|
51
53
|
|
|
52
|
-
<VMenu content-class="
|
|
54
|
+
<VMenu :content-class="menuContentClass(shape.menu === 'soft')">
|
|
53
55
|
<template #activator="{ props: menuProps }">
|
|
54
56
|
<VBtn
|
|
55
57
|
class="tiptapify-list-style-button__chevron"
|
|
@@ -63,23 +65,25 @@ function presetLabel(preset: ListStylePreset): string {
|
|
|
63
65
|
</VBtn>
|
|
64
66
|
</template>
|
|
65
67
|
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<span class="tiptapify-list-style-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
<SkinScope>
|
|
69
|
+
<div class="tiptapify-list-style-grid">
|
|
70
|
+
<button
|
|
71
|
+
v-for="preset in presets"
|
|
72
|
+
:key="preset.value ?? 'default'"
|
|
73
|
+
type="button"
|
|
74
|
+
class="tiptapify-list-style-grid__item"
|
|
75
|
+
:class="{ 'tiptapify-list-style-grid__item--active': isPresetActive(preset) }"
|
|
76
|
+
:title="presetLabel(preset)"
|
|
77
|
+
:aria-label="presetLabel(preset)"
|
|
78
|
+
@click="editor.chain().focus().setListStyle(listType, preset.value).run()"
|
|
79
|
+
>
|
|
80
|
+
<span v-for="(marker, index) in preset.markers" :key="index" class="tiptapify-list-style-grid__line">
|
|
81
|
+
<span class="tiptapify-list-style-grid__marker">{{ marker }}</span>
|
|
82
|
+
<span class="tiptapify-list-style-grid__bar" />
|
|
83
|
+
</span>
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</SkinScope>
|
|
83
87
|
</VMenu>
|
|
84
88
|
</div>
|
|
85
89
|
</template>
|
|
@@ -102,16 +106,12 @@ function presetLabel(preset: ListStylePreset): string {
|
|
|
102
106
|
</style>
|
|
103
107
|
|
|
104
108
|
<style lang="scss">
|
|
105
|
-
/* Teleported with the menu, so not scoped */
|
|
109
|
+
/* Teleported with the menu, so not scoped. The pastel skin of the grid lives in src/skins/menu.css. */
|
|
106
110
|
.tiptapify-list-style-grid {
|
|
107
111
|
display: grid;
|
|
108
112
|
grid-template-columns: repeat(3, 64px);
|
|
109
113
|
gap: 6px;
|
|
110
|
-
padding:
|
|
111
|
-
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
112
|
-
border-radius: 8px;
|
|
113
|
-
background: rgb(var(--v-theme-surface));
|
|
114
|
-
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
114
|
+
padding: 4px;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
.tiptapify-list-style-grid__item {
|
|
@@ -4,8 +4,10 @@ import * as mdi from '@mdi/js'
|
|
|
4
4
|
import { Level } from '@tiptap/extension-heading'
|
|
5
5
|
import { Editor } from '@tiptap/vue-3'
|
|
6
6
|
import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
|
|
7
|
+
import SkinScope from '@tiptapify/components/SkinScope.vue'
|
|
7
8
|
import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
|
|
8
9
|
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
10
|
+
import { menuContentClass } from '@tiptapify/skins/menu'
|
|
9
11
|
import { inject, Ref } from 'vue'
|
|
10
12
|
|
|
11
13
|
const props = defineProps({
|
|
@@ -14,7 +16,7 @@ const props = defineProps({
|
|
|
14
16
|
inline: { type: Boolean, default: () => false },
|
|
15
17
|
})
|
|
16
18
|
|
|
17
|
-
const { variantBtn } = useTiptapifyConfig()
|
|
19
|
+
const { variantBtn, shape } = useTiptapifyConfig()
|
|
18
20
|
|
|
19
21
|
interface MDIIcons { [key: string]: string }
|
|
20
22
|
const mdiIcons = mdi as MDIIcons
|
|
@@ -73,34 +75,36 @@ function currentStyleIcon(): string {
|
|
|
73
75
|
<BtnIcon :icon="`mdiSvg:${mdi.mdiChevronDown}`" class="tiptapify-dropdown-button__chevron" />
|
|
74
76
|
</VBtn>
|
|
75
77
|
|
|
76
|
-
<VMenu :activator="`#tiptapify-heading-button-${editor.instanceId}`" content-class="
|
|
77
|
-
<
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
78
|
+
<VMenu :activator="`#tiptapify-heading-button-${editor.instanceId}`" :content-class="menuContentClass(shape.menu === 'soft')">
|
|
79
|
+
<SkinScope>
|
|
80
|
+
<VList density="compact">
|
|
81
|
+
<VListItem
|
|
82
|
+
v-if="withParagraph"
|
|
83
|
+
link
|
|
84
|
+
:active="editor.isActive('paragraph')"
|
|
85
|
+
@click="editor.chain().focus().setParagraph().run()"
|
|
86
|
+
>
|
|
87
|
+
<Tooltip :label="t('style.paragraph')" hotkey="Mod-Alt-0" />
|
|
88
|
+
<template #prepend>
|
|
89
|
+
<BtnIcon :icon="`mdiSvg:${mdiIcons['mdiFormatParagraph']}`" />
|
|
90
|
+
</template>
|
|
91
|
+
<VListItemTitle>{{ t('style.paragraph') }}</VListItemTitle>
|
|
92
|
+
</VListItem>
|
|
93
|
+
<VListItem
|
|
94
|
+
v-for="headingLevel in headingLevels"
|
|
95
|
+
:key="headingLevel"
|
|
96
|
+
link
|
|
97
|
+
:active="editor.isActive('heading', { level: headingLevel })"
|
|
98
|
+
@click="editor.chain().focus().toggleHeading({ level: headingLevel }).run()"
|
|
99
|
+
>
|
|
100
|
+
<Tooltip :label="t(`style.headings.h${headingLevel}`)" :hotkey="`Mod-Alt-${headingLevel}`" />
|
|
101
|
+
<template #prepend>
|
|
102
|
+
<BtnIcon :icon="`mdiSvg:${mdiIcons[`mdiFormatHeader${headingLevel}`]}`" />
|
|
103
|
+
</template>
|
|
104
|
+
<VListItemTitle>{{ t(`style.headings.h${headingLevel}`) }}</VListItemTitle>
|
|
105
|
+
</VListItem>
|
|
106
|
+
</VList>
|
|
107
|
+
</SkinScope>
|
|
104
108
|
</VMenu>
|
|
105
109
|
</template>
|
|
106
110
|
</template>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { registerSkin, defineSkin } from '@tiptapify/index'
|
|
5
|
+
import { registerSkin as fromRegistry, defineSkin as defineFromRegistry } from '@tiptapify/skins/registry'
|
|
6
|
+
|
|
7
|
+
describe('package entry exports', () => {
|
|
8
|
+
it('exports registerSkin and defineSkin as functions', () => {
|
|
9
|
+
expect(typeof registerSkin).toBe('function')
|
|
10
|
+
expect(typeof defineSkin).toBe('function')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('re-exports the registry functions, not copies', () => {
|
|
14
|
+
expect(registerSkin).toBe(fromRegistry)
|
|
15
|
+
expect(defineSkin).toBe(defineFromRegistry)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('defineSkin is identity: the same object comes back', () => {
|
|
19
|
+
const skin = { extends: 'pastel', colors: { toolbarBg: 'rebeccapurple' } }
|
|
20
|
+
|
|
21
|
+
expect(defineSkin(skin)).toBe(skin)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('index.d.ts skin-type parity with src/types/editor.ts', () => {
|
|
26
|
+
// index.d.ts is hand-mirrored from the source types; this guard compares the
|
|
27
|
+
// exported skin-type declarations member-for-member (declared shape, not
|
|
28
|
+
// comments/formatting), ending the silent-drift risk of the hand mirror.
|
|
29
|
+
const SKIN_TYPES = [
|
|
30
|
+
'TiptapifySkin',
|
|
31
|
+
'TiptapifyShape',
|
|
32
|
+
'ResolvedShape',
|
|
33
|
+
'TiptapifySkinColors',
|
|
34
|
+
'TiptapifySkinMetrics',
|
|
35
|
+
'TiptapifySkinConfig',
|
|
36
|
+
'TiptapifySkinInput',
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
// cwd is the vitest root (the repo): the guard pins files of the package root
|
|
40
|
+
const dts = readFileSync(path.resolve(process.cwd(), 'index.d.ts'), 'utf8')
|
|
41
|
+
const source = readFileSync(path.resolve(process.cwd(), 'src/types/editor.ts'), 'utf8')
|
|
42
|
+
|
|
43
|
+
// The declaration block of `name`: a brace body to its closing brace,
|
|
44
|
+
// an alias body (no `{` on the header line) to the end of its line.
|
|
45
|
+
// Constraints of this extractor, adequate for the skin blocks it pins:
|
|
46
|
+
// declarations must be single-line aliases or brace-bodied types (the
|
|
47
|
+
// index.d.ts mirror style); the `//`-stripping in normalize treats any
|
|
48
|
+
// `//` as a comment start, so these blocks must contain no URLs in strings.
|
|
49
|
+
function declaration (file: string, name: string): string {
|
|
50
|
+
const lines = file.split('\n')
|
|
51
|
+
const start = lines.findIndex((line) => new RegExp(`^export (interface|type) ${name}\\b`).test(line))
|
|
52
|
+
if (start < 0) return ''
|
|
53
|
+
if (lines[start].indexOf('{') < 0) return lines[start]
|
|
54
|
+
let depth = 0
|
|
55
|
+
for (let i = start; i < lines.length; i++) {
|
|
56
|
+
for (const ch of lines[i]) {
|
|
57
|
+
if (ch === '{') depth++
|
|
58
|
+
if (ch === '}') depth--
|
|
59
|
+
}
|
|
60
|
+
if (depth === 0) return lines.slice(start, i + 1).join('\n')
|
|
61
|
+
}
|
|
62
|
+
return ''
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Tolerant to the formatting freedom of a hand-written mirror: interface vs type
|
|
66
|
+
// literal, comments, indentation, line breaks inside the body
|
|
67
|
+
function normalize (block: string, name: string): string {
|
|
68
|
+
return block
|
|
69
|
+
.split('\n')
|
|
70
|
+
.map((line) => line.replace(/\/\/.*$/, ''))
|
|
71
|
+
.join(' ')
|
|
72
|
+
.replace(new RegExp(`interface ${name} \\{`), `${name} {`)
|
|
73
|
+
.replace(new RegExp(`type ${name} = \\{`), `${name} {`)
|
|
74
|
+
.replace(/\s+/g, ' ')
|
|
75
|
+
.trim()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const name of SKIN_TYPES) {
|
|
79
|
+
it(`${name} is mirrored member-for-member`, () => {
|
|
80
|
+
const inDts = declaration(dts, name)
|
|
81
|
+
const inSource = declaration(source, name)
|
|
82
|
+
|
|
83
|
+
expect(inDts, `${name} missing from index.d.ts`).not.toBe('')
|
|
84
|
+
expect(inSource, `${name} missing from src/types/editor.ts`).not.toBe('')
|
|
85
|
+
expect(normalize(inDts, name)).toBe(normalize(inSource, name))
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import TiptapifyDialog from '@tiptapify/components/UI/TiptapifyDialog.vue'
|
|
|
4
4
|
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
5
5
|
import { TiptapifyFooterAlignment } from '@tiptapify/types/editor'
|
|
6
6
|
import { createAiBackendProvider } from '@tiptapify/extensions/components/ai/backend'
|
|
7
|
+
import { registerSkin, defineSkin } from '@tiptapify/skins/registry'
|
|
7
8
|
|
|
8
9
|
import { Editor as TipTapEditor, nodeViewProps, NodeViewWrapper, VueNodeViewRenderer } from '@tiptap/vue-3'
|
|
9
10
|
import { Node, Mark, markInputRule, markPasteRule, mergeAttributes } from '@tiptap/core'
|
|
@@ -13,6 +14,9 @@ import * as mdi from '@mdi/js'
|
|
|
13
14
|
|
|
14
15
|
import { messages } from './i18n'
|
|
15
16
|
|
|
17
|
+
// The shared menu skin stylesheet: shipped in the dist CSS next to the SFC styles (spec: Menu CSS strategy)
|
|
18
|
+
import './skins/menu.css'
|
|
19
|
+
|
|
16
20
|
interface PackageOptions {
|
|
17
21
|
i18n?: any;
|
|
18
22
|
}
|
|
@@ -46,11 +50,22 @@ export {
|
|
|
46
50
|
markInputRule,
|
|
47
51
|
markPasteRule,
|
|
48
52
|
mergeAttributes,
|
|
53
|
+
registerSkin,
|
|
54
|
+
defineSkin,
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export type { TiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
52
58
|
export type { TiptapifyFooterAlignment }
|
|
53
59
|
export type { CommandProps, InputRuleMatch, PasteRuleMatch }
|
|
60
|
+
export type {
|
|
61
|
+
TiptapifySkin,
|
|
62
|
+
TiptapifyShape,
|
|
63
|
+
TiptapifySkinColors,
|
|
64
|
+
TiptapifySkinMetrics,
|
|
65
|
+
TiptapifySkinConfig,
|
|
66
|
+
TiptapifySkinInput,
|
|
67
|
+
ResolvedShape,
|
|
68
|
+
} from '@tiptapify/types/editor'
|
|
54
69
|
export type {
|
|
55
70
|
TiptapifyAiBackendProviderOptions,
|
|
56
71
|
TiptapifyAiBackendRequest,
|