@blockslides/extension-add-slide-button 0.1.0 → 0.2.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/dist/index.d.cts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { Extension } from '@blockslides/core';
2
2
  import { EditorView } from 'prosemirror-view';
3
3
 
4
+ interface PresetTemplateOption {
5
+ key: string;
6
+ label: string;
7
+ icon?: string;
8
+ build: () => any;
9
+ }
4
10
  interface AddSlideButtonOptions {
5
11
  /**
6
12
  * Whether to inject CSS styles for the button.
@@ -27,6 +33,30 @@ interface AddSlideButtonOptions {
27
33
  * @example '+' | 'Add Slide' | '➕' | '<svg>...</svg>'
28
34
  */
29
35
  content: string;
36
+ /**
37
+ * Optional template chooser that renders a second button and modal.
38
+ * @default false
39
+ */
40
+ showPresets?: boolean;
41
+ /**
42
+ * Background color for the preset modal content area.
43
+ * @default '#ffffff'
44
+ */
45
+ presetBackground?: string;
46
+ /**
47
+ * Text/icon color for the preset modal content area.
48
+ * @default '#000000'
49
+ */
50
+ presetForeground?: string;
51
+ /**
52
+ * Presets to show in the modal when showPresets is true.
53
+ */
54
+ presets?: PresetTemplateOption[];
55
+ /**
56
+ * Content for the template button (emoji or HTML).
57
+ * @default '✨'
58
+ */
59
+ templateButtonContent?: string;
30
60
  /**
31
61
  * Custom click handler for the button.
32
62
  * If not provided, will insert a new slide at the clicked position.
@@ -40,4 +70,4 @@ interface AddSlideButtonOptions {
40
70
  }
41
71
  declare const AddSlideButton: Extension<AddSlideButtonOptions, any>;
42
72
 
43
- export { AddSlideButton, type AddSlideButtonOptions, AddSlideButton as default };
73
+ export { AddSlideButton, type AddSlideButtonOptions, type PresetTemplateOption, AddSlideButton as default };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { Extension } from '@blockslides/core';
2
2
  import { EditorView } from 'prosemirror-view';
3
3
 
4
+ interface PresetTemplateOption {
5
+ key: string;
6
+ label: string;
7
+ icon?: string;
8
+ build: () => any;
9
+ }
4
10
  interface AddSlideButtonOptions {
5
11
  /**
6
12
  * Whether to inject CSS styles for the button.
@@ -27,6 +33,30 @@ interface AddSlideButtonOptions {
27
33
  * @example '+' | 'Add Slide' | '➕' | '<svg>...</svg>'
28
34
  */
29
35
  content: string;
36
+ /**
37
+ * Optional template chooser that renders a second button and modal.
38
+ * @default false
39
+ */
40
+ showPresets?: boolean;
41
+ /**
42
+ * Background color for the preset modal content area.
43
+ * @default '#ffffff'
44
+ */
45
+ presetBackground?: string;
46
+ /**
47
+ * Text/icon color for the preset modal content area.
48
+ * @default '#000000'
49
+ */
50
+ presetForeground?: string;
51
+ /**
52
+ * Presets to show in the modal when showPresets is true.
53
+ */
54
+ presets?: PresetTemplateOption[];
55
+ /**
56
+ * Content for the template button (emoji or HTML).
57
+ * @default '✨'
58
+ */
59
+ templateButtonContent?: string;
30
60
  /**
31
61
  * Custom click handler for the button.
32
62
  * If not provided, will insert a new slide at the clicked position.
@@ -40,4 +70,4 @@ interface AddSlideButtonOptions {
40
70
  }
41
71
  declare const AddSlideButton: Extension<AddSlideButtonOptions, any>;
42
72
 
43
- export { AddSlideButton, type AddSlideButtonOptions, AddSlideButton as default };
73
+ export { AddSlideButton, type AddSlideButtonOptions, type PresetTemplateOption, AddSlideButton as default };
package/dist/index.js CHANGED
@@ -3070,6 +3070,7 @@ var PluginKey = class {
3070
3070
  // src/add-slide-button.ts
3071
3071
  var SlideNodeView = class {
3072
3072
  constructor(node, view, getPos, options) {
3073
+ var _a;
3073
3074
  this.node = node;
3074
3075
  this.view = view;
3075
3076
  this.getPos = getPos;
@@ -3095,21 +3096,33 @@ var SlideNodeView = class {
3095
3096
  this.contentDOM.className = "slide";
3096
3097
  this.contentDOM.setAttribute("data-node-type", "slide");
3097
3098
  }
3098
- this.button = document.createElement("button");
3099
- this.button.className = "add-slide-button";
3100
- this.button.innerHTML = options.content;
3101
- this.button.setAttribute("type", "button");
3102
- this.button.contentEditable = "false";
3103
- if (options.buttonStyle) {
3104
- Object.entries(options.buttonStyle).forEach(([key, value]) => {
3105
- const camelKey = key.replace(
3106
- /-([a-z])/g,
3107
- (_, letter) => letter.toUpperCase()
3108
- );
3109
- this.button.style[camelKey] = value;
3110
- });
3111
- }
3112
- this.button.onclick = (event) => {
3099
+ const createButton = (html) => {
3100
+ const btn = document.createElement("button");
3101
+ btn.className = "add-slide-button";
3102
+ btn.innerHTML = html;
3103
+ btn.setAttribute("type", "button");
3104
+ btn.contentEditable = "false";
3105
+ if (options.buttonStyle) {
3106
+ Object.entries(options.buttonStyle).forEach(([key, value]) => {
3107
+ const camelKey = key.replace(
3108
+ /-([a-z])/g,
3109
+ (_, letter) => letter.toUpperCase()
3110
+ );
3111
+ btn.style[camelKey] = value;
3112
+ });
3113
+ }
3114
+ return btn;
3115
+ };
3116
+ const insertEmptySlide = (pos) => {
3117
+ const schema = this.view.state.schema;
3118
+ const slideType = schema.nodes.slide;
3119
+ const paragraphType = schema.nodes.paragraph;
3120
+ const slideContent = paragraphType.create();
3121
+ const slide = slideType.create(null, slideContent);
3122
+ const tr = this.view.state.tr.insert(pos + this.node.nodeSize, slide);
3123
+ this.view.dispatch(tr);
3124
+ };
3125
+ const handlePlusClick = (event) => {
3113
3126
  event.preventDefault();
3114
3127
  event.stopPropagation();
3115
3128
  const pos = this.getPos();
@@ -3126,17 +3139,117 @@ var SlideNodeView = class {
3126
3139
  event
3127
3140
  });
3128
3141
  } else {
3129
- const schema = this.view.state.schema;
3130
- const slideType = schema.nodes.slide;
3131
- const paragraphType = schema.nodes.paragraph;
3132
- const slideContent = paragraphType.create();
3133
- const slide = slideType.create(null, slideContent);
3134
- const tr = this.view.state.tr.insert(pos + this.node.nodeSize, slide);
3135
- this.view.dispatch(tr);
3142
+ insertEmptySlide(pos);
3143
+ }
3144
+ };
3145
+ const openPresetModal = (pos) => {
3146
+ if (this.presetModal) {
3147
+ this.presetModal.remove();
3136
3148
  }
3149
+ const overlay = document.createElement("div");
3150
+ overlay.className = "add-slide-preset-modal";
3151
+ const dialog = document.createElement("div");
3152
+ dialog.className = "add-slide-preset-dialog";
3153
+ if (options.presetBackground) {
3154
+ dialog.style.setProperty("--add-slide-preset-bg", options.presetBackground);
3155
+ }
3156
+ if (options.presetForeground) {
3157
+ dialog.style.setProperty("--add-slide-preset-fg", options.presetForeground);
3158
+ }
3159
+ const search = document.createElement("input");
3160
+ search.className = "add-slide-preset-search";
3161
+ search.type = "search";
3162
+ search.placeholder = "Choose a template";
3163
+ dialog.appendChild(search);
3164
+ const list = document.createElement("div");
3165
+ list.className = "add-slide-preset-list";
3166
+ const items = [];
3167
+ (options.presets || []).forEach((preset) => {
3168
+ var _a2;
3169
+ const item = document.createElement("button");
3170
+ item.className = "add-slide-preset-item";
3171
+ item.setAttribute("type", "button");
3172
+ item.innerHTML = `
3173
+ <span class="add-slide-preset-icon">${(_a2 = preset.icon) != null ? _a2 : ""}</span>
3174
+ <span class="add-slide-preset-label">${preset.label}</span>
3175
+ `;
3176
+ item.onclick = (event) => {
3177
+ event.preventDefault();
3178
+ event.stopPropagation();
3179
+ try {
3180
+ const nodeJSON = preset.build();
3181
+ const newSlide = this.view.state.schema.nodeFromJSON(nodeJSON);
3182
+ const tr = this.view.state.tr.insert(
3183
+ pos + this.node.nodeSize,
3184
+ newSlide
3185
+ );
3186
+ this.view.dispatch(tr);
3187
+ } catch (err) {
3188
+ console.error("Failed to insert preset slide", err);
3189
+ } finally {
3190
+ overlay.remove();
3191
+ this.presetModal = null;
3192
+ }
3193
+ };
3194
+ list.appendChild(item);
3195
+ items.push(item);
3196
+ });
3197
+ search.oninput = () => {
3198
+ const term = search.value.toLowerCase();
3199
+ items.forEach((item) => {
3200
+ var _a2, _b;
3201
+ const label = (_b = (_a2 = item.querySelector(".add-slide-preset-label")) == null ? void 0 : _a2.textContent) == null ? void 0 : _b.toLowerCase();
3202
+ item.style.display = !term || (label == null ? void 0 : label.includes(term)) ? "" : "none";
3203
+ });
3204
+ };
3205
+ dialog.appendChild(list);
3206
+ const close2 = () => {
3207
+ overlay.remove();
3208
+ this.presetModal = null;
3209
+ };
3210
+ overlay.onclick = (e) => {
3211
+ if (e.target === overlay) {
3212
+ close2();
3213
+ }
3214
+ };
3215
+ document.addEventListener(
3216
+ "keydown",
3217
+ (e) => {
3218
+ if (e.key === "Escape") {
3219
+ close2();
3220
+ }
3221
+ },
3222
+ { once: true }
3223
+ );
3224
+ overlay.appendChild(dialog);
3225
+ document.body.appendChild(overlay);
3226
+ this.presetModal = overlay;
3137
3227
  };
3138
- this.dom.appendChild(this.contentDOM);
3139
- this.dom.appendChild(this.button);
3228
+ const plusButton = createButton(options.content);
3229
+ plusButton.onclick = handlePlusClick;
3230
+ this.button = plusButton;
3231
+ if (options.showPresets) {
3232
+ const templateButton = createButton(
3233
+ (_a = options.templateButtonContent) != null ? _a : "\u2728"
3234
+ );
3235
+ templateButton.onclick = (event) => {
3236
+ event.preventDefault();
3237
+ event.stopPropagation();
3238
+ const pos = this.getPos();
3239
+ if (pos === void 0) return;
3240
+ openPresetModal(pos);
3241
+ };
3242
+ const group = document.createElement("div");
3243
+ group.className = "add-slide-button-group";
3244
+ group.appendChild(plusButton);
3245
+ group.appendChild(templateButton);
3246
+ this.dom.appendChild(this.contentDOM);
3247
+ this.dom.appendChild(group);
3248
+ } else {
3249
+ this.button = plusButton;
3250
+ this.dom.appendChild(this.contentDOM);
3251
+ this.dom.appendChild(this.button);
3252
+ }
3140
3253
  }
3141
3254
  update(node) {
3142
3255
  if (node.type.name !== "slide") return false;
@@ -3144,7 +3257,13 @@ var SlideNodeView = class {
3144
3257
  return true;
3145
3258
  }
3146
3259
  destroy() {
3147
- this.button.onclick = null;
3260
+ if (this.button) {
3261
+ this.button.onclick = null;
3262
+ }
3263
+ if (this.presetModal) {
3264
+ this.presetModal.remove();
3265
+ this.presetModal = null;
3266
+ }
3148
3267
  }
3149
3268
  };
3150
3269
  var addSlideButtonStyles = `
@@ -3186,6 +3305,131 @@ var addSlideButtonStyles = `
3186
3305
  outline: 2px solid var(--editor-focus, #3b82f6);
3187
3306
  outline-offset: 2px;
3188
3307
  }
3308
+
3309
+ .add-slide-button-group {
3310
+ display: grid;
3311
+ grid-template-columns: repeat(2, 1fr);
3312
+ width: 180px;
3313
+ margin: 16px auto 32px auto;
3314
+ border: 1px solid var(--slide-border, #e5e5e5);
3315
+ border-radius: 12px;
3316
+ overflow: hidden;
3317
+ background-color: var(--slide-bg, #ffffff);
3318
+ box-shadow: var(--slide-shadow, 0 4px 12px rgba(0, 0, 0, 0.08));
3319
+ }
3320
+
3321
+ .add-slide-button-group .add-slide-button {
3322
+ margin: 0;
3323
+ border: none;
3324
+ border-radius: 0;
3325
+ width: 100%;
3326
+ height: 48px;
3327
+ }
3328
+
3329
+ .add-slide-button-group .add-slide-button:first-child {
3330
+ border-right: 1px solid var(--slide-border, #e5e5e5);
3331
+ border-top-left-radius: 12px;
3332
+ border-bottom-left-radius: 12px;
3333
+ }
3334
+
3335
+ .add-slide-button-group .add-slide-button:last-child {
3336
+ border-top-right-radius: 12px;
3337
+ border-bottom-right-radius: 12px;
3338
+ white-space: nowrap;
3339
+ font-size: 14px;
3340
+ padding: 0 12px;
3341
+ }
3342
+
3343
+ .add-slide-preset-modal {
3344
+ position: fixed;
3345
+ inset: 0;
3346
+ background: rgba(0, 0, 0, 0.35);
3347
+ display: flex;
3348
+ align-items: center;
3349
+ justify-content: center;
3350
+ z-index: 9999;
3351
+ }
3352
+
3353
+ .add-slide-preset-dialog {
3354
+ background: var(--add-slide-preset-bg, #ffffff);
3355
+ color: var(--add-slide-preset-fg, #000000);
3356
+ border-radius: 8px;
3357
+ padding: 16px 16px 12px 16px;
3358
+ min-width: 140px;
3359
+ max-width: 480px;
3360
+ max-height: 60vh;
3361
+ overflow: hidden;
3362
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
3363
+ }
3364
+
3365
+ .add-slide-preset-search {
3366
+ width: 100%;
3367
+ padding: 10px 12px;
3368
+ margin-bottom: 10px;
3369
+ border-radius: 8px;
3370
+ border: 1px solid color-mix(in srgb, var(--add-slide-preset-fg, #000000) 18%, transparent);
3371
+ font-size: 14px;
3372
+ color: inherit;
3373
+ background: color-mix(in srgb, var(--add-slide-preset-bg, #ffffff) 90%, var(--add-slide-preset-fg, #000000) 10%);
3374
+ }
3375
+ .add-slide-preset-search:focus {
3376
+ outline: 2px solid color-mix(in srgb, var(--add-slide-preset-fg, #3b82f6) 35%, transparent);
3377
+ outline-offset: 1px;
3378
+ }
3379
+
3380
+ .add-slide-preset-list {
3381
+ display: flex;
3382
+ flex-direction: column;
3383
+ gap: 6px;
3384
+ max-height: 52vh;
3385
+ overflow: auto;
3386
+ scrollbar-width: none;
3387
+ }
3388
+
3389
+ .add-slide-preset-list::-webkit-scrollbar {
3390
+ display: none;
3391
+ }
3392
+
3393
+ .add-slide-preset-item {
3394
+ display: flex;
3395
+ flex-direction: column;
3396
+ align-items: center;
3397
+ gap: 8px;
3398
+ width: 100%;
3399
+ border: none;
3400
+ background: transparent;
3401
+ color: inherit;
3402
+ padding: 10px 0 12px 0;
3403
+ cursor: pointer;
3404
+ border-bottom: 1px solid color-mix(in srgb, var(--add-slide-preset-fg, #000000) 12%, transparent);
3405
+ }
3406
+
3407
+ .add-slide-preset-item:last-child {
3408
+ border-bottom: none;
3409
+ }
3410
+
3411
+ .add-slide-preset-item:hover {
3412
+ background: rgba(0, 0, 0, 0.03);
3413
+ }
3414
+
3415
+ .add-slide-preset-icon {
3416
+ display: block;
3417
+ width: 100%;
3418
+ height: auto;
3419
+ line-height: 0;
3420
+ }
3421
+ .add-slide-preset-icon > svg {
3422
+ width: 100%;
3423
+ height: auto;
3424
+ display: block;
3425
+ }
3426
+
3427
+ .add-slide-preset-label {
3428
+ text-align: center;
3429
+ font-size: 14px;
3430
+ font-weight: 600;
3431
+ width: 100%;
3432
+ }
3189
3433
  `;
3190
3434
  var AddSlideButton = Extension.create({
3191
3435
  name: "addSlideButton",
@@ -3195,7 +3439,12 @@ var AddSlideButton = Extension.create({
3195
3439
  injectNonce: void 0,
3196
3440
  buttonStyle: {},
3197
3441
  content: "+",
3198
- onClick: null
3442
+ showPresets: false,
3443
+ presets: [],
3444
+ templateButtonContent: "Template +",
3445
+ onClick: null,
3446
+ presetBackground: "#ffffff",
3447
+ presetForeground: "#000000"
3199
3448
  };
3200
3449
  },
3201
3450
  addProseMirrorPlugins() {