@blockslides/extension-slide 0.1.0 → 0.2.1

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.mts CHANGED
@@ -12,6 +12,23 @@ interface SlideOptions {
12
12
  * @default true
13
13
  */
14
14
  injectCSS: boolean;
15
+ /**
16
+ * Render mode for sizing
17
+ * - fixed: width/height set from size registry (mm/in/px)
18
+ * - dynamic: width:100% with preserved aspect ratio
19
+ * @default 'fixed'
20
+ */
21
+ renderMode: "fixed" | "dynamic";
22
+ /**
23
+ * Default size applied when attrs.size is absent
24
+ * @default '16x9'
25
+ */
26
+ defaultSize: "16x9" | "4x3" | "a4-portrait" | "a4-landscape" | "letter-portrait" | "letter-landscape" | "linkedin-banner";
27
+ /**
28
+ * Inject @media print/@page CSS for paper sizes
29
+ * @default true
30
+ */
31
+ injectPrintCSS: boolean;
15
32
  /**
16
33
  * Content Security Policy nonce
17
34
  */
@@ -19,4 +36,4 @@ interface SlideOptions {
19
36
  }
20
37
  declare const Slide: Node<SlideOptions, any>;
21
38
 
22
- export { Slide };
39
+ export { Slide, type SlideOptions };
package/dist/index.d.ts CHANGED
@@ -12,6 +12,23 @@ interface SlideOptions {
12
12
  * @default true
13
13
  */
14
14
  injectCSS: boolean;
15
+ /**
16
+ * Render mode for sizing
17
+ * - fixed: width/height set from size registry (mm/in/px)
18
+ * - dynamic: width:100% with preserved aspect ratio
19
+ * @default 'fixed'
20
+ */
21
+ renderMode: "fixed" | "dynamic";
22
+ /**
23
+ * Default size applied when attrs.size is absent
24
+ * @default '16x9'
25
+ */
26
+ defaultSize: "16x9" | "4x3" | "a4-portrait" | "a4-landscape" | "letter-portrait" | "letter-landscape" | "linkedin-banner";
27
+ /**
28
+ * Inject @media print/@page CSS for paper sizes
29
+ * @default true
30
+ */
31
+ injectPrintCSS: boolean;
15
32
  /**
16
33
  * Content Security Policy nonce
17
34
  */
@@ -19,4 +36,4 @@ interface SlideOptions {
19
36
  }
20
37
  declare const Slide: Node<SlideOptions, any>;
21
38
 
22
- export { Slide };
39
+ export { Slide, type SlideOptions };
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ var import_core = require("@blockslides/core");
29
29
  var import_state = require("@blockslides/pm/state");
30
30
  var slideStyles = `
31
31
  .slide {
32
+ position: relative;
32
33
  height: var(--slide-height, 100%);
33
34
  min-height: var(--slide-min-height, 250px);
34
35
  display: flex;
@@ -39,7 +40,64 @@ var slideStyles = `
39
40
  box-shadow: var(--slide-shadow);
40
41
  margin-bottom: var(--slide-margin-bottom);
41
42
  }
43
+
44
+ /* Background helpers (driven by data attributes + inline CSS vars) */
45
+ .slide[data-bg-mode="color"] {
46
+ background-color: var(--slide-bg-color, var(--slide-bg));
47
+ }
48
+
49
+ .slide[data-bg-mode="image"],
50
+ .slide[data-bg-mode="imageOverlay"] {
51
+ background-size: cover;
52
+ background-repeat: no-repeat;
53
+ background-position: center;
54
+ }
55
+
56
+ .slide[data-bg-mode="imageOverlay"]::before {
57
+ content: "";
58
+ position: absolute;
59
+ inset: 0;
60
+ pointer-events: none;
61
+ background-color: var(--slide-bg-overlay-color, #000);
62
+ opacity: var(--slide-bg-overlay-opacity, 0.35);
63
+ }
42
64
  `;
65
+ var fixedSizeStyles = `
66
+ .slide[data-size="16x9"] { width: 1920px; height: 1080px; }
67
+ .slide[data-size="4x3"] { width: 1600px; height: 1200px; }
68
+ .slide[data-size="a4-portrait"] { width: 210mm; height: 297mm; }
69
+ .slide[data-size="a4-landscape"] { width: 297mm; height: 210mm; }
70
+ .slide[data-size="letter-portrait"] { width: 8.5in; height: 11in; }
71
+ .slide[data-size="letter-landscape"] { width: 11in; height: 8.5in; }
72
+ .slide[data-size="linkedin-banner"] { width: 1584px; height: 396px; }
73
+ `.trim();
74
+ var dynamicSizeStyles = `
75
+ .slide[data-size="16x9"] { width: 100%; height: auto; aspect-ratio: 16 / 9; }
76
+ .slide[data-size="4x3"] { width: 100%; height: auto; aspect-ratio: 4 / 3; }
77
+ .slide[data-size="a4-portrait"] { width: 100%; height: auto; aspect-ratio: 210 / 297; }
78
+ .slide[data-size="a4-landscape"] { width: 100%; height: auto; aspect-ratio: 297 / 210; }
79
+ .slide[data-size="letter-portrait"] { width: 100%; height: auto; aspect-ratio: 8.5 / 11; }
80
+ .slide[data-size="letter-landscape"] { width: 100%; height: auto; aspect-ratio: 11 / 8.5; }
81
+ .slide[data-size="linkedin-banner"] { width: 100%; height: auto; aspect-ratio: 1584 / 396; }
82
+ `.trim();
83
+ var printSizeStyles = `
84
+ @media print {
85
+ .slide[data-size="a4-portrait"] { width: 210mm; height: 297mm; }
86
+ @page { size: A4 portrait; margin: 0; }
87
+ }
88
+ @media print {
89
+ .slide[data-size="a4-landscape"] { width: 297mm; height: 210mm; }
90
+ @page { size: A4 landscape; margin: 0; }
91
+ }
92
+ @media print {
93
+ .slide[data-size="letter-portrait"] { width: 8.5in; height: 11in; }
94
+ @page { size: Letter portrait; margin: 0; }
95
+ }
96
+ @media print {
97
+ .slide[data-size="letter-landscape"] { width: 11in; height: 8.5in; }
98
+ @page { size: Letter landscape; margin: 0; }
99
+ }
100
+ `.trim();
43
101
  var SlidePluginKey = new import_state.PluginKey("slide");
44
102
  var Slide = import_core.Node.create({
45
103
  name: "slide",
@@ -51,19 +109,87 @@ var Slide = import_core.Node.create({
51
109
  return {
52
110
  HTMLAttributes: {},
53
111
  injectCSS: true,
112
+ renderMode: "fixed",
113
+ defaultSize: "16x9",
114
+ injectPrintCSS: true,
54
115
  injectNonce: void 0
55
116
  };
56
117
  },
118
+ addAttributes() {
119
+ return {
120
+ size: {
121
+ default: this.options.defaultSize,
122
+ parseHTML: (element) => element.getAttribute("data-size") || this.options.defaultSize,
123
+ renderHTML: (attributes) => {
124
+ if (!attributes.size) {
125
+ return { "data-size": this.options.defaultSize };
126
+ }
127
+ return { "data-size": attributes.size };
128
+ }
129
+ },
130
+ className: {
131
+ default: ""
132
+ },
133
+ id: {
134
+ default: null
135
+ },
136
+ backgroundMode: {
137
+ default: "none"
138
+ },
139
+ backgroundColor: {
140
+ default: null
141
+ },
142
+ backgroundImage: {
143
+ default: null
144
+ },
145
+ backgroundOverlayColor: {
146
+ default: null
147
+ },
148
+ backgroundOverlayOpacity: {
149
+ default: null
150
+ }
151
+ };
152
+ },
57
153
  parseHTML() {
58
154
  return [{ tag: "div.slide" }];
59
155
  },
60
156
  renderHTML({ HTMLAttributes }) {
157
+ const merged = (0, import_core.mergeAttributes)(this.options.HTMLAttributes, HTMLAttributes);
158
+ const {
159
+ backgroundMode,
160
+ backgroundColor,
161
+ backgroundImage,
162
+ backgroundOverlayColor,
163
+ backgroundOverlayOpacity,
164
+ ...rest
165
+ } = merged;
166
+ const styleParts = [];
167
+ if (backgroundColor) {
168
+ styleParts.push(`--slide-bg-color: ${backgroundColor}`);
169
+ }
170
+ if (backgroundImage) {
171
+ const escaped = String(backgroundImage).replace(/"/g, '\\"');
172
+ styleParts.push(`background-image: url("${escaped}")`);
173
+ }
174
+ if (backgroundOverlayColor) {
175
+ styleParts.push(`--slide-bg-overlay-color: ${backgroundOverlayColor}`);
176
+ }
177
+ if (backgroundOverlayOpacity != null) {
178
+ styleParts.push(`--slide-bg-overlay-opacity: ${backgroundOverlayOpacity}`);
179
+ }
180
+ const style = [rest.style, styleParts.join("; ")].filter(Boolean).join("; ");
181
+ const className = [rest.class, rest.className, "slide"].filter(Boolean).join(" ");
182
+ delete rest.className;
183
+ delete rest.class;
61
184
  return [
62
185
  "div",
63
- (0, import_core.mergeAttributes)(this.options.HTMLAttributes, HTMLAttributes, {
64
- class: "slide",
65
- "data-node-type": "slide"
66
- }),
186
+ {
187
+ ...rest,
188
+ class: className || "slide",
189
+ "data-node-type": "slide",
190
+ "data-bg-mode": backgroundMode || "none",
191
+ style: style || void 0
192
+ },
67
193
  0
68
194
  ];
69
195
  },
@@ -75,6 +201,16 @@ var Slide = import_core.Node.create({
75
201
  init: () => {
76
202
  if (this.options.injectCSS && typeof document !== "undefined") {
77
203
  (0, import_core.createStyleTag)(slideStyles, this.options.injectNonce, "slide");
204
+ const sizingCss = this.options.renderMode === "dynamic" ? dynamicSizeStyles : fixedSizeStyles;
205
+ if (sizingCss) {
206
+ (0, import_core.createStyleTag)(sizingCss, this.options.injectNonce, "slide-sizes");
207
+ }
208
+ if (this.options.injectPrintCSS) {
209
+ const printCss = printSizeStyles;
210
+ if (printCss) {
211
+ (0, import_core.createStyleTag)(printCss, this.options.injectNonce, "slide-print");
212
+ }
213
+ }
78
214
  }
79
215
  return {};
80
216
  },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/slide.ts"],"sourcesContent":["export { Slide } from \"./slide\";\n","import { Node, mergeAttributes, createStyleTag } from \"@blockslides/core\";\nimport { Plugin, PluginKey } from \"@blockslides/pm/state\";\n\nconst slideStyles = `\n.slide {\n height: var(--slide-height, 100%);\n min-height: var(--slide-min-height, 250px);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--slide-bg);\n border-radius: var(--slide-border-radius);\n box-shadow: var(--slide-shadow);\n margin-bottom: var(--slide-margin-bottom);\n}\n`;\n\nexport interface SlideOptions {\n /**\n * The HTML attributes for a slide node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>;\n /**\n * Whether to inject CSS styles\n * @default true\n */\n injectCSS: boolean;\n /**\n * Content Security Policy nonce\n */\n injectNonce?: string;\n}\n\nconst SlidePluginKey = new PluginKey(\"slide\");\n\nexport const Slide = Node.create<SlideOptions>({\n name: \"slide\",\n isolating: true,\n content: \"row+\",\n\n group: \"slide\",\n\n defining: true,\n\n addOptions() {\n return {\n HTMLAttributes: {},\n injectCSS: true,\n injectNonce: undefined,\n };\n },\n\n parseHTML() {\n return [{ tag: \"div.slide\" }];\n },\n\n renderHTML({ HTMLAttributes }) {\n return [\n \"div\",\n mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {\n class: \"slide\",\n \"data-node-type\": \"slide\",\n }),\n 0,\n ];\n },\n\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: SlidePluginKey,\n state: {\n init: () => {\n if (this.options.injectCSS && typeof document !== \"undefined\") {\n createStyleTag(slideStyles, this.options.injectNonce, \"slide\");\n }\n return {};\n },\n apply: (_tr, pluginState: Record<string, never>) => pluginState,\n },\n }),\n ];\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAAsD;AACtD,mBAAkC;AAElC,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCpB,IAAM,iBAAiB,IAAI,uBAAU,OAAO;AAErC,IAAM,QAAQ,iBAAK,OAAqB;AAAA,EAC7C,MAAM;AAAA,EACN,WAAW;AAAA,EACX,SAAS;AAAA,EAET,OAAO;AAAA,EAEP,UAAU;AAAA,EAEV,aAAa;AACX,WAAO;AAAA,MACL,gBAAgB,CAAC;AAAA,MACjB,WAAW;AAAA,MACX,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EAEA,YAAY;AACV,WAAO,CAAC,EAAE,KAAK,YAAY,CAAC;AAAA,EAC9B;AAAA,EAEA,WAAW,EAAE,eAAe,GAAG;AAC7B,WAAO;AAAA,MACL;AAAA,UACA,6BAAgB,KAAK,QAAQ,gBAAgB,gBAAgB;AAAA,QAC3D,OAAO;AAAA,QACP,kBAAkB;AAAA,MACpB,CAAC;AAAA,MACD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,wBAAwB;AACtB,WAAO;AAAA,MACL,IAAI,oBAAO;AAAA,QACT,KAAK;AAAA,QACL,OAAO;AAAA,UACL,MAAM,MAAM;AACV,gBAAI,KAAK,QAAQ,aAAa,OAAO,aAAa,aAAa;AAC7D,8CAAe,aAAa,KAAK,QAAQ,aAAa,OAAO;AAAA,YAC/D;AACA,mBAAO,CAAC;AAAA,UACV;AAAA,UACA,OAAO,CAAC,KAAK,gBAAuC;AAAA,QACtD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/slide.ts"],"sourcesContent":["export { Slide } from \"./slide\";\nexport type { SlideOptions } from \"./slide\";\n","import { Node, mergeAttributes, createStyleTag } from \"@blockslides/core\";\nimport { Plugin, PluginKey } from \"@blockslides/pm/state\";\n\nconst slideStyles = `\n.slide {\n position: relative;\n height: var(--slide-height, 100%);\n min-height: var(--slide-min-height, 250px);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--slide-bg);\n border-radius: var(--slide-border-radius);\n box-shadow: var(--slide-shadow);\n margin-bottom: var(--slide-margin-bottom);\n}\n\n/* Background helpers (driven by data attributes + inline CSS vars) */\n.slide[data-bg-mode=\"color\"] {\n background-color: var(--slide-bg-color, var(--slide-bg));\n}\n\n.slide[data-bg-mode=\"image\"],\n.slide[data-bg-mode=\"imageOverlay\"] {\n background-size: cover;\n background-repeat: no-repeat;\n background-position: center;\n}\n\n.slide[data-bg-mode=\"imageOverlay\"]::before {\n content: \"\";\n position: absolute;\n inset: 0;\n pointer-events: none;\n background-color: var(--slide-bg-overlay-color, #000);\n opacity: var(--slide-bg-overlay-opacity, 0.35);\n}\n`;\n\nconst fixedSizeStyles = `\n.slide[data-size=\"16x9\"] { width: 1920px; height: 1080px; }\n.slide[data-size=\"4x3\"] { width: 1600px; height: 1200px; }\n.slide[data-size=\"a4-portrait\"] { width: 210mm; height: 297mm; }\n.slide[data-size=\"a4-landscape\"] { width: 297mm; height: 210mm; }\n.slide[data-size=\"letter-portrait\"] { width: 8.5in; height: 11in; }\n.slide[data-size=\"letter-landscape\"] { width: 11in; height: 8.5in; }\n.slide[data-size=\"linkedin-banner\"] { width: 1584px; height: 396px; }\n`.trim();\n\nconst dynamicSizeStyles = `\n.slide[data-size=\"16x9\"] { width: 100%; height: auto; aspect-ratio: 16 / 9; }\n.slide[data-size=\"4x3\"] { width: 100%; height: auto; aspect-ratio: 4 / 3; }\n.slide[data-size=\"a4-portrait\"] { width: 100%; height: auto; aspect-ratio: 210 / 297; }\n.slide[data-size=\"a4-landscape\"] { width: 100%; height: auto; aspect-ratio: 297 / 210; }\n.slide[data-size=\"letter-portrait\"] { width: 100%; height: auto; aspect-ratio: 8.5 / 11; }\n.slide[data-size=\"letter-landscape\"] { width: 100%; height: auto; aspect-ratio: 11 / 8.5; }\n.slide[data-size=\"linkedin-banner\"] { width: 100%; height: auto; aspect-ratio: 1584 / 396; }\n`.trim();\n\nconst printSizeStyles = `\n@media print {\n .slide[data-size=\"a4-portrait\"] { width: 210mm; height: 297mm; }\n @page { size: A4 portrait; margin: 0; }\n}\n@media print {\n .slide[data-size=\"a4-landscape\"] { width: 297mm; height: 210mm; }\n @page { size: A4 landscape; margin: 0; }\n}\n@media print {\n .slide[data-size=\"letter-portrait\"] { width: 8.5in; height: 11in; }\n @page { size: Letter portrait; margin: 0; }\n}\n@media print {\n .slide[data-size=\"letter-landscape\"] { width: 11in; height: 8.5in; }\n @page { size: Letter landscape; margin: 0; }\n}\n`.trim();\n\nexport interface SlideOptions {\n /**\n * The HTML attributes for a slide node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>;\n /**\n * Whether to inject CSS styles\n * @default true\n */\n injectCSS: boolean;\n /**\n * Render mode for sizing\n * - fixed: width/height set from size registry (mm/in/px)\n * - dynamic: width:100% with preserved aspect ratio\n * @default 'fixed'\n */\n renderMode: \"fixed\" | \"dynamic\";\n /**\n * Default size applied when attrs.size is absent\n * @default '16x9'\n */\n defaultSize: \"16x9\" | \"4x3\" | \"a4-portrait\" | \"a4-landscape\" | \"letter-portrait\" | \"letter-landscape\" | \"linkedin-banner\";\n /**\n * Inject @media print/@page CSS for paper sizes\n * @default true\n */\n injectPrintCSS: boolean;\n /**\n * Content Security Policy nonce\n */\n injectNonce?: string;\n}\n\nconst SlidePluginKey = new PluginKey(\"slide\");\n\nexport const Slide = Node.create<SlideOptions>({\n name: \"slide\",\n isolating: true,\n content: \"row+\",\n\n group: \"slide\",\n\n defining: true,\n\n addOptions() {\n return {\n HTMLAttributes: {},\n injectCSS: true,\n renderMode: \"fixed\",\n defaultSize: \"16x9\",\n injectPrintCSS: true,\n injectNonce: undefined,\n };\n },\n\n addAttributes() {\n return {\n size: {\n default: this.options.defaultSize,\n parseHTML: (element) => element.getAttribute(\"data-size\") || this.options.defaultSize,\n renderHTML: (attributes) => {\n if (!attributes.size) {\n return { \"data-size\": this.options.defaultSize };\n }\n return { \"data-size\": attributes.size };\n },\n },\n className: {\n default: \"\",\n },\n id: {\n default: null,\n },\n backgroundMode: {\n default: \"none\",\n },\n backgroundColor: {\n default: null,\n },\n backgroundImage: {\n default: null,\n },\n backgroundOverlayColor: {\n default: null,\n },\n backgroundOverlayOpacity: {\n default: null,\n },\n };\n },\n\n parseHTML() {\n return [{ tag: \"div.slide\" }];\n },\n\n renderHTML({ HTMLAttributes }) {\n const merged = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes);\n const {\n backgroundMode,\n backgroundColor,\n backgroundImage,\n backgroundOverlayColor,\n backgroundOverlayOpacity,\n ...rest\n } = merged;\n\n const styleParts: string[] = [];\n\n if (backgroundColor) {\n styleParts.push(`--slide-bg-color: ${backgroundColor}`);\n }\n\n if (backgroundImage) {\n const escaped = String(backgroundImage).replace(/\"/g, '\\\\\"');\n styleParts.push(`background-image: url(\"${escaped}\")`);\n }\n\n if (backgroundOverlayColor) {\n styleParts.push(`--slide-bg-overlay-color: ${backgroundOverlayColor}`);\n }\n\n if (backgroundOverlayOpacity != null) {\n styleParts.push(`--slide-bg-overlay-opacity: ${backgroundOverlayOpacity}`);\n }\n\n const style = [rest.style, styleParts.join(\"; \")].filter(Boolean).join(\"; \");\n\n const className = [rest.class, rest.className, \"slide\"].filter(Boolean).join(\" \");\n\n delete (rest as any).className;\n delete (rest as any).class;\n\n return [\n \"div\",\n {\n ...rest,\n class: className || \"slide\",\n \"data-node-type\": \"slide\",\n \"data-bg-mode\": backgroundMode || \"none\",\n style: style || undefined,\n },\n 0,\n ];\n },\n\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: SlidePluginKey,\n state: {\n init: () => {\n if (this.options.injectCSS && typeof document !== \"undefined\") {\n createStyleTag(slideStyles, this.options.injectNonce, \"slide\");\n const sizingCss =\n this.options.renderMode === \"dynamic\" ? dynamicSizeStyles : fixedSizeStyles;\n if (sizingCss) {\n createStyleTag(sizingCss, this.options.injectNonce, \"slide-sizes\");\n }\n if (this.options.injectPrintCSS) {\n const printCss = printSizeStyles;\n if (printCss) {\n createStyleTag(printCss, this.options.injectNonce, \"slide-print\");\n }\n }\n }\n return {};\n },\n apply: (_tr, pluginState: Record<string, never>) => pluginState,\n },\n }),\n ];\n },\n});\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { Node, mergeAttributes, createStyleTag } from "@blockslides/core";
3
3
  import { Plugin, PluginKey } from "@blockslides/pm/state";
4
4
  var slideStyles = `
5
5
  .slide {
6
+ position: relative;
6
7
  height: var(--slide-height, 100%);
7
8
  min-height: var(--slide-min-height, 250px);
8
9
  display: flex;
@@ -13,7 +14,64 @@ var slideStyles = `
13
14
  box-shadow: var(--slide-shadow);
14
15
  margin-bottom: var(--slide-margin-bottom);
15
16
  }
17
+
18
+ /* Background helpers (driven by data attributes + inline CSS vars) */
19
+ .slide[data-bg-mode="color"] {
20
+ background-color: var(--slide-bg-color, var(--slide-bg));
21
+ }
22
+
23
+ .slide[data-bg-mode="image"],
24
+ .slide[data-bg-mode="imageOverlay"] {
25
+ background-size: cover;
26
+ background-repeat: no-repeat;
27
+ background-position: center;
28
+ }
29
+
30
+ .slide[data-bg-mode="imageOverlay"]::before {
31
+ content: "";
32
+ position: absolute;
33
+ inset: 0;
34
+ pointer-events: none;
35
+ background-color: var(--slide-bg-overlay-color, #000);
36
+ opacity: var(--slide-bg-overlay-opacity, 0.35);
37
+ }
16
38
  `;
39
+ var fixedSizeStyles = `
40
+ .slide[data-size="16x9"] { width: 1920px; height: 1080px; }
41
+ .slide[data-size="4x3"] { width: 1600px; height: 1200px; }
42
+ .slide[data-size="a4-portrait"] { width: 210mm; height: 297mm; }
43
+ .slide[data-size="a4-landscape"] { width: 297mm; height: 210mm; }
44
+ .slide[data-size="letter-portrait"] { width: 8.5in; height: 11in; }
45
+ .slide[data-size="letter-landscape"] { width: 11in; height: 8.5in; }
46
+ .slide[data-size="linkedin-banner"] { width: 1584px; height: 396px; }
47
+ `.trim();
48
+ var dynamicSizeStyles = `
49
+ .slide[data-size="16x9"] { width: 100%; height: auto; aspect-ratio: 16 / 9; }
50
+ .slide[data-size="4x3"] { width: 100%; height: auto; aspect-ratio: 4 / 3; }
51
+ .slide[data-size="a4-portrait"] { width: 100%; height: auto; aspect-ratio: 210 / 297; }
52
+ .slide[data-size="a4-landscape"] { width: 100%; height: auto; aspect-ratio: 297 / 210; }
53
+ .slide[data-size="letter-portrait"] { width: 100%; height: auto; aspect-ratio: 8.5 / 11; }
54
+ .slide[data-size="letter-landscape"] { width: 100%; height: auto; aspect-ratio: 11 / 8.5; }
55
+ .slide[data-size="linkedin-banner"] { width: 100%; height: auto; aspect-ratio: 1584 / 396; }
56
+ `.trim();
57
+ var printSizeStyles = `
58
+ @media print {
59
+ .slide[data-size="a4-portrait"] { width: 210mm; height: 297mm; }
60
+ @page { size: A4 portrait; margin: 0; }
61
+ }
62
+ @media print {
63
+ .slide[data-size="a4-landscape"] { width: 297mm; height: 210mm; }
64
+ @page { size: A4 landscape; margin: 0; }
65
+ }
66
+ @media print {
67
+ .slide[data-size="letter-portrait"] { width: 8.5in; height: 11in; }
68
+ @page { size: Letter portrait; margin: 0; }
69
+ }
70
+ @media print {
71
+ .slide[data-size="letter-landscape"] { width: 11in; height: 8.5in; }
72
+ @page { size: Letter landscape; margin: 0; }
73
+ }
74
+ `.trim();
17
75
  var SlidePluginKey = new PluginKey("slide");
18
76
  var Slide = Node.create({
19
77
  name: "slide",
@@ -25,19 +83,87 @@ var Slide = Node.create({
25
83
  return {
26
84
  HTMLAttributes: {},
27
85
  injectCSS: true,
86
+ renderMode: "fixed",
87
+ defaultSize: "16x9",
88
+ injectPrintCSS: true,
28
89
  injectNonce: void 0
29
90
  };
30
91
  },
92
+ addAttributes() {
93
+ return {
94
+ size: {
95
+ default: this.options.defaultSize,
96
+ parseHTML: (element) => element.getAttribute("data-size") || this.options.defaultSize,
97
+ renderHTML: (attributes) => {
98
+ if (!attributes.size) {
99
+ return { "data-size": this.options.defaultSize };
100
+ }
101
+ return { "data-size": attributes.size };
102
+ }
103
+ },
104
+ className: {
105
+ default: ""
106
+ },
107
+ id: {
108
+ default: null
109
+ },
110
+ backgroundMode: {
111
+ default: "none"
112
+ },
113
+ backgroundColor: {
114
+ default: null
115
+ },
116
+ backgroundImage: {
117
+ default: null
118
+ },
119
+ backgroundOverlayColor: {
120
+ default: null
121
+ },
122
+ backgroundOverlayOpacity: {
123
+ default: null
124
+ }
125
+ };
126
+ },
31
127
  parseHTML() {
32
128
  return [{ tag: "div.slide" }];
33
129
  },
34
130
  renderHTML({ HTMLAttributes }) {
131
+ const merged = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes);
132
+ const {
133
+ backgroundMode,
134
+ backgroundColor,
135
+ backgroundImage,
136
+ backgroundOverlayColor,
137
+ backgroundOverlayOpacity,
138
+ ...rest
139
+ } = merged;
140
+ const styleParts = [];
141
+ if (backgroundColor) {
142
+ styleParts.push(`--slide-bg-color: ${backgroundColor}`);
143
+ }
144
+ if (backgroundImage) {
145
+ const escaped = String(backgroundImage).replace(/"/g, '\\"');
146
+ styleParts.push(`background-image: url("${escaped}")`);
147
+ }
148
+ if (backgroundOverlayColor) {
149
+ styleParts.push(`--slide-bg-overlay-color: ${backgroundOverlayColor}`);
150
+ }
151
+ if (backgroundOverlayOpacity != null) {
152
+ styleParts.push(`--slide-bg-overlay-opacity: ${backgroundOverlayOpacity}`);
153
+ }
154
+ const style = [rest.style, styleParts.join("; ")].filter(Boolean).join("; ");
155
+ const className = [rest.class, rest.className, "slide"].filter(Boolean).join(" ");
156
+ delete rest.className;
157
+ delete rest.class;
35
158
  return [
36
159
  "div",
37
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
38
- class: "slide",
39
- "data-node-type": "slide"
40
- }),
160
+ {
161
+ ...rest,
162
+ class: className || "slide",
163
+ "data-node-type": "slide",
164
+ "data-bg-mode": backgroundMode || "none",
165
+ style: style || void 0
166
+ },
41
167
  0
42
168
  ];
43
169
  },
@@ -49,6 +175,16 @@ var Slide = Node.create({
49
175
  init: () => {
50
176
  if (this.options.injectCSS && typeof document !== "undefined") {
51
177
  createStyleTag(slideStyles, this.options.injectNonce, "slide");
178
+ const sizingCss = this.options.renderMode === "dynamic" ? dynamicSizeStyles : fixedSizeStyles;
179
+ if (sizingCss) {
180
+ createStyleTag(sizingCss, this.options.injectNonce, "slide-sizes");
181
+ }
182
+ if (this.options.injectPrintCSS) {
183
+ const printCss = printSizeStyles;
184
+ if (printCss) {
185
+ createStyleTag(printCss, this.options.injectNonce, "slide-print");
186
+ }
187
+ }
52
188
  }
53
189
  return {};
54
190
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/slide.ts"],"sourcesContent":["import { Node, mergeAttributes, createStyleTag } from \"@blockslides/core\";\nimport { Plugin, PluginKey } from \"@blockslides/pm/state\";\n\nconst slideStyles = `\n.slide {\n height: var(--slide-height, 100%);\n min-height: var(--slide-min-height, 250px);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--slide-bg);\n border-radius: var(--slide-border-radius);\n box-shadow: var(--slide-shadow);\n margin-bottom: var(--slide-margin-bottom);\n}\n`;\n\nexport interface SlideOptions {\n /**\n * The HTML attributes for a slide node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>;\n /**\n * Whether to inject CSS styles\n * @default true\n */\n injectCSS: boolean;\n /**\n * Content Security Policy nonce\n */\n injectNonce?: string;\n}\n\nconst SlidePluginKey = new PluginKey(\"slide\");\n\nexport const Slide = Node.create<SlideOptions>({\n name: \"slide\",\n isolating: true,\n content: \"row+\",\n\n group: \"slide\",\n\n defining: true,\n\n addOptions() {\n return {\n HTMLAttributes: {},\n injectCSS: true,\n injectNonce: undefined,\n };\n },\n\n parseHTML() {\n return [{ tag: \"div.slide\" }];\n },\n\n renderHTML({ HTMLAttributes }) {\n return [\n \"div\",\n mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {\n class: \"slide\",\n \"data-node-type\": \"slide\",\n }),\n 0,\n ];\n },\n\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: SlidePluginKey,\n state: {\n init: () => {\n if (this.options.injectCSS && typeof document !== \"undefined\") {\n createStyleTag(slideStyles, this.options.injectNonce, \"slide\");\n }\n return {};\n },\n apply: (_tr, pluginState: Record<string, never>) => pluginState,\n },\n }),\n ];\n },\n});\n"],"mappings":";AAAA,SAAS,MAAM,iBAAiB,sBAAsB;AACtD,SAAS,QAAQ,iBAAiB;AAElC,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCpB,IAAM,iBAAiB,IAAI,UAAU,OAAO;AAErC,IAAM,QAAQ,KAAK,OAAqB;AAAA,EAC7C,MAAM;AAAA,EACN,WAAW;AAAA,EACX,SAAS;AAAA,EAET,OAAO;AAAA,EAEP,UAAU;AAAA,EAEV,aAAa;AACX,WAAO;AAAA,MACL,gBAAgB,CAAC;AAAA,MACjB,WAAW;AAAA,MACX,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EAEA,YAAY;AACV,WAAO,CAAC,EAAE,KAAK,YAAY,CAAC;AAAA,EAC9B;AAAA,EAEA,WAAW,EAAE,eAAe,GAAG;AAC7B,WAAO;AAAA,MACL;AAAA,MACA,gBAAgB,KAAK,QAAQ,gBAAgB,gBAAgB;AAAA,QAC3D,OAAO;AAAA,QACP,kBAAkB;AAAA,MACpB,CAAC;AAAA,MACD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,wBAAwB;AACtB,WAAO;AAAA,MACL,IAAI,OAAO;AAAA,QACT,KAAK;AAAA,QACL,OAAO;AAAA,UACL,MAAM,MAAM;AACV,gBAAI,KAAK,QAAQ,aAAa,OAAO,aAAa,aAAa;AAC7D,6BAAe,aAAa,KAAK,QAAQ,aAAa,OAAO;AAAA,YAC/D;AACA,mBAAO,CAAC;AAAA,UACV;AAAA,UACA,OAAO,CAAC,KAAK,gBAAuC;AAAA,QACtD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/slide.ts"],"sourcesContent":["import { Node, mergeAttributes, createStyleTag } from \"@blockslides/core\";\nimport { Plugin, PluginKey } from \"@blockslides/pm/state\";\n\nconst slideStyles = `\n.slide {\n position: relative;\n height: var(--slide-height, 100%);\n min-height: var(--slide-min-height, 250px);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--slide-bg);\n border-radius: var(--slide-border-radius);\n box-shadow: var(--slide-shadow);\n margin-bottom: var(--slide-margin-bottom);\n}\n\n/* Background helpers (driven by data attributes + inline CSS vars) */\n.slide[data-bg-mode=\"color\"] {\n background-color: var(--slide-bg-color, var(--slide-bg));\n}\n\n.slide[data-bg-mode=\"image\"],\n.slide[data-bg-mode=\"imageOverlay\"] {\n background-size: cover;\n background-repeat: no-repeat;\n background-position: center;\n}\n\n.slide[data-bg-mode=\"imageOverlay\"]::before {\n content: \"\";\n position: absolute;\n inset: 0;\n pointer-events: none;\n background-color: var(--slide-bg-overlay-color, #000);\n opacity: var(--slide-bg-overlay-opacity, 0.35);\n}\n`;\n\nconst fixedSizeStyles = `\n.slide[data-size=\"16x9\"] { width: 1920px; height: 1080px; }\n.slide[data-size=\"4x3\"] { width: 1600px; height: 1200px; }\n.slide[data-size=\"a4-portrait\"] { width: 210mm; height: 297mm; }\n.slide[data-size=\"a4-landscape\"] { width: 297mm; height: 210mm; }\n.slide[data-size=\"letter-portrait\"] { width: 8.5in; height: 11in; }\n.slide[data-size=\"letter-landscape\"] { width: 11in; height: 8.5in; }\n.slide[data-size=\"linkedin-banner\"] { width: 1584px; height: 396px; }\n`.trim();\n\nconst dynamicSizeStyles = `\n.slide[data-size=\"16x9\"] { width: 100%; height: auto; aspect-ratio: 16 / 9; }\n.slide[data-size=\"4x3\"] { width: 100%; height: auto; aspect-ratio: 4 / 3; }\n.slide[data-size=\"a4-portrait\"] { width: 100%; height: auto; aspect-ratio: 210 / 297; }\n.slide[data-size=\"a4-landscape\"] { width: 100%; height: auto; aspect-ratio: 297 / 210; }\n.slide[data-size=\"letter-portrait\"] { width: 100%; height: auto; aspect-ratio: 8.5 / 11; }\n.slide[data-size=\"letter-landscape\"] { width: 100%; height: auto; aspect-ratio: 11 / 8.5; }\n.slide[data-size=\"linkedin-banner\"] { width: 100%; height: auto; aspect-ratio: 1584 / 396; }\n`.trim();\n\nconst printSizeStyles = `\n@media print {\n .slide[data-size=\"a4-portrait\"] { width: 210mm; height: 297mm; }\n @page { size: A4 portrait; margin: 0; }\n}\n@media print {\n .slide[data-size=\"a4-landscape\"] { width: 297mm; height: 210mm; }\n @page { size: A4 landscape; margin: 0; }\n}\n@media print {\n .slide[data-size=\"letter-portrait\"] { width: 8.5in; height: 11in; }\n @page { size: Letter portrait; margin: 0; }\n}\n@media print {\n .slide[data-size=\"letter-landscape\"] { width: 11in; height: 8.5in; }\n @page { size: Letter landscape; margin: 0; }\n}\n`.trim();\n\nexport interface SlideOptions {\n /**\n * The HTML attributes for a slide node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>;\n /**\n * Whether to inject CSS styles\n * @default true\n */\n injectCSS: boolean;\n /**\n * Render mode for sizing\n * - fixed: width/height set from size registry (mm/in/px)\n * - dynamic: width:100% with preserved aspect ratio\n * @default 'fixed'\n */\n renderMode: \"fixed\" | \"dynamic\";\n /**\n * Default size applied when attrs.size is absent\n * @default '16x9'\n */\n defaultSize: \"16x9\" | \"4x3\" | \"a4-portrait\" | \"a4-landscape\" | \"letter-portrait\" | \"letter-landscape\" | \"linkedin-banner\";\n /**\n * Inject @media print/@page CSS for paper sizes\n * @default true\n */\n injectPrintCSS: boolean;\n /**\n * Content Security Policy nonce\n */\n injectNonce?: string;\n}\n\nconst SlidePluginKey = new PluginKey(\"slide\");\n\nexport const Slide = Node.create<SlideOptions>({\n name: \"slide\",\n isolating: true,\n content: \"row+\",\n\n group: \"slide\",\n\n defining: true,\n\n addOptions() {\n return {\n HTMLAttributes: {},\n injectCSS: true,\n renderMode: \"fixed\",\n defaultSize: \"16x9\",\n injectPrintCSS: true,\n injectNonce: undefined,\n };\n },\n\n addAttributes() {\n return {\n size: {\n default: this.options.defaultSize,\n parseHTML: (element) => element.getAttribute(\"data-size\") || this.options.defaultSize,\n renderHTML: (attributes) => {\n if (!attributes.size) {\n return { \"data-size\": this.options.defaultSize };\n }\n return { \"data-size\": attributes.size };\n },\n },\n className: {\n default: \"\",\n },\n id: {\n default: null,\n },\n backgroundMode: {\n default: \"none\",\n },\n backgroundColor: {\n default: null,\n },\n backgroundImage: {\n default: null,\n },\n backgroundOverlayColor: {\n default: null,\n },\n backgroundOverlayOpacity: {\n default: null,\n },\n };\n },\n\n parseHTML() {\n return [{ tag: \"div.slide\" }];\n },\n\n renderHTML({ HTMLAttributes }) {\n const merged = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes);\n const {\n backgroundMode,\n backgroundColor,\n backgroundImage,\n backgroundOverlayColor,\n backgroundOverlayOpacity,\n ...rest\n } = merged;\n\n const styleParts: string[] = [];\n\n if (backgroundColor) {\n styleParts.push(`--slide-bg-color: ${backgroundColor}`);\n }\n\n if (backgroundImage) {\n const escaped = String(backgroundImage).replace(/\"/g, '\\\\\"');\n styleParts.push(`background-image: url(\"${escaped}\")`);\n }\n\n if (backgroundOverlayColor) {\n styleParts.push(`--slide-bg-overlay-color: ${backgroundOverlayColor}`);\n }\n\n if (backgroundOverlayOpacity != null) {\n styleParts.push(`--slide-bg-overlay-opacity: ${backgroundOverlayOpacity}`);\n }\n\n const style = [rest.style, styleParts.join(\"; \")].filter(Boolean).join(\"; \");\n\n const className = [rest.class, rest.className, \"slide\"].filter(Boolean).join(\" \");\n\n delete (rest as any).className;\n delete (rest as any).class;\n\n return [\n \"div\",\n {\n ...rest,\n class: className || \"slide\",\n \"data-node-type\": \"slide\",\n \"data-bg-mode\": backgroundMode || \"none\",\n style: style || undefined,\n },\n 0,\n ];\n },\n\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: SlidePluginKey,\n state: {\n init: () => {\n if (this.options.injectCSS && typeof document !== \"undefined\") {\n createStyleTag(slideStyles, this.options.injectNonce, \"slide\");\n const sizingCss =\n this.options.renderMode === \"dynamic\" ? dynamicSizeStyles : fixedSizeStyles;\n if (sizingCss) {\n createStyleTag(sizingCss, this.options.injectNonce, \"slide-sizes\");\n }\n if (this.options.injectPrintCSS) {\n const printCss = printSizeStyles;\n if (printCss) {\n createStyleTag(printCss, this.options.injectNonce, \"slide-print\");\n }\n }\n }\n return {};\n },\n apply: (_tr, pluginState: Record<string, never>) => pluginState,\n },\n }),\n ];\n },\n});\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockslides/extension-slide",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Slide node extension for BlockSlides slide editor",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { Slide } from "./slide";
2
+ export type { SlideOptions } from "./slide";
package/src/slide.ts CHANGED
@@ -3,6 +3,7 @@ import { Plugin, PluginKey } from "@blockslides/pm/state";
3
3
 
4
4
  const slideStyles = `
5
5
  .slide {
6
+ position: relative;
6
7
  height: var(--slide-height, 100%);
7
8
  min-height: var(--slide-min-height, 250px);
8
9
  display: flex;
@@ -13,8 +14,68 @@ const slideStyles = `
13
14
  box-shadow: var(--slide-shadow);
14
15
  margin-bottom: var(--slide-margin-bottom);
15
16
  }
17
+
18
+ /* Background helpers (driven by data attributes + inline CSS vars) */
19
+ .slide[data-bg-mode="color"] {
20
+ background-color: var(--slide-bg-color, var(--slide-bg));
21
+ }
22
+
23
+ .slide[data-bg-mode="image"],
24
+ .slide[data-bg-mode="imageOverlay"] {
25
+ background-size: cover;
26
+ background-repeat: no-repeat;
27
+ background-position: center;
28
+ }
29
+
30
+ .slide[data-bg-mode="imageOverlay"]::before {
31
+ content: "";
32
+ position: absolute;
33
+ inset: 0;
34
+ pointer-events: none;
35
+ background-color: var(--slide-bg-overlay-color, #000);
36
+ opacity: var(--slide-bg-overlay-opacity, 0.35);
37
+ }
16
38
  `;
17
39
 
40
+ const fixedSizeStyles = `
41
+ .slide[data-size="16x9"] { width: 1920px; height: 1080px; }
42
+ .slide[data-size="4x3"] { width: 1600px; height: 1200px; }
43
+ .slide[data-size="a4-portrait"] { width: 210mm; height: 297mm; }
44
+ .slide[data-size="a4-landscape"] { width: 297mm; height: 210mm; }
45
+ .slide[data-size="letter-portrait"] { width: 8.5in; height: 11in; }
46
+ .slide[data-size="letter-landscape"] { width: 11in; height: 8.5in; }
47
+ .slide[data-size="linkedin-banner"] { width: 1584px; height: 396px; }
48
+ `.trim();
49
+
50
+ const dynamicSizeStyles = `
51
+ .slide[data-size="16x9"] { width: 100%; height: auto; aspect-ratio: 16 / 9; }
52
+ .slide[data-size="4x3"] { width: 100%; height: auto; aspect-ratio: 4 / 3; }
53
+ .slide[data-size="a4-portrait"] { width: 100%; height: auto; aspect-ratio: 210 / 297; }
54
+ .slide[data-size="a4-landscape"] { width: 100%; height: auto; aspect-ratio: 297 / 210; }
55
+ .slide[data-size="letter-portrait"] { width: 100%; height: auto; aspect-ratio: 8.5 / 11; }
56
+ .slide[data-size="letter-landscape"] { width: 100%; height: auto; aspect-ratio: 11 / 8.5; }
57
+ .slide[data-size="linkedin-banner"] { width: 100%; height: auto; aspect-ratio: 1584 / 396; }
58
+ `.trim();
59
+
60
+ const printSizeStyles = `
61
+ @media print {
62
+ .slide[data-size="a4-portrait"] { width: 210mm; height: 297mm; }
63
+ @page { size: A4 portrait; margin: 0; }
64
+ }
65
+ @media print {
66
+ .slide[data-size="a4-landscape"] { width: 297mm; height: 210mm; }
67
+ @page { size: A4 landscape; margin: 0; }
68
+ }
69
+ @media print {
70
+ .slide[data-size="letter-portrait"] { width: 8.5in; height: 11in; }
71
+ @page { size: Letter portrait; margin: 0; }
72
+ }
73
+ @media print {
74
+ .slide[data-size="letter-landscape"] { width: 11in; height: 8.5in; }
75
+ @page { size: Letter landscape; margin: 0; }
76
+ }
77
+ `.trim();
78
+
18
79
  export interface SlideOptions {
19
80
  /**
20
81
  * The HTML attributes for a slide node.
@@ -27,6 +88,23 @@ export interface SlideOptions {
27
88
  * @default true
28
89
  */
29
90
  injectCSS: boolean;
91
+ /**
92
+ * Render mode for sizing
93
+ * - fixed: width/height set from size registry (mm/in/px)
94
+ * - dynamic: width:100% with preserved aspect ratio
95
+ * @default 'fixed'
96
+ */
97
+ renderMode: "fixed" | "dynamic";
98
+ /**
99
+ * Default size applied when attrs.size is absent
100
+ * @default '16x9'
101
+ */
102
+ defaultSize: "16x9" | "4x3" | "a4-portrait" | "a4-landscape" | "letter-portrait" | "letter-landscape" | "linkedin-banner";
103
+ /**
104
+ * Inject @media print/@page CSS for paper sizes
105
+ * @default true
106
+ */
107
+ injectPrintCSS: boolean;
30
108
  /**
31
109
  * Content Security Policy nonce
32
110
  */
@@ -48,21 +126,99 @@ export const Slide = Node.create<SlideOptions>({
48
126
  return {
49
127
  HTMLAttributes: {},
50
128
  injectCSS: true,
129
+ renderMode: "fixed",
130
+ defaultSize: "16x9",
131
+ injectPrintCSS: true,
51
132
  injectNonce: undefined,
52
133
  };
53
134
  },
54
135
 
136
+ addAttributes() {
137
+ return {
138
+ size: {
139
+ default: this.options.defaultSize,
140
+ parseHTML: (element) => element.getAttribute("data-size") || this.options.defaultSize,
141
+ renderHTML: (attributes) => {
142
+ if (!attributes.size) {
143
+ return { "data-size": this.options.defaultSize };
144
+ }
145
+ return { "data-size": attributes.size };
146
+ },
147
+ },
148
+ className: {
149
+ default: "",
150
+ },
151
+ id: {
152
+ default: null,
153
+ },
154
+ backgroundMode: {
155
+ default: "none",
156
+ },
157
+ backgroundColor: {
158
+ default: null,
159
+ },
160
+ backgroundImage: {
161
+ default: null,
162
+ },
163
+ backgroundOverlayColor: {
164
+ default: null,
165
+ },
166
+ backgroundOverlayOpacity: {
167
+ default: null,
168
+ },
169
+ };
170
+ },
171
+
55
172
  parseHTML() {
56
173
  return [{ tag: "div.slide" }];
57
174
  },
58
175
 
59
176
  renderHTML({ HTMLAttributes }) {
177
+ const merged = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes);
178
+ const {
179
+ backgroundMode,
180
+ backgroundColor,
181
+ backgroundImage,
182
+ backgroundOverlayColor,
183
+ backgroundOverlayOpacity,
184
+ ...rest
185
+ } = merged;
186
+
187
+ const styleParts: string[] = [];
188
+
189
+ if (backgroundColor) {
190
+ styleParts.push(`--slide-bg-color: ${backgroundColor}`);
191
+ }
192
+
193
+ if (backgroundImage) {
194
+ const escaped = String(backgroundImage).replace(/"/g, '\\"');
195
+ styleParts.push(`background-image: url("${escaped}")`);
196
+ }
197
+
198
+ if (backgroundOverlayColor) {
199
+ styleParts.push(`--slide-bg-overlay-color: ${backgroundOverlayColor}`);
200
+ }
201
+
202
+ if (backgroundOverlayOpacity != null) {
203
+ styleParts.push(`--slide-bg-overlay-opacity: ${backgroundOverlayOpacity}`);
204
+ }
205
+
206
+ const style = [rest.style, styleParts.join("; ")].filter(Boolean).join("; ");
207
+
208
+ const className = [rest.class, rest.className, "slide"].filter(Boolean).join(" ");
209
+
210
+ delete (rest as any).className;
211
+ delete (rest as any).class;
212
+
60
213
  return [
61
214
  "div",
62
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
63
- class: "slide",
215
+ {
216
+ ...rest,
217
+ class: className || "slide",
64
218
  "data-node-type": "slide",
65
- }),
219
+ "data-bg-mode": backgroundMode || "none",
220
+ style: style || undefined,
221
+ },
66
222
  0,
67
223
  ];
68
224
  },
@@ -75,6 +231,17 @@ export const Slide = Node.create<SlideOptions>({
75
231
  init: () => {
76
232
  if (this.options.injectCSS && typeof document !== "undefined") {
77
233
  createStyleTag(slideStyles, this.options.injectNonce, "slide");
234
+ const sizingCss =
235
+ this.options.renderMode === "dynamic" ? dynamicSizeStyles : fixedSizeStyles;
236
+ if (sizingCss) {
237
+ createStyleTag(sizingCss, this.options.injectNonce, "slide-sizes");
238
+ }
239
+ if (this.options.injectPrintCSS) {
240
+ const printCss = printSizeStyles;
241
+ if (printCss) {
242
+ createStyleTag(printCss, this.options.injectNonce, "slide-print");
243
+ }
244
+ }
78
245
  }
79
246
  return {};
80
247
  },