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
|
@@ -6,7 +6,15 @@ 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'
|
|
12
20
|
import ImageBubbleMenu from '@tiptapify/components/ImageBubbleMenu.vue'
|
|
@@ -15,6 +23,7 @@ import { useI18n } from 'vue-i18n'
|
|
|
15
23
|
|
|
16
24
|
import { getTiptapEditor } from '@tiptapify/components/index'
|
|
17
25
|
import { TIPTAPIFY_CONFIG_KEY } from '@tiptapify/composables/useTiptapifyConfig'
|
|
26
|
+
import { resolveSkin } from '@tiptapify/skins/registry'
|
|
18
27
|
import { createAiBackendProvider } from '@tiptapify/extensions/components/ai/backend'
|
|
19
28
|
|
|
20
29
|
import Footer from '@tiptapify/components/Footer.vue'
|
|
@@ -52,6 +61,8 @@ const props = defineProps({
|
|
|
52
61
|
loadingHeight: { type: String, default() { return '1px' } },
|
|
53
62
|
// From this many images in a row the editor shows them as a slider grid; 0 turns it off
|
|
54
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 } },
|
|
55
66
|
})
|
|
56
67
|
|
|
57
68
|
const emit = defineEmits(['update:modelValue', 'editor-ready', 'content-changed'])
|
|
@@ -151,9 +162,33 @@ const scrollContainer = ref<HTMLElement | null>(null)
|
|
|
151
162
|
provide('tiptapifyScrollContainer', scrollContainer)
|
|
152
163
|
provide('tiptapifyI18n', { t })
|
|
153
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
|
+
|
|
154
185
|
provide(TIPTAPIFY_CONFIG_KEY, {
|
|
155
186
|
variantBtn: toRef(props, 'variantBtn'),
|
|
156
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),
|
|
157
192
|
})
|
|
158
193
|
|
|
159
194
|
editor.value?.chain().setFontFamily(props.defaultFontFamily).run()
|
|
@@ -173,7 +208,11 @@ onBeforeUnmount(() => {
|
|
|
173
208
|
</script>
|
|
174
209
|
|
|
175
210
|
<template>
|
|
176
|
-
<div
|
|
211
|
+
<div
|
|
212
|
+
:id="`tiptapify-editor-${editor?.instanceId}`"
|
|
213
|
+
:class="[resolvedSkin.className, shapeClasses]"
|
|
214
|
+
:style="resolvedSkin.tokens"
|
|
215
|
+
>
|
|
177
216
|
<div :class="`tiptapify-frame rounded-${rounded}`">
|
|
178
217
|
<template v-if="toolbar">
|
|
179
218
|
<Toolbar
|
|
@@ -245,11 +284,28 @@ onBeforeUnmount(() => {
|
|
|
245
284
|
--border: 1px solid var(--gray-2);
|
|
246
285
|
}
|
|
247
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
|
+
*/
|
|
248
296
|
.tiptapify-frame {
|
|
249
|
-
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
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) */
|
|
250
308
|
border-bottom-color: rgba(var(--v-border-color), calc(var(--v-border-opacity) * 1.6));
|
|
251
|
-
background: rgb(var(--v-theme-surface));
|
|
252
|
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
253
309
|
}
|
|
254
310
|
|
|
255
311
|
.resizable {
|
|
@@ -557,29 +613,24 @@ onBeforeUnmount(() => {
|
|
|
557
613
|
max-width: 100%;
|
|
558
614
|
height: auto !important;
|
|
559
615
|
margin: 0;
|
|
560
|
-
border-radius: 4px;
|
|
561
|
-
background: rgba(var(--v-theme-on-surface), 0.04);
|
|
562
616
|
}
|
|
563
617
|
|
|
564
618
|
[data-resize-wrapper] {
|
|
565
619
|
max-width: min(100%, calc(var(--tiptapify-image-max-height) * var(--tiptapify-image-ratio, 100)));
|
|
566
|
-
border-radius:
|
|
567
|
-
outline:
|
|
568
|
-
outline-offset: 2px;
|
|
620
|
+
border-radius: var(--tf-image-radius, 0.125rem);
|
|
621
|
+
outline: 1px solid transparent;
|
|
569
622
|
transition: outline-color 0.15s;
|
|
570
623
|
}
|
|
571
624
|
|
|
572
625
|
/* One block: hovering outlines it, a click selects it and shows its toolbar */
|
|
573
626
|
img {
|
|
574
627
|
cursor: pointer;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
&:hover [data-resize-wrapper] {
|
|
578
|
-
outline-color: rgba(var(--v-theme-primary), 0.35);
|
|
628
|
+
border-radius: var(--tf-image-radius, 0px);
|
|
629
|
+
background: var(--tf-image-bg, transparent);
|
|
579
630
|
}
|
|
580
631
|
|
|
581
632
|
&.ProseMirror-selectednode [data-resize-wrapper] {
|
|
582
|
-
outline
|
|
633
|
+
outline: var(--tf-image-outline, 1px solid rgba(0, 0, 0, 0.25));
|
|
583
634
|
}
|
|
584
635
|
|
|
585
636
|
/* Centered unless floated; a floated image lets the text wrap around it */
|
|
@@ -696,7 +747,7 @@ onBeforeUnmount(() => {
|
|
|
696
747
|
}
|
|
697
748
|
}
|
|
698
749
|
|
|
699
|
-
/* Resize handles of a selected image:
|
|
750
|
+
/* Resize handles of a selected image: dark squares at the corners and edge middles */
|
|
700
751
|
[data-node='image'] [data-resize-handle] {
|
|
701
752
|
display: none;
|
|
702
753
|
}
|
|
@@ -707,10 +758,19 @@ onBeforeUnmount(() => {
|
|
|
707
758
|
display: block;
|
|
708
759
|
width: 10px;
|
|
709
760
|
height: 10px;
|
|
710
|
-
border:
|
|
711
|
-
border-radius:
|
|
712
|
-
background:
|
|
713
|
-
box-shadow:
|
|
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));
|
|
773
|
+
}
|
|
714
774
|
|
|
715
775
|
&[data-resize-handle='top-left'] { top: -5px !important; left: -5px !important; cursor: nwse-resize; }
|
|
716
776
|
&[data-resize-handle='top-right'] { top: -5px !important; right: -5px !important; left: auto !important; cursor: nesw-resize; }
|
|
@@ -723,6 +783,29 @@ onBeforeUnmount(() => {
|
|
|
723
783
|
}
|
|
724
784
|
}
|
|
725
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;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
726
809
|
/*
|
|
727
810
|
* Dialog layout primitives.
|
|
728
811
|
*
|
|
@@ -739,7 +822,7 @@ onBeforeUnmount(() => {
|
|
|
739
822
|
*/
|
|
740
823
|
.tiptapify-dialog-grid {
|
|
741
824
|
display: grid;
|
|
742
|
-
gap:
|
|
825
|
+
gap: var(--tf-dialog-grid-gap, 24px);
|
|
743
826
|
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
744
827
|
|
|
745
828
|
> .tiptapify-dialog-col {
|
|
@@ -766,6 +849,17 @@ onBeforeUnmount(() => {
|
|
|
766
849
|
}
|
|
767
850
|
}
|
|
768
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
|
+
|
|
769
863
|
.tiptapify-dialog-actions {
|
|
770
864
|
display: flex;
|
|
771
865
|
flex-wrap: wrap;
|
|
@@ -7,6 +7,7 @@ import { computed, inject, PropType, Ref } from 'vue'
|
|
|
7
7
|
import { itemsPropType, toolbarSections } from '@tiptapify/types/toolbarTypes'
|
|
8
8
|
|
|
9
9
|
import { default as toolbarItemsGroups, availableItems } from '@tiptapify/components/Toolbar/items'
|
|
10
|
+
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
10
11
|
|
|
11
12
|
const props = defineProps({
|
|
12
13
|
variantField: { type: String as PropType<variantFieldTypes>, default() { return defaults.variantField } },
|
|
@@ -23,6 +24,10 @@ const props = defineProps({
|
|
|
23
24
|
|
|
24
25
|
const editor = inject('tiptapifyEditor') as Ref<Editor>
|
|
25
26
|
|
|
27
|
+
// The toolbar shape flag styles the bar: a flat tinted bar when `toolbar` is flat;
|
|
28
|
+
// the classic shape keeps the raised Vuetify toolbar
|
|
29
|
+
const { shape } = useTiptapifyConfig()
|
|
30
|
+
|
|
26
31
|
const propsItems = computed(() => props.items)
|
|
27
32
|
const propsItemsExclude = computed(() => props.itemsExclude)
|
|
28
33
|
|
|
@@ -272,7 +277,13 @@ toolbarItems = toolbarItems
|
|
|
272
277
|
|
|
273
278
|
<template>
|
|
274
279
|
<div v-if="editor" class="tiptapify-toolbar">
|
|
275
|
-
<VToolbar
|
|
280
|
+
<VToolbar
|
|
281
|
+
:elevation="shape.toolbar === 'flat' ? 0 : 1"
|
|
282
|
+
:flat="shape.toolbar === 'flat'"
|
|
283
|
+
:theme="theme"
|
|
284
|
+
height="auto"
|
|
285
|
+
:class="`rounded-t-${rounded}`"
|
|
286
|
+
>
|
|
276
287
|
<VSlideGroup v-if="toolbarScrollable">
|
|
277
288
|
<Items :items="toolbarItems" />
|
|
278
289
|
</VSlideGroup>
|
|
@@ -284,30 +295,39 @@ toolbarItems = toolbarItems
|
|
|
284
295
|
|
|
285
296
|
<style lang="scss">
|
|
286
297
|
/*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
298
|
+
* Flat toolbar: a tinted bar under a hairline. The raised shape paints the bar with the Vuetify
|
|
299
|
+
* toolbar and its own button variants instead, so this rule set is gated on the shape and its
|
|
300
|
+
* fallbacks are the flat shape's base look (the raised values are Vuetify's, not skin rules).
|
|
290
301
|
*/
|
|
291
|
-
.tiptapify-toolbar {
|
|
302
|
+
.tiptapify-shape-toolbar-flat .tiptapify-toolbar {
|
|
292
303
|
> .v-toolbar {
|
|
293
|
-
background: rgba(var(--v-theme-on-surface), 0.03);
|
|
294
|
-
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
295
|
-
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
304
|
+
background: var(--tf-toolbar-bg, rgba(var(--v-theme-on-surface), 0.03));
|
|
305
|
+
border-bottom: var(--tf-toolbar-border-bottom, 1px solid rgba(var(--v-border-color), var(--v-border-opacity)));
|
|
306
|
+
color: var(--tf-toolbar-fg, rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)));
|
|
296
307
|
}
|
|
297
308
|
}
|
|
298
309
|
|
|
299
310
|
/*
|
|
300
|
-
* Quiet buttons
|
|
301
|
-
*
|
|
302
|
-
*
|
|
311
|
+
* Quiet buttons: ghost icon buttons that only get a background on hover, and a neutral "pressed"
|
|
312
|
+
* state instead of the filled primary button. The buttons keep coloring themselves with
|
|
313
|
+
* `color="primary"` when active; this rule maps that to the pressed look. The rules apply iff
|
|
314
|
+
* `buttons` is quiet: through the root shape class in the toolbar, and through the
|
|
315
|
+
* `.tiptapify-quiet-buttons` carrier that the bubble menus carry only in that shape.
|
|
316
|
+
* Tripled class: outranks Vuetify's variant rules (`.v-btn--variant-elevated` background/shadow
|
|
317
|
+
* are plain (0-1-0), so the doubled/tripled class beats them — no `!important` needed for the
|
|
318
|
+
* resting state). The pressed-state block keeps its `!important`s: an active button carries the
|
|
319
|
+
* runtime theme utility class `.bg-primary`/`.text-primary`, which Vuetify generates with
|
|
320
|
+
* `!important`, and the consumer "admin theme" repaints those colored buttons on hover, focus
|
|
321
|
+
* and press (`.bg-primary:focus` and the like) — plain specificity loses against `!important`;
|
|
322
|
+
* this is the admin-theme defense set of the skin layer (spec: Specificity convention).
|
|
303
323
|
*/
|
|
304
|
-
.tiptapify-toolbar,
|
|
324
|
+
.tiptapify-shape-buttons-quiet .tiptapify-toolbar,
|
|
305
325
|
.tiptapify-quiet-buttons {
|
|
306
326
|
.v-btn.v-btn.v-btn {
|
|
307
|
-
border-radius: 6px;
|
|
308
|
-
box-shadow: none
|
|
309
|
-
background-color: transparent
|
|
310
|
-
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))
|
|
327
|
+
border-radius: var(--tf-button-radius, 6px);
|
|
328
|
+
box-shadow: none;
|
|
329
|
+
background-color: transparent;
|
|
330
|
+
color: var(--tf-button-fg, rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)));
|
|
311
331
|
transition: background-color 0.15s, color 0.15s;
|
|
312
332
|
|
|
313
333
|
> .v-btn__overlay,
|
|
@@ -318,8 +338,8 @@ toolbarItems = toolbarItems
|
|
|
318
338
|
&:hover,
|
|
319
339
|
&:focus-visible,
|
|
320
340
|
&[aria-expanded='true'] {
|
|
321
|
-
background-color: rgba(var(--v-theme-on-surface), 0.07) !important;
|
|
322
|
-
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
|
341
|
+
background-color: var(--tf-button-hover-bg, rgba(var(--v-theme-on-surface), 0.07)) !important;
|
|
342
|
+
color: var(--tf-button-hover-fg, rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))) !important;
|
|
323
343
|
}
|
|
324
344
|
|
|
325
345
|
&.bg-primary,
|
|
@@ -328,14 +348,14 @@ toolbarItems = toolbarItems
|
|
|
328
348
|
&,
|
|
329
349
|
&:focus,
|
|
330
350
|
&:active {
|
|
331
|
-
background-color: rgba(var(--v-theme-on-surface), 0.1) !important;
|
|
332
|
-
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
|
351
|
+
background-color: var(--tf-button-active-bg, rgba(var(--v-theme-on-surface), 0.1)) !important;
|
|
352
|
+
color: var(--tf-button-hover-fg, rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))) !important;
|
|
333
353
|
}
|
|
334
354
|
|
|
335
355
|
&:hover,
|
|
336
356
|
&:focus-visible,
|
|
337
357
|
&[aria-expanded='true'] {
|
|
338
|
-
background-color: rgba(var(--v-theme-on-surface), 0.14) !important;
|
|
358
|
+
background-color: var(--tf-button-active-hover-bg, rgba(var(--v-theme-on-surface), 0.14)) !important;
|
|
339
359
|
}
|
|
340
360
|
}
|
|
341
361
|
|
|
@@ -357,45 +377,6 @@ toolbarItems = toolbarItems
|
|
|
357
377
|
}
|
|
358
378
|
}
|
|
359
379
|
|
|
360
|
-
/* Dropdowns opened from the toolbar: a light card with compact rows of icon + label.
|
|
361
|
-
|
|
362
|
-
/* Doubled classes: the admin theme pads menu lists and items with its own !important rules */
|
|
363
|
-
> .v-list.v-list {
|
|
364
|
-
padding: 4px !important;
|
|
365
|
-
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
366
|
-
border-radius: 8px;
|
|
367
|
-
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
368
|
-
|
|
369
|
-
> .v-list-item.v-list-item {
|
|
370
|
-
min-height: 34px;
|
|
371
|
-
margin: 0 !important;
|
|
372
|
-
padding: 4px 8px !important;
|
|
373
|
-
border-radius: 6px;
|
|
374
|
-
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
375
|
-
|
|
376
|
-
.v-list-item__prepend > .v-icon {
|
|
377
|
-
margin-inline-end: 0;
|
|
378
|
-
opacity: 0.6;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
.v-list-item__spacer {
|
|
382
|
-
width: 10px !important;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
.v-list-item-title {
|
|
386
|
-
font-size: 14px;
|
|
387
|
-
font-weight: 500;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
&.v-list-item--active {
|
|
391
|
-
background: rgba(var(--v-theme-on-surface), 0.06);
|
|
392
|
-
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
|
393
|
-
|
|
394
|
-
> .v-list-item__overlay {
|
|
395
|
-
opacity: 0;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
380
|
+
/* Dropdowns opened from the toolbar: a light card with compact rows of icon + label.
|
|
381
|
+
* The pastel menu skin rules moved to src/skins/menu.css (SkinScope wrapper, spec step 5). */
|
|
401
382
|
</style>
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } 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 Items from '@tiptapify/components/Toolbar/Items.vue'
|
|
6
6
|
import HeadingButton from '@tiptapify/extensions/components/style/heading/Button.vue'
|
|
7
7
|
import AlignmentMenuButton from '@tiptapify/extensions/components/alignment/menu/Button.vue'
|
|
8
8
|
import { headingLevels } from '@tiptapify/constants/style'
|
|
9
|
+
import { TIPTAPIFY_CONFIG_KEY } from '@tiptapify/composables/useTiptapifyConfig'
|
|
10
|
+
import { resolveSkin } from '@tiptapify/skins/registry'
|
|
11
|
+
import { variantBtnTypes } from '@tiptapify/types/editor'
|
|
9
12
|
|
|
10
13
|
const editor = {
|
|
11
14
|
instanceId: 1,
|
|
@@ -23,16 +26,31 @@ const items = [
|
|
|
23
26
|
},
|
|
24
27
|
]
|
|
25
28
|
|
|
26
|
-
async function renderToolbar (
|
|
29
|
+
async function renderToolbar (
|
|
30
|
+
toolbarItems: typeof items = items,
|
|
31
|
+
skin: unknown = 'default',
|
|
32
|
+
variantBtn: variantBtnTypes = 'flat',
|
|
33
|
+
) {
|
|
27
34
|
const warnings: string[] = []
|
|
28
35
|
const warn = vi.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => {
|
|
29
36
|
warnings.push(args.map(String).join(' '))
|
|
30
37
|
})
|
|
31
38
|
|
|
39
|
+
// The shape the section markup reads: exactly the chain a real editor provides
|
|
40
|
+
const resolved = resolveSkin(skin as never)
|
|
41
|
+
|
|
32
42
|
const app = createSSRApp({
|
|
33
43
|
setup () {
|
|
34
44
|
provide('tiptapifyEditor', ref(editor))
|
|
35
45
|
provide('tiptapifyI18n', { t: (key: string) => key })
|
|
46
|
+
provide(TIPTAPIFY_CONFIG_KEY, {
|
|
47
|
+
variantBtn: ref(variantBtn),
|
|
48
|
+
variantField: ref('outlined'),
|
|
49
|
+
skin: ref(skin),
|
|
50
|
+
skinName: computed(() => resolved.name),
|
|
51
|
+
tokens: computed(() => resolved.tokens),
|
|
52
|
+
shape: computed(() => resolved.shape),
|
|
53
|
+
})
|
|
36
54
|
|
|
37
55
|
return () => h(Items, { items: toolbarItems })
|
|
38
56
|
},
|
|
@@ -45,6 +63,13 @@ async function renderToolbar (toolbarItems: typeof items = items) {
|
|
|
45
63
|
return { html, warnings }
|
|
46
64
|
}
|
|
47
65
|
|
|
66
|
+
// The opening tag of the section's group element
|
|
67
|
+
function groupTag (html: string): string {
|
|
68
|
+
const match = html.match(/<div[^>]*class="[^"]*tiptapify-toolbar-group[^"]*"[^>]*>/)
|
|
69
|
+
|
|
70
|
+
return match ? match[0] : ''
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
describe('Toolbar Items', () => {
|
|
49
74
|
afterEach(() => {
|
|
50
75
|
headingLevels.value = [1, 2, 3, 4, 5, 6]
|
|
@@ -96,4 +121,35 @@ describe('Toolbar Items', () => {
|
|
|
96
121
|
expect(html).toContain('tiptapify-align-button-1')
|
|
97
122
|
expect(html).toContain('tiptapify-dropdown-button__chevron')
|
|
98
123
|
})
|
|
124
|
+
|
|
125
|
+
it('renders a grouped section as a VBtnGroup under groups: box', async () => {
|
|
126
|
+
const tag = groupTag(await renderToolbar(items, 'default').then((r) => r.html))
|
|
127
|
+
|
|
128
|
+
expect(tag).toContain('tiptapify-toolbar-group')
|
|
129
|
+
expect(tag).toContain('v-btn-group')
|
|
130
|
+
expect(tag).toContain('elevation-4')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('keeps the 0.3.2 variantBtn elevation class conditional', async () => {
|
|
134
|
+
const flat = groupTag(await renderToolbar(items, 'default', 'flat').then((r) => r.html))
|
|
135
|
+
expect(flat).toContain('v-btn-group')
|
|
136
|
+
expect(flat).not.toContain('tiptapify-btn-group--elevated')
|
|
137
|
+
|
|
138
|
+
const elevated = groupTag(await renderToolbar(items, 'default', 'elevated').then((r) => r.html))
|
|
139
|
+
expect(elevated).toContain('tiptapify-btn-group--elevated')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('renders a grouped section as a plain div under groups: flat', async () => {
|
|
143
|
+
const tag = groupTag(await renderToolbar(items, 'pastel').then((r) => r.html))
|
|
144
|
+
|
|
145
|
+
expect(tag).toContain('tiptapify-toolbar-group')
|
|
146
|
+
expect(tag).not.toContain('v-btn-group')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('an inline groups: box flag on a pastel-base skin gets the VBtnGroup markup', async () => {
|
|
150
|
+
const tag = groupTag(await renderToolbar(items, { extends: 'pastel', groups: 'box' }).then((r) => r.html))
|
|
151
|
+
|
|
152
|
+
expect(tag).toContain('tiptapify-toolbar-group')
|
|
153
|
+
expect(tag).toContain('v-btn-group')
|
|
154
|
+
})
|
|
99
155
|
})
|
|
@@ -4,10 +4,17 @@ import { PropType } from 'vue'
|
|
|
4
4
|
|
|
5
5
|
import { toolbarSections } from '@tiptapify/types/toolbarTypes'
|
|
6
6
|
|
|
7
|
+
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
8
|
+
|
|
7
9
|
defineProps({
|
|
8
10
|
items: { type: Array as PropType<toolbarSections>, default() { return {} } },
|
|
9
11
|
})
|
|
10
12
|
|
|
13
|
+
// The section markup follows the `groups` shape flag: 'box' renders the 0.3.2 VBtnGroup
|
|
14
|
+
// (joined buttons in a raised box, the tokens below paint it), 'flat' keeps the plain div.
|
|
15
|
+
// `variantBtn` carries the 0.3.2 elevation-override class onto the group.
|
|
16
|
+
const { shape, variantBtn } = useTiptapifyConfig()
|
|
17
|
+
|
|
11
18
|
</script>
|
|
12
19
|
|
|
13
20
|
<template>
|
|
@@ -20,7 +27,17 @@ defineProps({
|
|
|
20
27
|
<div class="tiptapify-toolbar-spacer" />
|
|
21
28
|
</template>
|
|
22
29
|
<template v-else>
|
|
23
|
-
<
|
|
30
|
+
<VBtnGroup
|
|
31
|
+
v-if="item.group && shape.groups === 'box'"
|
|
32
|
+
class="tiptapify-toolbar-group"
|
|
33
|
+
elevation="4"
|
|
34
|
+
:class="{ 'tiptapify-btn-group--elevated': variantBtn === 'elevated' }"
|
|
35
|
+
>
|
|
36
|
+
<template v-for="sectionItem in item.components" :key="sectionItem.name">
|
|
37
|
+
<component :is="sectionItem.component" v-bind="sectionItem.props ?? {}" />
|
|
38
|
+
</template>
|
|
39
|
+
</VBtnGroup>
|
|
40
|
+
<div v-else-if="item.group" class="tiptapify-toolbar-group">
|
|
24
41
|
<template v-for="sectionItem in item.components" :key="sectionItem.name">
|
|
25
42
|
<component :is="sectionItem.component" v-bind="sectionItem.props ?? {}" />
|
|
26
43
|
</template>
|
|
@@ -40,26 +57,44 @@ defineProps({
|
|
|
40
57
|
</template>
|
|
41
58
|
|
|
42
59
|
<style lang="scss" scoped>
|
|
60
|
+
/*
|
|
61
|
+
* Skin rules for the bar, the group boxes and the dividers (spec 2026-10-18: single rules
|
|
62
|
+
* reading `var(--tf-*)`). Base = the classic look, the fixture-canonical fallbacks; the
|
|
63
|
+
* shape gates (`toolbar: flat`, `groups: flat`) carry the same tokens with the shape's
|
|
64
|
+
* own base as fallback, so a shape flag without tokens paints the 0.3.3 look.
|
|
65
|
+
*/
|
|
43
66
|
.tiptapify-toolbar-items {
|
|
44
67
|
/* Vuetify sizes .v-toolbar-items to its content; grow it to the full toolbar width,
|
|
45
68
|
* otherwise the `~` spacer has no free space to expand into. */
|
|
46
69
|
flex-grow: 1;
|
|
47
70
|
flex-wrap: wrap;
|
|
48
71
|
align-items: center;
|
|
49
|
-
gap:
|
|
50
|
-
|
|
72
|
+
row-gap: var(--tf-items-row-gap, 5px);
|
|
73
|
+
/* Classic: no column gap (Vuetify's `normal`); the flat gate sets the token's own base */
|
|
74
|
+
column-gap: var(--tf-items-gap, normal);
|
|
75
|
+
padding: var(--tf-items-padding, 12px 16px);
|
|
51
76
|
}
|
|
52
77
|
|
|
53
78
|
.tiptapify-toolbar-group {
|
|
54
79
|
display: flex;
|
|
55
80
|
align-items: center;
|
|
56
|
-
|
|
81
|
+
|
|
82
|
+
/* The classic group: a raised box around the section's buttons, Vuetify elevation 2 (fixture-canonical) */
|
|
83
|
+
height: var(--tf-group-height, 32px);
|
|
84
|
+
border-radius: var(--tf-group-radius, 4px);
|
|
85
|
+
background: var(--tf-group-bg, rgb(var(--v-theme-surface)));
|
|
86
|
+
box-shadow:
|
|
87
|
+
var(--tf-group-shadow,
|
|
88
|
+
0 3px 1px -2px var(--v-shadow-key-umbra-opacity, rgba(0, 0, 0, 0.2)),
|
|
89
|
+
0 2px 2px 0 var(--v-shadow-key-penumbra-opacity, rgba(0, 0, 0, 0.14)),
|
|
90
|
+
0 1px 5px 0 var(--v-shadow-key-ambient-opacity, rgba(0, 0, 0, 0.12)));
|
|
57
91
|
}
|
|
58
92
|
|
|
59
93
|
.menu-divider {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
94
|
+
margin: 0 4px;
|
|
95
|
+
width: var(--tf-divider-width, 0px);
|
|
96
|
+
height: var(--tf-divider-height, 0px);
|
|
97
|
+
background: var(--tf-divider-color, transparent);
|
|
63
98
|
}
|
|
64
99
|
|
|
65
100
|
.menu-divider:nth-last-child(1),
|
|
@@ -70,4 +105,55 @@ defineProps({
|
|
|
70
105
|
.tiptapify-toolbar-spacer {
|
|
71
106
|
flex: 1 0 0;
|
|
72
107
|
}
|
|
108
|
+
|
|
109
|
+
/* Flat toolbar: compact padding and tighter gaps in the flat bar */
|
|
110
|
+
.tiptapify-shape-toolbar-flat {
|
|
111
|
+
.tiptapify-toolbar-items {
|
|
112
|
+
column-gap: var(--tf-items-gap, 6px);
|
|
113
|
+
row-gap: var(--tf-items-row-gap, 4px);
|
|
114
|
+
padding: var(--tf-items-padding, 6px 8px);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Box groups: the section is a VBtnGroup (0.3.2 structure restored). The base token rules
|
|
119
|
+
* above keep painting the box: the scoped `.tiptapify-toolbar-group` (class + scope
|
|
120
|
+
* attribute) out-specifies Vuetify's single-class `.v-btn-group` background, radius and
|
|
121
|
+
* zero-shadow rules, and `.elevation-4`. Two 0.3.2 pieces are what its own CSS fights:
|
|
122
|
+
* - `.v-btn-group--density-default.v-btn-group` pins 48px at equal specificity, decided by
|
|
123
|
+
* stylesheet order — 0.3.2 needed `:deep(.v-btn-group){ height: 32px !important }`, kept
|
|
124
|
+
* here as the tokenized `--tf-group-height` consumer (the 0.3.2 set demands the
|
|
125
|
+
* !important; the token contract stays honest, no new tokens);
|
|
126
|
+
* - `.menu-button { margin: 0 1px }` re-added for CSS-set parity with 0.3.2: inert there
|
|
127
|
+
* too (no toolbar element carries the class), and the buttons joining inside the box is
|
|
128
|
+
* native VBtnGroup (`border-radius: 0` + corner `inherit`), needing no rule of ours.
|
|
129
|
+
* The 0.3.2 `.tiptapify-btn-group--elevated` shadow override is NOT re-added: the token
|
|
130
|
+
* fallback is exactly that elevation-2 triple, and the scoped base rule beats elevation-4. */
|
|
131
|
+
.tiptapify-shape-groups-box {
|
|
132
|
+
.tiptapify-toolbar-group {
|
|
133
|
+
height: var(--tf-group-height, 32px) !important;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.menu-button {
|
|
137
|
+
margin: 0 1px;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Flat groups: sections become plain runs of buttons with hairline dividers between them;
|
|
142
|
+
* the box shape keeps the raised group defaults above untouched. */
|
|
143
|
+
.tiptapify-shape-groups-flat {
|
|
144
|
+
.tiptapify-toolbar-group {
|
|
145
|
+
gap: var(--tf-group-gap, 2px);
|
|
146
|
+
height: var(--tf-group-height, auto);
|
|
147
|
+
border-radius: var(--tf-group-radius, 0px);
|
|
148
|
+
background: var(--tf-group-bg, transparent);
|
|
149
|
+
box-shadow: var(--tf-group-shadow, none);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.menu-divider {
|
|
153
|
+
margin: 0;
|
|
154
|
+
width: var(--tf-divider-width, 1px);
|
|
155
|
+
height: var(--tf-divider-height, 20px);
|
|
156
|
+
background: var(--tf-divider-color, rgba(var(--v-border-color), var(--v-border-opacity)));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
73
159
|
</style>
|