@blockslides/vue-3-prebuilts 0.1.0

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/LICENSE.md ADDED
@@ -0,0 +1,36 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 BlockSlides
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ ## Acknowledgments
26
+
27
+ This project includes code and patterns adapted from [Tiptap](https://tiptap.dev),
28
+ an open-source headless editor framework for web artisans.
29
+
30
+ **Tiptap License:**
31
+
32
+ - Copyright (c) 2025, Tiptap GmbH
33
+ - Licensed under MIT License
34
+ - Original source: https://github.com/ueberdosis/tiptap
35
+
36
+ See `_tiptap/LICENSE.md` for the full Tiptap license.
@@ -0,0 +1,101 @@
1
+ import { defineComponent as T, ref as S, onMounted as E, watch as y, onBeforeUnmount as _, openBlock as A, createElementBlock as D, createCommentVNode as C } from "vue";
2
+ import { BubbleMenuPlugin as N } from "@blockslides/extension-bubble-menu";
3
+ import { NodeSelection as x } from "@blockslides/pm/state";
4
+ import { buildMenuElement as L, DEFAULT_ALIGNMENTS as b, DEFAULT_FONT_SIZES as v, DEFAULT_FONTS as B, DEFAULT_HIGHLIGHT_PALETTE as I, DEFAULT_COLOR_PALETTE as R, DEFAULT_ITEMS as z } from "@blockslides/extension-bubble-menu-preset";
5
+ import { isTextSelection as F } from "@blockslides/core";
6
+ const M = {
7
+ key: 0,
8
+ style: { display: "none" }
9
+ }, m = "bubbleMenuPreset", j = /* @__PURE__ */ T({
10
+ __name: "BubbleMenuPreset",
11
+ props: {
12
+ editor: {},
13
+ updateDelay: {},
14
+ resizeDelay: {},
15
+ appendTo: {},
16
+ shouldShow: {},
17
+ getReferencedVirtualElement: {},
18
+ options: {},
19
+ items: {},
20
+ className: {},
21
+ injectStyles: { type: Boolean, default: !0 },
22
+ textColors: {},
23
+ highlightColors: {},
24
+ fonts: {},
25
+ fontSizes: {},
26
+ alignments: {},
27
+ onTextAction: {},
28
+ onImageReplace: {}
29
+ },
30
+ setup(p) {
31
+ const e = p, n = S(null);
32
+ let o;
33
+ const s = () => {
34
+ const t = e.editor;
35
+ if (!t || t.isDestroyed)
36
+ return;
37
+ o && (o(), o = void 0);
38
+ const { element: i, cleanup: d } = L(t, {
39
+ items: e.items ?? z,
40
+ className: e.className ?? "",
41
+ injectStyles: e.injectStyles !== !1,
42
+ textColors: e.textColors ?? R,
43
+ highlightColors: e.highlightColors ?? I,
44
+ fonts: e.fonts ?? B,
45
+ fontSizes: e.fontSizes ?? v,
46
+ alignments: e.alignments ?? b,
47
+ onTextAction: e.onTextAction,
48
+ onImageReplace: e.onImageReplace
49
+ });
50
+ n.value = i, o = d;
51
+ const f = ({ state: h, editor: a }) => {
52
+ var u, c;
53
+ const l = h.selection, r = l instanceof x && ["image", "imageBlock"].includes((c = (u = l.node) == null ? void 0 : u.type) == null ? void 0 : c.name) || a.isActive("image") || a.isActive("imageBlock");
54
+ return !!(r || F(l) && !l.empty && !r);
55
+ }, g = N({
56
+ editor: t,
57
+ element: i,
58
+ updateDelay: e.updateDelay,
59
+ resizeDelay: e.resizeDelay,
60
+ appendTo: e.appendTo,
61
+ pluginKey: m,
62
+ shouldShow: e.shouldShow ?? f,
63
+ getReferencedVirtualElement: e.getReferencedVirtualElement,
64
+ options: e.options
65
+ });
66
+ t.registerPlugin(g);
67
+ };
68
+ return E(() => {
69
+ s();
70
+ }), y(
71
+ () => [
72
+ e.editor,
73
+ e.updateDelay,
74
+ e.resizeDelay,
75
+ e.appendTo,
76
+ e.shouldShow,
77
+ e.getReferencedVirtualElement,
78
+ e.options,
79
+ e.items,
80
+ e.className,
81
+ e.injectStyles,
82
+ e.textColors,
83
+ e.highlightColors,
84
+ e.fonts,
85
+ e.fontSizes,
86
+ e.alignments,
87
+ e.onTextAction,
88
+ e.onImageReplace
89
+ ],
90
+ () => {
91
+ s();
92
+ }
93
+ ), _(() => {
94
+ var t;
95
+ e.editor && e.editor.unregisterPlugin(m), o && o(), (t = n.value) != null && t.parentNode && n.value.parentNode.removeChild(n.value);
96
+ }), (t, i) => n.value ? (A(), D("div", M)) : C("", !0);
97
+ }
98
+ });
99
+ export {
100
+ j as _
101
+ };
@@ -0,0 +1 @@
1
+ "use strict";const n=require("vue"),E=require("@blockslides/extension-bubble-menu"),y=require("@blockslides/pm/state"),o=require("@blockslides/extension-bubble-menu-preset"),_=require("@blockslides/core"),A={key:0,style:{display:"none"}},p="bubbleMenuPreset",D=n.defineComponent({__name:"BubbleMenuPreset",props:{editor:{},updateDelay:{},resizeDelay:{},appendTo:{},shouldShow:{},getReferencedVirtualElement:{},options:{},items:{},className:{},injectStyles:{type:Boolean,default:!0},textColors:{},highlightColors:{},fonts:{},fontSizes:{},alignments:{},onTextAction:{},onImageReplace:{}},setup(g){const e=g,l=n.ref(null);let s;const r=()=>{const t=e.editor;if(!t||t.isDestroyed)return;s&&(s(),s=void 0);const{element:a,cleanup:f}=o.buildMenuElement(t,{items:e.items??o.DEFAULT_ITEMS,className:e.className??"",injectStyles:e.injectStyles!==!1,textColors:e.textColors??o.DEFAULT_COLOR_PALETTE,highlightColors:e.highlightColors??o.DEFAULT_HIGHLIGHT_PALETTE,fonts:e.fonts??o.DEFAULT_FONTS,fontSizes:e.fontSizes??o.DEFAULT_FONT_SIZES,alignments:e.alignments??o.DEFAULT_ALIGNMENTS,onTextAction:e.onTextAction,onImageReplace:e.onImageReplace});l.value=a,s=f;const h=({state:S,editor:u})=>{var d,m;const i=S.selection,c=i instanceof y.NodeSelection&&["image","imageBlock"].includes((m=(d=i.node)==null?void 0:d.type)==null?void 0:m.name)||u.isActive("image")||u.isActive("imageBlock");return!!(c||_.isTextSelection(i)&&!i.empty&&!c)},T=E.BubbleMenuPlugin({editor:t,element:a,updateDelay:e.updateDelay,resizeDelay:e.resizeDelay,appendTo:e.appendTo,pluginKey:p,shouldShow:e.shouldShow??h,getReferencedVirtualElement:e.getReferencedVirtualElement,options:e.options});t.registerPlugin(T)};return n.onMounted(()=>{r()}),n.watch(()=>[e.editor,e.updateDelay,e.resizeDelay,e.appendTo,e.shouldShow,e.getReferencedVirtualElement,e.options,e.items,e.className,e.injectStyles,e.textColors,e.highlightColors,e.fonts,e.fontSizes,e.alignments,e.onTextAction,e.onImageReplace],()=>{r()}),n.onBeforeUnmount(()=>{var t;e.editor&&e.editor.unregisterPlugin(p),s&&s(),(t=l.value)!=null&&t.parentNode&&l.value.parentNode.removeChild(l.value)}),(t,a)=>l.value?(n.openBlock(),n.createElementBlock("div",A)):n.createCommentVNode("",!0)}});exports._sfc_main=D;
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("vue"),c=require("@blockslides/vue-3"),v=require("./BubbleMenuPreset.vue_vue_type_script_setup_true_lang-Tkjxu33S.cjs"),S=require("@blockslides/ai-context"),O=require("@blockslides/extension-kit"),h=require("@blockslides/core"),C=()=>({type:"doc",content:[{type:"slide",attrs:{size:"16x9",className:"",id:"slide-1",backgroundMode:"none",backgroundColor:null,backgroundImage:null,backgroundOverlayColor:null,backgroundOverlayOpacity:null},content:[{type:"column",attrs:{align:"center",padding:"lg",margin:null,gap:"md",backgroundColor:"#ffffff",backgroundImage:null,borderRadius:null,border:null,fill:!0,width:null,height:null,justify:"center"},content:[{type:"heading",attrs:{align:null,padding:null,margin:null,gap:null,backgroundColor:null,backgroundImage:null,borderRadius:null,border:null,fill:null,width:null,height:null,justify:null,id:"1fc4664c-333d-4203-a3f1-3ad27a54c535","data-toc-id":"1fc4664c-333d-4203-a3f1-3ad27a54c535",level:1},content:[{type:"text",text:"Lorem ipsum dolor sit amet"}]},{type:"paragraph",attrs:{align:null,padding:null,margin:null,gap:null,backgroundColor:null,backgroundImage:null,borderRadius:null,border:null,fill:null,width:null,height:null,justify:null},content:[{type:"text",text:"Consectetur adipiscing elit. Sed do eiusmod tempor incididunt. "}]}]}]}]}),E=t=>({showPresets:!0,presets:t,presetBackground:"#0f172a",presetForeground:"#e5e7eb"}),j={renderMode:"dynamic",hoverOutline:{color:"#3b82f6",width:"1.5px",offset:"4px"},hoverOutlineCascade:!1},_=(t={})=>{const{content:r,onChange:s,extensions:a,extensionKitOptions:l,presetTemplates:p,onEditorReady:o,theme:d="light",editorProps:u,onUpdate:i,editorOptions:m={}}=t,g=e.computed(()=>p??S.templatesV1.listPresetTemplates()),P=e.computed(()=>{const n=(l==null?void 0:l.addSlideButton)===!1?!1:{...E(g.value),...(l==null?void 0:l.addSlideButton)??{}},b=(l==null?void 0:l.slide)===!1?!1:{...j,...(l==null?void 0:l.slide)??{}};return{...l,addSlideButton:n,slide:b}}),f=e.computed(()=>{const n=O.ExtensionKit.configure(P.value);return a?[n,...a]:[n]}),k=r??C();console.log("resolvedExtensions",f.value,f),console.log("editorOptions (as single prop):",m);const y=c.useEditor({content:k,extensions:f.value,theme:d,editorProps:{attributes:{autocomplete:"off",autocorrect:"off",autocapitalize:"off",class:"min-h-full min-w-full",...(u==null?void 0:u.attributes)??{}},...u},...m,onUpdate:n=>{const b=n.editor.getJSON();s==null||s(b,n.editor),i==null||i(n)}});return e.watch(y,n=>{n&&!n.isDestroyed?o==null||o(n):console.log("[useSlideEditor] ❌ Editor not ready or destroyed")},{immediate:!0}),{editor:y,presets:g}},B={class:"bs-viewport"},M=e.defineComponent({__name:"SlideEditor",props:{bubbleMenuPreset:{type:[Boolean,Object],default:!0},className:{},style:{},content:{},onChange:{},extensions:{},extensionKitOptions:{},presetTemplates:{},onEditorReady:{},theme:{},editorProps:{},onUpdate:{},editorOptions:{}},setup(t){const r=t,{bubbleMenuPreset:s,className:a,style:l,...p}=r,{editor:o}=_(p),d=e.computed(()=>r.bubbleMenuPreset===!1?null:r.bubbleMenuPreset===!0?{}:r.bubbleMenuPreset);return(u,i)=>e.unref(o)?(e.openBlock(),e.createElementBlock("div",{key:0,class:e.normalizeClass(e.unref(a)),style:e.normalizeStyle(e.unref(l))},[e.createElementVNode("div",B,[e.createVNode(e.unref(c.EditorContent),{editor:e.unref(o)},null,8,["editor"]),d.value?(e.openBlock(),e.createBlock(v._sfc_main,e.mergeProps({key:0,editor:e.unref(o)},d.value),null,16,["editor"])):e.createCommentVNode("",!0)])],6)):e.createCommentVNode("",!0)}});exports.BubbleMenuPreset=v._sfc_main;exports.SlideEditor=M;exports.useSlideEditor=_;Object.keys(c).forEach(t=>{t!=="default"&&!Object.prototype.hasOwnProperty.call(exports,t)&&Object.defineProperty(exports,t,{enumerable:!0,get:()=>c[t]})});Object.keys(h).forEach(t=>{t!=="default"&&!Object.prototype.hasOwnProperty.call(exports,t)&&Object.defineProperty(exports,t,{enumerable:!0,get:()=>h[t]})});
@@ -0,0 +1,5 @@
1
+ export { default as SlideEditor } from './SlideEditor.vue';
2
+ export * from './useSlideEditor.js';
3
+ export { default as BubbleMenuPreset } from './menus/BubbleMenuPreset.vue';
4
+ export * from '../../vue-3/src';
5
+ export * from '../../core/src';
package/dist/index.js ADDED
@@ -0,0 +1,211 @@
1
+ import { computed as c, watch as C, defineComponent as P, unref as n, openBlock as h, createElementBlock as E, normalizeStyle as _, normalizeClass as x, createElementVNode as B, createVNode as M, createBlock as N, mergeProps as w, createCommentVNode as k } from "vue";
2
+ import { useEditor as O, EditorContent as j } from "@blockslides/vue-3";
3
+ export * from "@blockslides/vue-3";
4
+ import { _ as z } from "./BubbleMenuPreset.vue_vue_type_script_setup_true_lang-B5Ihs2fW.js";
5
+ import { templatesV1 as I } from "@blockslides/ai-context";
6
+ import { ExtensionKit as V } from "@blockslides/extension-kit";
7
+ export * from "@blockslides/core";
8
+ const R = () => ({
9
+ /**
10
+ * Placeholder slide if no content is provided.
11
+ */
12
+ type: "doc",
13
+ content: [
14
+ {
15
+ type: "slide",
16
+ attrs: {
17
+ size: "16x9",
18
+ className: "",
19
+ id: "slide-1",
20
+ backgroundMode: "none",
21
+ backgroundColor: null,
22
+ backgroundImage: null,
23
+ backgroundOverlayColor: null,
24
+ backgroundOverlayOpacity: null
25
+ },
26
+ content: [
27
+ {
28
+ type: "column",
29
+ attrs: {
30
+ align: "center",
31
+ padding: "lg",
32
+ margin: null,
33
+ gap: "md",
34
+ backgroundColor: "#ffffff",
35
+ backgroundImage: null,
36
+ borderRadius: null,
37
+ border: null,
38
+ fill: !0,
39
+ width: null,
40
+ height: null,
41
+ justify: "center"
42
+ },
43
+ content: [
44
+ {
45
+ type: "heading",
46
+ attrs: {
47
+ align: null,
48
+ padding: null,
49
+ margin: null,
50
+ gap: null,
51
+ backgroundColor: null,
52
+ backgroundImage: null,
53
+ borderRadius: null,
54
+ border: null,
55
+ fill: null,
56
+ width: null,
57
+ height: null,
58
+ justify: null,
59
+ id: "1fc4664c-333d-4203-a3f1-3ad27a54c535",
60
+ "data-toc-id": "1fc4664c-333d-4203-a3f1-3ad27a54c535",
61
+ level: 1
62
+ },
63
+ content: [
64
+ {
65
+ type: "text",
66
+ text: "Lorem ipsum dolor sit amet"
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ type: "paragraph",
72
+ attrs: {
73
+ align: null,
74
+ padding: null,
75
+ margin: null,
76
+ gap: null,
77
+ backgroundColor: null,
78
+ backgroundImage: null,
79
+ borderRadius: null,
80
+ border: null,
81
+ fill: null,
82
+ width: null,
83
+ height: null,
84
+ justify: null
85
+ },
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: "Consectetur adipiscing elit. Sed do eiusmod tempor incididunt. "
90
+ }
91
+ ]
92
+ }
93
+ ]
94
+ }
95
+ ]
96
+ }
97
+ ]
98
+ }), T = (r) => ({
99
+ showPresets: !0,
100
+ presets: r,
101
+ presetBackground: "#0f172a",
102
+ presetForeground: "#e5e7eb"
103
+ }), A = {
104
+ renderMode: "dynamic",
105
+ hoverOutline: { color: "#3b82f6", width: "1.5px", offset: "4px" },
106
+ hoverOutlineCascade: !1
107
+ }, D = (r = {}) => {
108
+ const {
109
+ content: o,
110
+ onChange: u,
111
+ extensions: a,
112
+ extensionKitOptions: e,
113
+ presetTemplates: p,
114
+ onEditorReady: l,
115
+ theme: d = "light",
116
+ editorProps: s,
117
+ onUpdate: i,
118
+ editorOptions: g = {}
119
+ } = r, b = c(
120
+ () => p ?? I.listPresetTemplates()
121
+ ), v = c(() => {
122
+ const t = (e == null ? void 0 : e.addSlideButton) === !1 ? !1 : {
123
+ ...T(b.value),
124
+ ...(e == null ? void 0 : e.addSlideButton) ?? {}
125
+ }, f = (e == null ? void 0 : e.slide) === !1 ? !1 : {
126
+ ...A,
127
+ ...(e == null ? void 0 : e.slide) ?? {}
128
+ };
129
+ return {
130
+ ...e,
131
+ addSlideButton: t,
132
+ slide: f
133
+ };
134
+ }), m = c(() => {
135
+ const t = V.configure(v.value);
136
+ return a ? [t, ...a] : [t];
137
+ }), S = o ?? R();
138
+ console.log("resolvedExtensions", m.value, m), console.log("editorOptions (as single prop):", g);
139
+ const y = O(
140
+ {
141
+ content: S,
142
+ extensions: m.value,
143
+ theme: d,
144
+ editorProps: {
145
+ attributes: {
146
+ autocomplete: "off",
147
+ autocorrect: "off",
148
+ autocapitalize: "off",
149
+ class: "min-h-full min-w-full",
150
+ ...(s == null ? void 0 : s.attributes) ?? {}
151
+ },
152
+ ...s
153
+ },
154
+ ...g,
155
+ onUpdate: (t) => {
156
+ const f = t.editor.getJSON();
157
+ u == null || u(f, t.editor), i == null || i(t);
158
+ }
159
+ }
160
+ );
161
+ return C(
162
+ y,
163
+ (t) => {
164
+ t && !t.isDestroyed ? l == null || l(t) : console.log("[useSlideEditor] ❌ Editor not ready or destroyed");
165
+ },
166
+ { immediate: !0 }
167
+ ), { editor: y, presets: b };
168
+ }, F = { class: "bs-viewport" }, G = /* @__PURE__ */ P({
169
+ __name: "SlideEditor",
170
+ props: {
171
+ bubbleMenuPreset: { type: [Boolean, Object], default: !0 },
172
+ className: {},
173
+ style: {},
174
+ content: {},
175
+ onChange: {},
176
+ extensions: {},
177
+ extensionKitOptions: {},
178
+ presetTemplates: {},
179
+ onEditorReady: {},
180
+ theme: {},
181
+ editorProps: {},
182
+ onUpdate: {},
183
+ editorOptions: {}
184
+ },
185
+ setup(r) {
186
+ const o = r, {
187
+ bubbleMenuPreset: u,
188
+ className: a,
189
+ style: e,
190
+ ...p
191
+ } = o, { editor: l } = D(p), d = c(() => o.bubbleMenuPreset === !1 ? null : o.bubbleMenuPreset === !0 ? {} : o.bubbleMenuPreset);
192
+ return (s, i) => n(l) ? (h(), E("div", {
193
+ key: 0,
194
+ class: x(n(a)),
195
+ style: _(n(e))
196
+ }, [
197
+ B("div", F, [
198
+ M(n(j), { editor: n(l) }, null, 8, ["editor"]),
199
+ d.value ? (h(), N(z, w({
200
+ key: 0,
201
+ editor: n(l)
202
+ }, d.value), null, 16, ["editor"])) : k("", !0)
203
+ ])
204
+ ], 6)) : k("", !0);
205
+ }
206
+ });
207
+ export {
208
+ z as BubbleMenuPreset,
209
+ G as SlideEditor,
210
+ D as useSlideEditor
211
+ };
@@ -0,0 +1,6 @@
1
+ import { BubbleMenuPluginProps } from '../../../extension-bubble-menu/src';
2
+
3
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
4
+ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>;
5
+ export declare const BubbleMenu: any;
6
+ export {};
@@ -0,0 +1,6 @@
1
+ import { FloatingMenuPluginProps } from '../../../extension-floating-menu/src';
2
+
3
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
4
+ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element'>;
5
+ export declare const FloatingMenu: any;
6
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from './BubbleMenu.js';
2
+ export * from './FloatingMenu.js';
3
+ export { default as BubbleMenuPreset } from './BubbleMenuPreset.vue';
package/dist/menus.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("@blockslides/extension-bubble-menu"),t=require("vue"),f=require("@blockslides/extension-floating-menu"),g=require("./BubbleMenuPreset.vue_vue_type_script_setup_true_lang-Tkjxu33S.cjs"),v=t.defineComponent({name:"BubbleMenu",inheritAttrs:!1,props:{pluginKey:{type:[String,Object],default:"bubbleMenu"},editor:{type:Object,required:!0},updateDelay:{type:Number,default:void 0},resizeDelay:{type:Number,default:void 0},options:{type:Object,default:()=>({})},appendTo:{type:[Object,Function],default:void 0},shouldShow:{type:Function,default:null},getReferencedVirtualElement:{type:Function,default:void 0}},setup(o,{slots:u,attrs:d}){const l=t.ref(null);return t.onMounted(()=>{const{editor:e,options:n,pluginKey:a,resizeDelay:p,appendTo:s,shouldShow:i,getReferencedVirtualElement:c,updateDelay:b}=o,r=l.value;r&&(r.style.visibility="hidden",r.style.position="absolute",r.remove(),t.nextTick(()=>{e.registerPlugin(y.BubbleMenuPlugin({editor:e,element:r,options:n,pluginKey:a,resizeDelay:p,appendTo:s,shouldShow:i,getReferencedVirtualElement:c,updateDelay:b}))}))}),t.onBeforeUnmount(()=>{const{pluginKey:e,editor:n}=o;n.unregisterPlugin(e)}),()=>{var e;return t.h("div",{ref:l,...d},(e=u.default)==null?void 0:e.call(u))}}}),M=t.defineComponent({name:"FloatingMenu",inheritAttrs:!1,props:{pluginKey:{type:null,default:"floatingMenu"},editor:{type:Object,required:!0},options:{type:Object,default:()=>({})},appendTo:{type:[Object,Function],default:void 0},shouldShow:{type:Function,default:null}},setup(o,{slots:u,attrs:d}){const l=t.ref(null);return t.onMounted(()=>{const{pluginKey:e,editor:n,options:a,appendTo:p,shouldShow:s}=o,i=l.value;i&&(i.style.visibility="hidden",i.style.position="absolute",i.remove(),n.registerPlugin(f.FloatingMenuPlugin({pluginKey:e,editor:n,element:i,options:a,appendTo:p,shouldShow:s})))}),t.onBeforeUnmount(()=>{const{pluginKey:e,editor:n}=o;n.unregisterPlugin(e)}),()=>{var e;return t.h("div",{ref:l,...d},(e=u.default)==null?void 0:e.call(u))}}});exports.BubbleMenuPreset=g._sfc_main;exports.BubbleMenu=v;exports.FloatingMenu=M;
@@ -0,0 +1 @@
1
+ export * from './menus/index'
package/dist/menus.js ADDED
@@ -0,0 +1,132 @@
1
+ import { BubbleMenuPlugin as h } from "@blockslides/extension-bubble-menu";
2
+ import { defineComponent as f, ref as y, onMounted as s, nextTick as v, onBeforeUnmount as c, h as b } from "vue";
3
+ import { FloatingMenuPlugin as M } from "@blockslides/extension-floating-menu";
4
+ import { _ as T } from "./BubbleMenuPreset.vue_vue_type_script_setup_true_lang-B5Ihs2fW.js";
5
+ const P = f({
6
+ name: "BubbleMenu",
7
+ inheritAttrs: !1,
8
+ props: {
9
+ pluginKey: {
10
+ type: [String, Object],
11
+ default: "bubbleMenu"
12
+ },
13
+ editor: {
14
+ type: Object,
15
+ required: !0
16
+ },
17
+ updateDelay: {
18
+ type: Number,
19
+ default: void 0
20
+ },
21
+ resizeDelay: {
22
+ type: Number,
23
+ default: void 0
24
+ },
25
+ options: {
26
+ type: Object,
27
+ default: () => ({})
28
+ },
29
+ appendTo: {
30
+ type: [Object, Function],
31
+ default: void 0
32
+ },
33
+ shouldShow: {
34
+ type: Function,
35
+ default: null
36
+ },
37
+ getReferencedVirtualElement: {
38
+ type: Function,
39
+ default: void 0
40
+ }
41
+ },
42
+ setup(i, { slots: n, attrs: r }) {
43
+ const o = y(null);
44
+ return s(() => {
45
+ const {
46
+ editor: e,
47
+ options: t,
48
+ pluginKey: d,
49
+ resizeDelay: p,
50
+ appendTo: a,
51
+ shouldShow: u,
52
+ getReferencedVirtualElement: g,
53
+ updateDelay: m
54
+ } = i, l = o.value;
55
+ l && (l.style.visibility = "hidden", l.style.position = "absolute", l.remove(), v(() => {
56
+ e.registerPlugin(
57
+ h({
58
+ editor: e,
59
+ element: l,
60
+ options: t,
61
+ pluginKey: d,
62
+ resizeDelay: p,
63
+ appendTo: a,
64
+ shouldShow: u,
65
+ getReferencedVirtualElement: g,
66
+ updateDelay: m
67
+ })
68
+ );
69
+ }));
70
+ }), c(() => {
71
+ const { pluginKey: e, editor: t } = i;
72
+ t.unregisterPlugin(e);
73
+ }), () => {
74
+ var e;
75
+ return b("div", { ref: o, ...r }, (e = n.default) == null ? void 0 : e.call(n));
76
+ };
77
+ }
78
+ }), K = f({
79
+ name: "FloatingMenu",
80
+ inheritAttrs: !1,
81
+ props: {
82
+ pluginKey: {
83
+ // TODO: TypeScript breaks
84
+ // type: [String, Object as PropType<Exclude<FloatingMenuPluginProps['pluginKey'], string>>],
85
+ type: null,
86
+ default: "floatingMenu"
87
+ },
88
+ editor: {
89
+ type: Object,
90
+ required: !0
91
+ },
92
+ options: {
93
+ type: Object,
94
+ default: () => ({})
95
+ },
96
+ appendTo: {
97
+ type: [Object, Function],
98
+ default: void 0
99
+ },
100
+ shouldShow: {
101
+ type: Function,
102
+ default: null
103
+ }
104
+ },
105
+ setup(i, { slots: n, attrs: r }) {
106
+ const o = y(null);
107
+ return s(() => {
108
+ const { pluginKey: e, editor: t, options: d, appendTo: p, shouldShow: a } = i, u = o.value;
109
+ u && (u.style.visibility = "hidden", u.style.position = "absolute", u.remove(), t.registerPlugin(
110
+ M({
111
+ pluginKey: e,
112
+ editor: t,
113
+ element: u,
114
+ options: d,
115
+ appendTo: p,
116
+ shouldShow: a
117
+ })
118
+ ));
119
+ }), c(() => {
120
+ const { pluginKey: e, editor: t } = i;
121
+ t.unregisterPlugin(e);
122
+ }), () => {
123
+ var e;
124
+ return b("div", { ref: o, ...r }, (e = n.default) == null ? void 0 : e.call(n));
125
+ };
126
+ }
127
+ });
128
+ export {
129
+ P as BubbleMenu,
130
+ T as BubbleMenuPreset,
131
+ K as FloatingMenu
132
+ };
@@ -0,0 +1,53 @@
1
+ import { templatesV1 } from '../../ai-context/src';
2
+ import { AnyExtension, Editor, JSONContent, EditorOptions } from '../../core/src';
3
+ import { ExtensionKitOptions } from '../../extension-kit/src';
4
+
5
+ type PresetTemplates = ReturnType<typeof templatesV1.listPresetTemplates>;
6
+ export interface UseSlideEditorProps {
7
+ /**
8
+ * Initial content for the editor. If omitted, a single preset slide is used.
9
+ */
10
+ content?: EditorOptions['content'];
11
+ /**
12
+ * Called on every update with the current JSON document.
13
+ */
14
+ onChange?: (doc: JSONContent, editor: Editor) => void;
15
+ /**
16
+ * Additional extensions to append after the ExtensionKit bundle.
17
+ */
18
+ extensions?: AnyExtension[];
19
+ /**
20
+ * Customize or disable pieces of ExtensionKit (e.g., bubbleMenu: false).
21
+ */
22
+ extensionKitOptions?: ExtensionKitOptions;
23
+ /**
24
+ * Optional preset list to power the add-slide button.
25
+ */
26
+ presetTemplates?: PresetTemplates;
27
+ /**
28
+ * Called once when an editor instance is ready.
29
+ */
30
+ onEditorReady?: (editor: Editor) => void;
31
+ /**
32
+ * Editor theme for UI styling
33
+ */
34
+ theme?: EditorOptions['theme'];
35
+ /**
36
+ * The editor's props
37
+ */
38
+ editorProps?: EditorOptions['editorProps'];
39
+ /**
40
+ * Called on every update
41
+ */
42
+ onUpdate?: EditorOptions['onUpdate'];
43
+ /**
44
+ * Additional editor options to pass through to the core editor.
45
+ * This allows passing any EditorOptions without Vue auto-initializing them.
46
+ */
47
+ editorOptions?: Partial<EditorOptions>;
48
+ }
49
+ export declare const useSlideEditor: (props?: UseSlideEditorProps) => {
50
+ editor: import('vue').ShallowRef<import('../../vue-3/src').Editor | undefined, import('../../vue-3/src').Editor | undefined>;
51
+ presets: import('vue').ComputedRef<templatesV1.PresetTemplate[]>;
52
+ };
53
+ export {};
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@blockslides/vue-3-prebuilts",
3
+ "description": "Pre-built Vue 3 components for blockslides - includes SlideEditor, useSlideEditor, and menu components",
4
+ "version": "0.1.0",
5
+ "homepage": "https://github.com/keivanmojmali/blockslides",
6
+ "keywords": [
7
+ "blockslides",
8
+ "blockslides vue components",
9
+ "editor",
10
+ "wysiwyg",
11
+ "slide editor",
12
+ "prebuilt components"
13
+ ],
14
+ "license": "MIT",
15
+ "exports": {
16
+ ".": {
17
+ "types": {
18
+ "import": "./dist/index.d.ts",
19
+ "require": "./dist/index.d.cts"
20
+ },
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ },
24
+ "./menus": {
25
+ "types": {
26
+ "import": "./dist/menus/index.d.ts",
27
+ "require": "./dist/menus/index.d.cts"
28
+ },
29
+ "import": "./dist/menus/index.js",
30
+ "require": "./dist/menus/index.cjs"
31
+ }
32
+ },
33
+ "main": "dist/index.cjs",
34
+ "module": "dist/index.js",
35
+ "types": "dist/index.d.ts",
36
+ "type": "module",
37
+ "files": [
38
+ "src",
39
+ "dist"
40
+ ],
41
+ "dependencies": {
42
+ "@blockslides/vue-3": "^0.4.1",
43
+ "@blockslides/extension-kit": "^0.7.3",
44
+ "@blockslides/ai-context": "^0.3.1",
45
+ "@blockslides/extension-bubble-menu-preset": "^0.3.1",
46
+ "@blockslides/extension-floating-menu": "^0.1.1",
47
+ "@blockslides/extension-bubble-menu": "^0.1.1"
48
+ },
49
+ "devDependencies": {
50
+ "@floating-ui/dom": "^1.0.0",
51
+ "@vitejs/plugin-vue": "^5.0.0",
52
+ "vite": "^5.0.0",
53
+ "vite-plugin-dts": "^3.0.0",
54
+ "vue": "^3.5.13",
55
+ "@blockslides/core": "^0.3.3",
56
+ "@blockslides/pm": "^0.1.1"
57
+ },
58
+ "peerDependencies": {
59
+ "@floating-ui/dom": "^1.0.0",
60
+ "vue": "^3.0.0",
61
+ "@blockslides/core": "^0.3.3",
62
+ "@blockslides/pm": "^0.1.1"
63
+ },
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "https://github.com/keivanmojmali/blockslides",
67
+ "directory": "packages/vue-3-prebuilts"
68
+ },
69
+ "sideEffects": false,
70
+ "author": "keivanmojmali",
71
+ "scripts": {
72
+ "build": "vite build",
73
+ "lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
74
+ }
75
+ }
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import { computed, watch, onMounted } from 'vue'
3
+ import { EditorContent } from '@blockslides/vue-3'
4
+ import BubbleMenuPreset from './menus/BubbleMenuPreset.vue'
5
+ import { useSlideEditor, type UseSlideEditorProps } from './useSlideEditor'
6
+ import type { Editor } from '@blockslides/core'
7
+
8
+ export interface BubbleMenuPresetProps {
9
+ editor: Editor
10
+ [key: string]: any
11
+ }
12
+
13
+ export interface SlideEditorProps extends UseSlideEditorProps {
14
+ /**
15
+ * Toggle or customize the built-in BubbleMenuPreset.
16
+ * - true (default): render with defaults
17
+ * - false: disable entirely
18
+ * - object: pass through to BubbleMenuPreset
19
+ */
20
+ bubbleMenuPreset?: boolean | BubbleMenuPresetProps
21
+ className?: string
22
+ style?: any
23
+ }
24
+
25
+ const props = withDefaults(defineProps<SlideEditorProps>(), {
26
+ bubbleMenuPreset: true,
27
+ })
28
+
29
+ const {
30
+ bubbleMenuPreset,
31
+ className,
32
+ style,
33
+ ...hookProps
34
+ } = props
35
+
36
+ const { editor } = useSlideEditor(hookProps)
37
+
38
+ const bubbleMenuProps = computed(() => {
39
+ if (props.bubbleMenuPreset === false) return null
40
+ if (props.bubbleMenuPreset === true) return {}
41
+ return props.bubbleMenuPreset
42
+ })
43
+
44
+ </script>
45
+
46
+ <template>
47
+ <div v-if="editor" :class="className" :style="style">
48
+ <div class="bs-viewport">
49
+ <EditorContent :editor="editor" />
50
+ <BubbleMenuPreset v-if="bubbleMenuProps" :editor="editor" v-bind="bubbleMenuProps" />
51
+ </div>
52
+ </div>
53
+ </template>
54
+
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { default as SlideEditor } from './SlideEditor.vue'
2
+ export * from './useSlideEditor.js'
3
+ export { default as BubbleMenuPreset } from './menus/BubbleMenuPreset.vue'
4
+
5
+ // Re-export everything from @blockslides/vue-3 core
6
+ export * from '@blockslides/vue-3'
7
+
8
+ // Re-export everything from @blockslides/core for convenience
9
+ export * from '@blockslides/core'
@@ -0,0 +1,111 @@
1
+ import type { BubbleMenuPluginProps } from '@blockslides/extension-bubble-menu'
2
+ import { BubbleMenuPlugin } from '@blockslides/extension-bubble-menu'
3
+ import type { PropType } from 'vue'
4
+ import { defineComponent, h, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
5
+
6
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
7
+
8
+ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>
9
+
10
+ export const BubbleMenu = defineComponent({
11
+ name: 'BubbleMenu',
12
+
13
+ inheritAttrs: false,
14
+
15
+ props: {
16
+ pluginKey: {
17
+ type: [String, Object] as PropType<BubbleMenuPluginProps['pluginKey']>,
18
+ default: 'bubbleMenu',
19
+ },
20
+
21
+ editor: {
22
+ type: Object as PropType<BubbleMenuPluginProps['editor']>,
23
+ required: true,
24
+ },
25
+
26
+ updateDelay: {
27
+ type: Number as PropType<BubbleMenuPluginProps['updateDelay']>,
28
+ default: undefined,
29
+ },
30
+
31
+ resizeDelay: {
32
+ type: Number as PropType<BubbleMenuPluginProps['resizeDelay']>,
33
+ default: undefined,
34
+ },
35
+
36
+ options: {
37
+ type: Object as PropType<BubbleMenuPluginProps['options']>,
38
+ default: () => ({}),
39
+ },
40
+
41
+ appendTo: {
42
+ type: [Object, Function] as PropType<BubbleMenuPluginProps['appendTo']>,
43
+ default: undefined,
44
+ },
45
+
46
+ shouldShow: {
47
+ type: Function as PropType<Exclude<Required<BubbleMenuPluginProps>['shouldShow'], null>>,
48
+ default: null,
49
+ },
50
+
51
+ getReferencedVirtualElement: {
52
+ type: Function as PropType<Exclude<Required<BubbleMenuPluginProps>['getReferencedVirtualElement'], null>>,
53
+ default: undefined,
54
+ },
55
+ },
56
+
57
+ setup(props, { slots, attrs }) {
58
+ const root = ref<HTMLElement | null>(null)
59
+
60
+ onMounted(() => {
61
+ const {
62
+ editor,
63
+ options,
64
+ pluginKey,
65
+ resizeDelay,
66
+ appendTo,
67
+ shouldShow,
68
+ getReferencedVirtualElement,
69
+ updateDelay,
70
+ } = props
71
+
72
+ const el = root.value
73
+
74
+ if (!el) {
75
+ return
76
+ }
77
+
78
+ el.style.visibility = 'hidden'
79
+ el.style.position = 'absolute'
80
+
81
+ // Remove element from DOM; plugin will re-parent it when shown
82
+ el.remove()
83
+
84
+ nextTick(() => {
85
+ editor.registerPlugin(
86
+ BubbleMenuPlugin({
87
+ editor,
88
+ element: el,
89
+ options,
90
+ pluginKey,
91
+ resizeDelay,
92
+ appendTo,
93
+ shouldShow,
94
+ getReferencedVirtualElement,
95
+ updateDelay,
96
+ }),
97
+ )
98
+ })
99
+ })
100
+
101
+ onBeforeUnmount(() => {
102
+ const { pluginKey, editor } = props
103
+
104
+ editor.unregisterPlugin(pluginKey)
105
+ })
106
+
107
+ // Vue owns this element; attrs are applied reactively by Vue
108
+ // Plugin re-parents it when showing the menu
109
+ return () => h('div', { ref: root, ...attrs }, slots.default?.())
110
+ },
111
+ }) as any
@@ -0,0 +1,137 @@
1
+ <script setup lang="ts">
2
+ import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
+ import { BubbleMenuPlugin, type BubbleMenuPluginProps } from '@blockslides/extension-bubble-menu'
4
+ import { NodeSelection } from '@blockslides/pm/state'
5
+ import {
6
+ type BubbleMenuPresetOptions,
7
+ buildMenuElement,
8
+ DEFAULT_ITEMS,
9
+ DEFAULT_COLOR_PALETTE,
10
+ DEFAULT_HIGHLIGHT_PALETTE,
11
+ DEFAULT_FONTS,
12
+ DEFAULT_FONT_SIZES,
13
+ DEFAULT_ALIGNMENTS,
14
+ } from '@blockslides/extension-bubble-menu-preset'
15
+ import { isTextSelection, type Editor } from '@blockslides/core'
16
+
17
+ export interface BubbleMenuPresetProps extends Omit<BubbleMenuPresetOptions, 'element' | 'pluginKey'> {
18
+ editor: Editor
19
+ updateDelay?: number
20
+ resizeDelay?: number
21
+ appendTo?: HTMLElement | (() => HTMLElement)
22
+ shouldShow?: BubbleMenuPluginProps['shouldShow']
23
+ getReferencedVirtualElement?: () => any
24
+ options?: any
25
+ }
26
+
27
+ const props = withDefaults(defineProps<BubbleMenuPresetProps>(), {
28
+ injectStyles: true,
29
+ })
30
+
31
+ const pluginKey = 'bubbleMenuPreset'
32
+ const menuEl = ref<HTMLElement | null>(null)
33
+ let cleanup: (() => void) | undefined
34
+
35
+ const setupBubbleMenu = () => {
36
+ const attachToEditor = props.editor
37
+
38
+ if (!attachToEditor || (attachToEditor as any).isDestroyed) {
39
+ return
40
+ }
41
+
42
+ // Cleanup previous instance if any
43
+ if (cleanup) {
44
+ cleanup()
45
+ cleanup = undefined
46
+ }
47
+
48
+ const { element, cleanup: elementCleanup } = buildMenuElement(attachToEditor, {
49
+ items: props.items ?? DEFAULT_ITEMS,
50
+ className: props.className ?? '',
51
+ injectStyles: props.injectStyles !== false,
52
+ textColors: props.textColors ?? DEFAULT_COLOR_PALETTE,
53
+ highlightColors: props.highlightColors ?? DEFAULT_HIGHLIGHT_PALETTE,
54
+ fonts: props.fonts ?? DEFAULT_FONTS,
55
+ fontSizes: props.fontSizes ?? DEFAULT_FONT_SIZES,
56
+ alignments: props.alignments ?? DEFAULT_ALIGNMENTS,
57
+ onTextAction: props.onTextAction,
58
+ onImageReplace: props.onImageReplace,
59
+ })
60
+
61
+ menuEl.value = element
62
+ cleanup = elementCleanup
63
+
64
+ const defaultShouldShow: Exclude<BubbleMenuPluginProps['shouldShow'], null> = ({ state, editor }) => {
65
+ const sel = state.selection
66
+ const imageSelection =
67
+ (sel instanceof NodeSelection && ['image', 'imageBlock'].includes((sel as any).node?.type?.name)) ||
68
+ editor.isActive('image') ||
69
+ editor.isActive('imageBlock')
70
+
71
+ if (imageSelection) return true
72
+ if (isTextSelection(sel) && !sel.empty && !imageSelection) return true
73
+ return false
74
+ }
75
+
76
+ const plugin = BubbleMenuPlugin({
77
+ editor: attachToEditor,
78
+ element,
79
+ updateDelay: props.updateDelay,
80
+ resizeDelay: props.resizeDelay,
81
+ appendTo: props.appendTo,
82
+ pluginKey,
83
+ shouldShow: props.shouldShow ?? defaultShouldShow,
84
+ getReferencedVirtualElement: props.getReferencedVirtualElement,
85
+ options: props.options,
86
+ })
87
+
88
+ attachToEditor.registerPlugin(plugin)
89
+ }
90
+
91
+ onMounted(() => {
92
+ setupBubbleMenu()
93
+ })
94
+
95
+ // Watch for prop changes and rebuild
96
+ watch(
97
+ () => [
98
+ props.editor,
99
+ props.updateDelay,
100
+ props.resizeDelay,
101
+ props.appendTo,
102
+ props.shouldShow,
103
+ props.getReferencedVirtualElement,
104
+ props.options,
105
+ props.items,
106
+ props.className,
107
+ props.injectStyles,
108
+ props.textColors,
109
+ props.highlightColors,
110
+ props.fonts,
111
+ props.fontSizes,
112
+ props.alignments,
113
+ props.onTextAction,
114
+ props.onImageReplace,
115
+ ],
116
+ () => {
117
+ setupBubbleMenu()
118
+ }
119
+ )
120
+
121
+ onBeforeUnmount(() => {
122
+ if (props.editor) {
123
+ props.editor.unregisterPlugin(pluginKey)
124
+ }
125
+ if (cleanup) {
126
+ cleanup()
127
+ }
128
+ if (menuEl.value?.parentNode) {
129
+ menuEl.value.parentNode.removeChild(menuEl.value)
130
+ }
131
+ })
132
+ </script>
133
+
134
+ <template>
135
+ <!-- Vue doesn't need portal here as buildMenuElement handles DOM placement -->
136
+ <div v-if="menuEl" style="display: none"></div>
137
+ </template>
@@ -0,0 +1,84 @@
1
+ import type { FloatingMenuPluginProps } from '@blockslides/extension-floating-menu'
2
+ import { FloatingMenuPlugin } from '@blockslides/extension-floating-menu'
3
+ import type { PropType } from 'vue'
4
+ import { defineComponent, h, onBeforeUnmount, onMounted, ref } from 'vue'
5
+
6
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
7
+
8
+ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element'>
9
+
10
+ export const FloatingMenu = defineComponent({
11
+ name: 'FloatingMenu',
12
+
13
+ inheritAttrs: false,
14
+
15
+ props: {
16
+ pluginKey: {
17
+ // TODO: TypeScript breaks
18
+ // type: [String, Object as PropType<Exclude<FloatingMenuPluginProps['pluginKey'], string>>],
19
+ type: null,
20
+ default: 'floatingMenu',
21
+ },
22
+
23
+ editor: {
24
+ type: Object as PropType<FloatingMenuPluginProps['editor']>,
25
+ required: true,
26
+ },
27
+
28
+ options: {
29
+ type: Object as PropType<FloatingMenuPluginProps['options']>,
30
+ default: () => ({}),
31
+ },
32
+
33
+ appendTo: {
34
+ type: [Object, Function] as PropType<FloatingMenuPluginProps['appendTo']>,
35
+ default: undefined,
36
+ },
37
+
38
+ shouldShow: {
39
+ type: Function as PropType<Exclude<Required<FloatingMenuPluginProps>['shouldShow'], null>>,
40
+ default: null,
41
+ },
42
+ },
43
+
44
+ setup(props, { slots, attrs }) {
45
+ const root = ref<HTMLElement | null>(null)
46
+
47
+ onMounted(() => {
48
+ const { pluginKey, editor, options, appendTo, shouldShow } = props
49
+
50
+ const el = root.value
51
+
52
+ if (!el) {
53
+ return
54
+ }
55
+
56
+ el.style.visibility = 'hidden'
57
+ el.style.position = 'absolute'
58
+
59
+ // Remove element from DOM; plugin will re-parent it when shown
60
+ el.remove()
61
+
62
+ editor.registerPlugin(
63
+ FloatingMenuPlugin({
64
+ pluginKey,
65
+ editor,
66
+ element: el,
67
+ options,
68
+ appendTo,
69
+ shouldShow,
70
+ }),
71
+ )
72
+ })
73
+
74
+ onBeforeUnmount(() => {
75
+ const { pluginKey, editor } = props
76
+
77
+ editor.unregisterPlugin(pluginKey)
78
+ })
79
+
80
+ // Vue owns this element; attrs are applied reactively by Vue
81
+ // Plugin re-parents it when showing the menu
82
+ return () => h('div', { ref: root, ...attrs }, slots.default?.())
83
+ },
84
+ }) as any
@@ -0,0 +1,3 @@
1
+ export * from './BubbleMenu.js'
2
+ export * from './FloatingMenu.js'
3
+ export { default as BubbleMenuPreset } from './BubbleMenuPreset.vue'
@@ -0,0 +1,255 @@
1
+ import { computed, watch, toRefs } from 'vue'
2
+ import { templatesV1 } from '@blockslides/ai-context'
3
+ import type { AnyExtension, Editor, JSONContent } from '@blockslides/core'
4
+ import type { SlideOptions } from '@blockslides/extension-slide'
5
+ import { ExtensionKit, type ExtensionKitOptions } from '@blockslides/extension-kit'
6
+
7
+ import { useEditor } from '@blockslides/vue-3'
8
+ import type { EditorOptions } from '@blockslides/core'
9
+
10
+ type PresetTemplates = ReturnType<typeof templatesV1.listPresetTemplates>
11
+
12
+ export interface UseSlideEditorProps {
13
+ /**
14
+ * Initial content for the editor. If omitted, a single preset slide is used.
15
+ */
16
+ content?: EditorOptions['content']
17
+ /**
18
+ * Called on every update with the current JSON document.
19
+ */
20
+ onChange?: (doc: JSONContent, editor: Editor) => void
21
+ /**
22
+ * Additional extensions to append after the ExtensionKit bundle.
23
+ */
24
+ extensions?: AnyExtension[]
25
+ /**
26
+ * Customize or disable pieces of ExtensionKit (e.g., bubbleMenu: false).
27
+ */
28
+ extensionKitOptions?: ExtensionKitOptions
29
+ /**
30
+ * Optional preset list to power the add-slide button.
31
+ */
32
+ presetTemplates?: PresetTemplates
33
+ /**
34
+ * Called once when an editor instance is ready.
35
+ */
36
+ onEditorReady?: (editor: Editor) => void
37
+ /**
38
+ * Editor theme for UI styling
39
+ */
40
+ theme?: EditorOptions['theme']
41
+ /**
42
+ * The editor's props
43
+ */
44
+ editorProps?: EditorOptions['editorProps']
45
+ /**
46
+ * Called on every update
47
+ */
48
+ onUpdate?: EditorOptions['onUpdate']
49
+ /**
50
+ * Additional editor options to pass through to the core editor.
51
+ * This allows passing any EditorOptions without Vue auto-initializing them.
52
+ */
53
+ editorOptions?: Partial<EditorOptions>
54
+ }
55
+
56
+ const defaultSlide = () => ({
57
+ /**
58
+ * Placeholder slide if no content is provided.
59
+ */
60
+ type: 'doc',
61
+ content: [
62
+ {
63
+ type: 'slide',
64
+ attrs: {
65
+ size: '16x9',
66
+ className: '',
67
+ id: 'slide-1',
68
+ backgroundMode: 'none',
69
+ backgroundColor: null,
70
+ backgroundImage: null,
71
+ backgroundOverlayColor: null,
72
+ backgroundOverlayOpacity: null,
73
+ },
74
+ content: [
75
+ {
76
+ type: 'column',
77
+ attrs: {
78
+ align: 'center',
79
+ padding: 'lg',
80
+ margin: null,
81
+ gap: 'md',
82
+ backgroundColor: '#ffffff',
83
+ backgroundImage: null,
84
+ borderRadius: null,
85
+ border: null,
86
+ fill: true,
87
+ width: null,
88
+ height: null,
89
+ justify: 'center',
90
+ },
91
+ content: [
92
+ {
93
+ type: 'heading',
94
+ attrs: {
95
+ align: null,
96
+ padding: null,
97
+ margin: null,
98
+ gap: null,
99
+ backgroundColor: null,
100
+ backgroundImage: null,
101
+ borderRadius: null,
102
+ border: null,
103
+ fill: null,
104
+ width: null,
105
+ height: null,
106
+ justify: null,
107
+ id: '1fc4664c-333d-4203-a3f1-3ad27a54c535',
108
+ 'data-toc-id': '1fc4664c-333d-4203-a3f1-3ad27a54c535',
109
+ level: 1,
110
+ },
111
+ content: [
112
+ {
113
+ type: 'text',
114
+ text: 'Lorem ipsum dolor sit amet',
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ type: 'paragraph',
120
+ attrs: {
121
+ align: null,
122
+ padding: null,
123
+ margin: null,
124
+ gap: null,
125
+ backgroundColor: null,
126
+ backgroundImage: null,
127
+ borderRadius: null,
128
+ border: null,
129
+ fill: null,
130
+ width: null,
131
+ height: null,
132
+ justify: null,
133
+ },
134
+ content: [
135
+ {
136
+ type: 'text',
137
+ text: 'Consectetur adipiscing elit. Sed do eiusmod tempor incididunt. ',
138
+ },
139
+ ],
140
+ },
141
+ ],
142
+ },
143
+ ],
144
+ },
145
+ ],
146
+ })
147
+
148
+ const defaultAddSlideButton = (presets: PresetTemplates) => ({
149
+ showPresets: true,
150
+ presets,
151
+ presetBackground: '#0f172a',
152
+ presetForeground: '#e5e7eb',
153
+ })
154
+
155
+ const defaultSlideOptions: Partial<SlideOptions> = {
156
+ renderMode: 'dynamic',
157
+ hoverOutline: { color: '#3b82f6', width: '1.5px', offset: '4px' },
158
+ hoverOutlineCascade: false,
159
+ }
160
+
161
+ export const useSlideEditor = (props: UseSlideEditorProps = {}) => {
162
+ const {
163
+ content,
164
+ onChange,
165
+ extensions,
166
+ extensionKitOptions,
167
+ presetTemplates,
168
+ onEditorReady,
169
+ theme = 'light',
170
+ editorProps,
171
+ onUpdate,
172
+ editorOptions = {}
173
+ } = props
174
+ /**
175
+ * Presets for add slide button.
176
+ */
177
+ const presets = computed<PresetTemplates>(
178
+ () => presetTemplates ?? templatesV1.listPresetTemplates()
179
+ )
180
+
181
+ const mergedExtensionKitOptions = computed<ExtensionKitOptions>(() => {
182
+ const addSlideButton =
183
+ extensionKitOptions?.addSlideButton === false
184
+ ? false
185
+ : {
186
+ ...defaultAddSlideButton(presets.value),
187
+ ...(extensionKitOptions?.addSlideButton ?? {}),
188
+ }
189
+
190
+ const slide =
191
+ extensionKitOptions?.slide === false
192
+ ? false
193
+ : {
194
+ ...defaultSlideOptions,
195
+ ...(extensionKitOptions?.slide ?? {}),
196
+ }
197
+
198
+ return {
199
+ ...extensionKitOptions,
200
+ addSlideButton,
201
+ slide,
202
+ }
203
+ })
204
+
205
+ const resolvedExtensions = computed<AnyExtension[]>(() => {
206
+ const kit = ExtensionKit.configure(mergedExtensionKitOptions.value)
207
+ return extensions ? [kit, ...extensions] : [kit]
208
+ })
209
+
210
+ /**
211
+ * Initial content for the editor.
212
+ */
213
+ const initialContent = content ?? defaultSlide()
214
+
215
+ console.log('resolvedExtensions', resolvedExtensions.value, resolvedExtensions);
216
+ console.log('editorOptions (as single prop):', editorOptions);
217
+
218
+ const editor = useEditor(
219
+ {
220
+ content: initialContent,
221
+ extensions: resolvedExtensions.value,
222
+ theme,
223
+ editorProps: {
224
+ attributes: {
225
+ autocomplete: 'off',
226
+ autocorrect: 'off',
227
+ autocapitalize: 'off',
228
+ class: 'min-h-full min-w-full',
229
+ ...(editorProps?.attributes ?? {}),
230
+ },
231
+ ...editorProps,
232
+ },
233
+ ...editorOptions,
234
+ onUpdate: (ctx: any) => {
235
+ const json = ctx.editor.getJSON()
236
+ onChange?.(json, ctx.editor)
237
+ onUpdate?.(ctx)
238
+ },
239
+ }
240
+ );
241
+
242
+ watch(
243
+ editor,
244
+ newEditor => {
245
+ if (newEditor && !newEditor.isDestroyed) {
246
+ onEditorReady?.(newEditor)
247
+ } else {
248
+ console.log('[useSlideEditor] ❌ Editor not ready or destroyed')
249
+ }
250
+ },
251
+ { immediate: true }
252
+ )
253
+
254
+ return { editor, presets }
255
+ }
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+ const component: DefineComponent<{}, {}, any>
4
+ export default component
5
+ }