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
@@ -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="tiptapify-menu">
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
- <div class="tiptapify-list-style-grid">
67
- <button
68
- v-for="preset in presets"
69
- :key="preset.value ?? 'default'"
70
- type="button"
71
- class="tiptapify-list-style-grid__item"
72
- :class="{ 'tiptapify-list-style-grid__item--active': isPresetActive(preset) }"
73
- :title="presetLabel(preset)"
74
- :aria-label="presetLabel(preset)"
75
- @click="editor.chain().focus().setListStyle(listType, preset.value).run()"
76
- >
77
- <span v-for="(marker, index) in preset.markers" :key="index" class="tiptapify-list-style-grid__line">
78
- <span class="tiptapify-list-style-grid__marker">{{ marker }}</span>
79
- <span class="tiptapify-list-style-grid__bar" />
80
- </span>
81
- </button>
82
- </div>
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: 6px;
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="tiptapify-menu">
77
- <VList density="compact">
78
- <VListItem
79
- v-if="withParagraph"
80
- link
81
- :active="editor.isActive('paragraph')"
82
- @click="editor.chain().focus().setParagraph().run()"
83
- >
84
- <Tooltip :label="t('style.paragraph')" hotkey="Mod-Alt-0" />
85
- <template #prepend>
86
- <BtnIcon :icon="`mdiSvg:${mdiIcons['mdiFormatParagraph']}`" />
87
- </template>
88
- <VListItemTitle>{{ t('style.paragraph') }}</VListItemTitle>
89
- </VListItem>
90
- <VListItem
91
- v-for="headingLevel in headingLevels"
92
- :key="headingLevel"
93
- link
94
- :active="editor.isActive('heading', { level: headingLevel })"
95
- @click="editor.chain().focus().toggleHeading({ level: headingLevel }).run()"
96
- >
97
- <Tooltip :label="t(`style.headings.h${headingLevel}`)" :hotkey="`Mod-Alt-${headingLevel}`" />
98
- <template #prepend>
99
- <BtnIcon :icon="`mdiSvg:${mdiIcons[`mdiFormatHeader${headingLevel}`]}`" />
100
- </template>
101
- <VListItemTitle>{{ t(`style.headings.h${headingLevel}`) }}</VListItemTitle>
102
- </VListItem>
103
- </VList>
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,