@twelvemonday/blog-editor 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -0
- package/dist/index.cjs +2228 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +2226 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +36 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2228 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
BLOCK_LABELS: () => BLOCK_LABELS,
|
|
25
|
+
BlogEditor: () => BlogEditor,
|
|
26
|
+
EditorProvider: () => EditorProvider,
|
|
27
|
+
HEADING_CLASSES: () => HEADING_CLASSES,
|
|
28
|
+
HEADING_LABELS: () => HEADING_LABELS,
|
|
29
|
+
blocksToHtml: () => blocksToHtml,
|
|
30
|
+
cleanPastedHtml: () => cleanPastedHtml,
|
|
31
|
+
computeSeoScore: () => computeSeoScore,
|
|
32
|
+
createBlock: () => createBlock,
|
|
33
|
+
defaultTheme: () => defaultTheme,
|
|
34
|
+
descriptionLengthColor: () => descriptionLengthColor,
|
|
35
|
+
getSeoPreview: () => getSeoPreview,
|
|
36
|
+
htmlToBlocks: () => htmlToBlocks,
|
|
37
|
+
inlineHtmlFromBlocks: () => inlineHtmlFromBlocks,
|
|
38
|
+
insertHtmlAtCursor: () => insertHtmlAtCursor,
|
|
39
|
+
isMultiBlockPaste: () => isMultiBlockPaste,
|
|
40
|
+
pastedHtmlToBlocks: () => pastedHtmlToBlocks,
|
|
41
|
+
resolveTheme: () => resolveTheme,
|
|
42
|
+
slugifyTitle: () => slugifyTitle,
|
|
43
|
+
themeToCssVars: () => themeToCssVars,
|
|
44
|
+
titleLengthColor: () => titleLengthColor,
|
|
45
|
+
useEditorContext: () => useEditorContext
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(index_exports);
|
|
48
|
+
|
|
49
|
+
// src/components/BlogEditor.tsx
|
|
50
|
+
var import_react5 = require("react");
|
|
51
|
+
var import_lucide_react6 = require("lucide-react");
|
|
52
|
+
var import_core3 = require("@dnd-kit/core");
|
|
53
|
+
var import_sortable2 = require("@dnd-kit/sortable");
|
|
54
|
+
|
|
55
|
+
// src/context.tsx
|
|
56
|
+
var import_react = require("react");
|
|
57
|
+
|
|
58
|
+
// src/theme.ts
|
|
59
|
+
var defaultTheme = {
|
|
60
|
+
primary: "#04644A",
|
|
61
|
+
primaryHover: "#035239",
|
|
62
|
+
primaryMuted: "#f0fdf4",
|
|
63
|
+
primaryBorder: "#bbf7d0",
|
|
64
|
+
background: "#f0f0f1",
|
|
65
|
+
surface: "#ffffff",
|
|
66
|
+
successText: "#04644A"
|
|
67
|
+
};
|
|
68
|
+
function resolveTheme(partial) {
|
|
69
|
+
return { ...defaultTheme, ...partial };
|
|
70
|
+
}
|
|
71
|
+
function themeToCssVars(theme) {
|
|
72
|
+
return {
|
|
73
|
+
"--be-primary": theme.primary,
|
|
74
|
+
"--be-primary-hover": theme.primaryHover,
|
|
75
|
+
"--be-primary-muted": theme.primaryMuted,
|
|
76
|
+
"--be-primary-border": theme.primaryBorder,
|
|
77
|
+
"--be-bg": theme.background,
|
|
78
|
+
"--be-surface": theme.surface,
|
|
79
|
+
"--be-success-text": theme.successText
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/context.tsx
|
|
84
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
85
|
+
var EditorContext = (0, import_react.createContext)({
|
|
86
|
+
theme: defaultTheme,
|
|
87
|
+
siteName: "Site",
|
|
88
|
+
baseUrl: "https://example.com"
|
|
89
|
+
});
|
|
90
|
+
function EditorProvider({
|
|
91
|
+
children,
|
|
92
|
+
onUploadImage,
|
|
93
|
+
theme,
|
|
94
|
+
siteName = "Site",
|
|
95
|
+
baseUrl = "https://example.com"
|
|
96
|
+
}) {
|
|
97
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
98
|
+
EditorContext.Provider,
|
|
99
|
+
{
|
|
100
|
+
value: {
|
|
101
|
+
onUploadImage,
|
|
102
|
+
theme: resolveTheme(theme),
|
|
103
|
+
siteName,
|
|
104
|
+
baseUrl
|
|
105
|
+
},
|
|
106
|
+
children
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
function useEditorContext() {
|
|
111
|
+
return (0, import_react.useContext)(EditorContext);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/lib/blog-blocks.ts
|
|
115
|
+
var BLOCK_LABELS = {
|
|
116
|
+
paragraph: "Paragraph",
|
|
117
|
+
heading: "Heading",
|
|
118
|
+
list: "List",
|
|
119
|
+
quote: "Quote",
|
|
120
|
+
image: "Image",
|
|
121
|
+
embed: "Embed / iframe",
|
|
122
|
+
html: "Custom HTML",
|
|
123
|
+
code: "Code",
|
|
124
|
+
separator: "Separator"
|
|
125
|
+
};
|
|
126
|
+
var HEADING_LABELS = {
|
|
127
|
+
1: "Heading 1",
|
|
128
|
+
2: "Heading 2",
|
|
129
|
+
3: "Heading 3",
|
|
130
|
+
4: "Heading 4",
|
|
131
|
+
5: "Heading 5",
|
|
132
|
+
6: "Heading 6"
|
|
133
|
+
};
|
|
134
|
+
function createBlock(type) {
|
|
135
|
+
const id = `blk-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
136
|
+
switch (type) {
|
|
137
|
+
case "heading":
|
|
138
|
+
return { id, type, level: 2, html: "" };
|
|
139
|
+
case "list":
|
|
140
|
+
return { id, type, items: [""], ordered: false };
|
|
141
|
+
case "image":
|
|
142
|
+
return { id, type, src: "", alt: "", caption: "" };
|
|
143
|
+
case "embed":
|
|
144
|
+
return { id, type, embedUrl: "", embedHeight: 400 };
|
|
145
|
+
case "html":
|
|
146
|
+
return { id, type, rawHtml: "<div></div>" };
|
|
147
|
+
case "code":
|
|
148
|
+
return { id, type, html: "" };
|
|
149
|
+
case "separator":
|
|
150
|
+
return { id, type };
|
|
151
|
+
case "quote":
|
|
152
|
+
return { id, type, html: "" };
|
|
153
|
+
default:
|
|
154
|
+
return { id, type: "paragraph", html: "" };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function parseHeading(el) {
|
|
158
|
+
const level = parseInt(el.tagName[1], 10);
|
|
159
|
+
return { ...createBlock("heading"), level: level >= 1 && level <= 6 ? level : 2, html: el.innerHTML };
|
|
160
|
+
}
|
|
161
|
+
function parseList(el) {
|
|
162
|
+
const ordered = el.tagName.toLowerCase() === "ol";
|
|
163
|
+
const items = Array.from(el.querySelectorAll(":scope > li")).map((li) => li.innerHTML);
|
|
164
|
+
return { ...createBlock("list"), ordered, items: items.length ? items : [""] };
|
|
165
|
+
}
|
|
166
|
+
function parseImageDimensions(img) {
|
|
167
|
+
let imageWidth;
|
|
168
|
+
let imageHeight;
|
|
169
|
+
let imageWidthPercent;
|
|
170
|
+
const style = img.getAttribute("style") || "";
|
|
171
|
+
const wStyle = style.match(/width:\s*(\d+(?:\.\d+)?)(px|%)/i);
|
|
172
|
+
if (wStyle) {
|
|
173
|
+
if (wStyle[2] === "%") imageWidthPercent = parseFloat(wStyle[1]);
|
|
174
|
+
else imageWidth = parseFloat(wStyle[1]);
|
|
175
|
+
}
|
|
176
|
+
const hStyle = style.match(/height:\s*(\d+(?:\.\d+)?)px/i);
|
|
177
|
+
if (hStyle) imageHeight = parseFloat(hStyle[1]);
|
|
178
|
+
const wAttr = img.getAttribute("width");
|
|
179
|
+
const hAttr = img.getAttribute("height");
|
|
180
|
+
if (!imageWidth && !imageWidthPercent && wAttr) {
|
|
181
|
+
const w = parseInt(wAttr, 10);
|
|
182
|
+
if (Number.isFinite(w)) imageWidth = w;
|
|
183
|
+
}
|
|
184
|
+
if (!imageHeight && hAttr) {
|
|
185
|
+
const h = parseInt(hAttr, 10);
|
|
186
|
+
if (Number.isFinite(h)) imageHeight = h;
|
|
187
|
+
}
|
|
188
|
+
return { imageWidth, imageHeight, imageWidthPercent };
|
|
189
|
+
}
|
|
190
|
+
function imageStyleAttr(block) {
|
|
191
|
+
const parts = [];
|
|
192
|
+
if (block.imageWidthPercent) parts.push(`width: ${block.imageWidthPercent}%`);
|
|
193
|
+
else if (block.imageWidth) parts.push(`width: ${block.imageWidth}px`);
|
|
194
|
+
if (block.imageHeight) parts.push(`height: ${block.imageHeight}px`);
|
|
195
|
+
else if (block.imageWidthPercent || block.imageWidth) parts.push("height: auto");
|
|
196
|
+
return parts.length ? ` style="${parts.join("; ")}"` : "";
|
|
197
|
+
}
|
|
198
|
+
function parseEmbed(el) {
|
|
199
|
+
const iframe = el.tagName.toLowerCase() === "iframe" ? el : el.querySelector("iframe");
|
|
200
|
+
if (!iframe) return null;
|
|
201
|
+
const height = parseInt(iframe.getAttribute("height") || "400", 10);
|
|
202
|
+
return {
|
|
203
|
+
...createBlock("embed"),
|
|
204
|
+
embedUrl: iframe.getAttribute("src") || "",
|
|
205
|
+
embedHeight: Number.isFinite(height) ? height : 400,
|
|
206
|
+
rawHtml: el.tagName.toLowerCase() === "iframe" ? void 0 : el.outerHTML
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function htmlToBlocks(html) {
|
|
210
|
+
const trimmed = html?.trim();
|
|
211
|
+
if (!trimmed) return [createBlock("paragraph")];
|
|
212
|
+
if (typeof DOMParser === "undefined") return [createBlock("paragraph")];
|
|
213
|
+
const doc = new DOMParser().parseFromString(trimmed, "text/html");
|
|
214
|
+
const blocks = [];
|
|
215
|
+
for (const node of Array.from(doc.body.childNodes)) {
|
|
216
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
217
|
+
const text = node.textContent?.trim();
|
|
218
|
+
if (text) blocks.push({ ...createBlock("paragraph"), html: text });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
222
|
+
const el = node;
|
|
223
|
+
const tag = el.tagName.toLowerCase();
|
|
224
|
+
if (tag === "p") {
|
|
225
|
+
if (!el.textContent?.trim() && !el.querySelector("img")) continue;
|
|
226
|
+
blocks.push({ ...createBlock("paragraph"), html: el.innerHTML });
|
|
227
|
+
} else if (/^h[1-6]$/.test(tag)) {
|
|
228
|
+
blocks.push(parseHeading(el));
|
|
229
|
+
} else if (tag === "ul" || tag === "ol") {
|
|
230
|
+
blocks.push(parseList(el));
|
|
231
|
+
} else if (tag === "blockquote") {
|
|
232
|
+
blocks.push({ ...createBlock("quote"), html: el.innerHTML });
|
|
233
|
+
} else if (tag === "hr") {
|
|
234
|
+
blocks.push(createBlock("separator"));
|
|
235
|
+
} else if (tag === "pre") {
|
|
236
|
+
const code = el.querySelector("code");
|
|
237
|
+
blocks.push({ ...createBlock("code"), html: code?.textContent || el.textContent || "" });
|
|
238
|
+
} else if (tag === "figure") {
|
|
239
|
+
const img = el.querySelector("img");
|
|
240
|
+
const caption = el.querySelector("figcaption")?.textContent || "";
|
|
241
|
+
if (img) {
|
|
242
|
+
blocks.push({
|
|
243
|
+
...createBlock("image"),
|
|
244
|
+
src: img.getAttribute("src") || "",
|
|
245
|
+
alt: img.getAttribute("alt") || "",
|
|
246
|
+
caption,
|
|
247
|
+
...parseImageDimensions(img)
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
blocks.push({ ...createBlock("html"), rawHtml: el.outerHTML });
|
|
251
|
+
}
|
|
252
|
+
} else if (tag === "img") {
|
|
253
|
+
blocks.push({
|
|
254
|
+
...createBlock("image"),
|
|
255
|
+
src: el.getAttribute("src") || "",
|
|
256
|
+
alt: el.getAttribute("alt") || "",
|
|
257
|
+
...parseImageDimensions(el)
|
|
258
|
+
});
|
|
259
|
+
} else if (tag === "iframe" || el.querySelector("iframe")) {
|
|
260
|
+
const embed = parseEmbed(el);
|
|
261
|
+
blocks.push(embed || { ...createBlock("html"), rawHtml: el.outerHTML });
|
|
262
|
+
} else if (tag === "div" && el.classList.contains("embed-responsive")) {
|
|
263
|
+
const embed = parseEmbed(el);
|
|
264
|
+
blocks.push(embed || { ...createBlock("html"), rawHtml: el.outerHTML });
|
|
265
|
+
} else {
|
|
266
|
+
blocks.push({ ...createBlock("html"), rawHtml: el.outerHTML });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return blocks.length ? blocks : [createBlock("paragraph")];
|
|
270
|
+
}
|
|
271
|
+
function blocksToHtml(blocks) {
|
|
272
|
+
return blocks.map((block) => {
|
|
273
|
+
switch (block.type) {
|
|
274
|
+
case "paragraph":
|
|
275
|
+
return `<p>${block.html || ""}</p>`;
|
|
276
|
+
case "heading": {
|
|
277
|
+
const level = block.level && block.level >= 1 && block.level <= 6 ? block.level : 2;
|
|
278
|
+
return `<h${level}>${block.html || ""}</h${level}>`;
|
|
279
|
+
}
|
|
280
|
+
case "list": {
|
|
281
|
+
const tag = block.ordered ? "ol" : "ul";
|
|
282
|
+
const items = (block.items || []).filter((item) => item.trim());
|
|
283
|
+
if (!items.length) return "";
|
|
284
|
+
return `<${tag}>${items.map((item) => `<li>${item}</li>`).join("")}</${tag}>`;
|
|
285
|
+
}
|
|
286
|
+
case "quote":
|
|
287
|
+
return `<blockquote>${block.html || ""}</blockquote>`;
|
|
288
|
+
case "image":
|
|
289
|
+
if (!block.src) return "";
|
|
290
|
+
return `<figure><img src="${block.src}" alt="${block.alt || ""}"${imageStyleAttr(block)} />${block.caption ? `<figcaption>${block.caption}</figcaption>` : ""}</figure>`;
|
|
291
|
+
case "embed":
|
|
292
|
+
if (block.rawHtml?.trim()) return block.rawHtml;
|
|
293
|
+
if (!block.embedUrl) return "";
|
|
294
|
+
return `<div class="embed-responsive"><iframe src="${block.embedUrl}" width="100%" height="${block.embedHeight || 400}" frameborder="0" allowfullscreen loading="lazy" title="Embedded content"></iframe></div>`;
|
|
295
|
+
case "html":
|
|
296
|
+
return block.rawHtml || "";
|
|
297
|
+
case "code":
|
|
298
|
+
return `<pre><code>${(block.html || "").replace(/</g, "<").replace(/>/g, ">")}</code></pre>`;
|
|
299
|
+
case "separator":
|
|
300
|
+
return "<hr />";
|
|
301
|
+
default:
|
|
302
|
+
return "";
|
|
303
|
+
}
|
|
304
|
+
}).filter(Boolean).join("\n");
|
|
305
|
+
}
|
|
306
|
+
function slugifyTitle(title) {
|
|
307
|
+
return title.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "new-post";
|
|
308
|
+
}
|
|
309
|
+
var HEADING_CLASSES = {
|
|
310
|
+
1: "text-4xl md:text-5xl",
|
|
311
|
+
2: "text-3xl md:text-4xl",
|
|
312
|
+
3: "text-2xl md:text-3xl",
|
|
313
|
+
4: "text-xl md:text-2xl",
|
|
314
|
+
5: "text-lg md:text-xl",
|
|
315
|
+
6: "text-base md:text-lg"
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// src/lib/blog-seo.ts
|
|
319
|
+
function getSeoPreview(post, siteName = "Site", baseUrl = "https://example.com") {
|
|
320
|
+
const title = post.metaTitle?.trim() || post.title.trim() || "Untitled Post";
|
|
321
|
+
const description = post.metaDescription?.trim() || post.excerpt.trim() || "Add a meta description for better search visibility.";
|
|
322
|
+
const slug = post.slug && post.slug !== "new-post" ? post.slug : "your-post-slug";
|
|
323
|
+
const url = `${baseUrl.replace(/\/$/, "")}/blogs/${slug}`;
|
|
324
|
+
return { title, description, url, siteName };
|
|
325
|
+
}
|
|
326
|
+
function computeSeoScore(post, contentHtml = "") {
|
|
327
|
+
const plain = contentHtml.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
328
|
+
const wordCount = plain ? plain.split(" ").filter(Boolean).length : 0;
|
|
329
|
+
const title = post.metaTitle?.trim() || post.title.trim();
|
|
330
|
+
const description = post.metaDescription?.trim() || post.excerpt.trim();
|
|
331
|
+
const keyword = post.focusKeyword?.trim().toLowerCase() || "";
|
|
332
|
+
const slug = post.slug?.toLowerCase() || "";
|
|
333
|
+
const checks = [
|
|
334
|
+
{ id: "title", label: "SEO title is set", passed: title.length > 0 },
|
|
335
|
+
{
|
|
336
|
+
id: "title-length",
|
|
337
|
+
label: "SEO title length is good (30\u201360 chars)",
|
|
338
|
+
passed: title.length >= 30 && title.length <= 60
|
|
339
|
+
},
|
|
340
|
+
{ id: "description", label: "Meta description is set", passed: description.length > 0 },
|
|
341
|
+
{
|
|
342
|
+
id: "description-length",
|
|
343
|
+
label: "Meta description length is good (120\u2013160 chars)",
|
|
344
|
+
passed: description.length >= 120 && description.length <= 160
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "keyword-title",
|
|
348
|
+
label: "Focus keyword appears in SEO title",
|
|
349
|
+
passed: !keyword || title.toLowerCase().includes(keyword)
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: "keyword-slug",
|
|
353
|
+
label: "Focus keyword appears in URL slug",
|
|
354
|
+
passed: !keyword || slug.includes(keyword.replace(/\s+/g, "-"))
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
id: "keyword-content",
|
|
358
|
+
label: "Focus keyword appears in content",
|
|
359
|
+
passed: !keyword || plain.toLowerCase().includes(keyword)
|
|
360
|
+
},
|
|
361
|
+
{ id: "featured", label: "Featured image is set", passed: Boolean(post.featuredImage) },
|
|
362
|
+
{
|
|
363
|
+
id: "content-length",
|
|
364
|
+
label: "Content has enough words (300+)",
|
|
365
|
+
passed: wordCount >= 300
|
|
366
|
+
},
|
|
367
|
+
{ id: "tags", label: "At least one tag added", passed: post.tags.length > 0 }
|
|
368
|
+
];
|
|
369
|
+
const passed = checks.filter((c) => c.passed).length;
|
|
370
|
+
const score = Math.round(passed / checks.length * 100);
|
|
371
|
+
return { score, checks };
|
|
372
|
+
}
|
|
373
|
+
function titleLengthColor(len) {
|
|
374
|
+
if (len === 0) return "bg-gray-200";
|
|
375
|
+
if (len < 30 || len > 60) return "bg-amber-400";
|
|
376
|
+
return "bg-green-500";
|
|
377
|
+
}
|
|
378
|
+
function descriptionLengthColor(len) {
|
|
379
|
+
if (len === 0) return "bg-gray-200";
|
|
380
|
+
if (len < 120 || len > 160) return "bg-amber-400";
|
|
381
|
+
return "bg-green-500";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/components/BlockCanvas.tsx
|
|
385
|
+
var import_react3 = require("react");
|
|
386
|
+
var import_react_dom2 = require("react-dom");
|
|
387
|
+
var import_core = require("@dnd-kit/core");
|
|
388
|
+
var import_sortable = require("@dnd-kit/sortable");
|
|
389
|
+
var import_utilities = require("@dnd-kit/utilities");
|
|
390
|
+
var import_lucide_react2 = require("lucide-react");
|
|
391
|
+
|
|
392
|
+
// src/lib/paste-html.ts
|
|
393
|
+
function cleanPastedHtml(raw) {
|
|
394
|
+
if (!raw?.trim()) return "";
|
|
395
|
+
let html = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<\?xml[\s\S]*?\?>/gi, "").replace(/<meta[\s\S]*?>/gi, "").replace(/<link[\s\S]*?>/gi, "").replace(/<o:p>\s*<\/o:p>/gi, "").replace(/<\/?o:[^>]*>/gi, "").replace(/<\/?w:[^>]*>/gi, "").replace(/<\/?m:[^>]*>/gi, "");
|
|
396
|
+
if (typeof DOMParser === "undefined") return html;
|
|
397
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
398
|
+
flattenSingleWrapper(doc.body);
|
|
399
|
+
walkAndClean(doc.body);
|
|
400
|
+
return doc.body.innerHTML.trim();
|
|
401
|
+
}
|
|
402
|
+
function flattenSingleWrapper(root) {
|
|
403
|
+
while (root.childNodes.length === 1 && root.firstElementChild?.tagName === "DIV" && root.firstElementChild.childElementCount > 1) {
|
|
404
|
+
const div = root.firstElementChild;
|
|
405
|
+
while (div.firstChild) root.insertBefore(div.firstChild, div);
|
|
406
|
+
root.removeChild(div);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function wordParagraphHeadingTag(el) {
|
|
410
|
+
const style = el.getAttribute("style") || "";
|
|
411
|
+
const cls = el.getAttribute("class") || "";
|
|
412
|
+
const outline = style.match(/mso-outline-level:\s*(\d)/i);
|
|
413
|
+
if (outline) {
|
|
414
|
+
const n = Math.min(6, Math.max(1, parseInt(outline[1], 10)));
|
|
415
|
+
return `h${n}`;
|
|
416
|
+
}
|
|
417
|
+
if (/MsoTitle|Heading1/i.test(cls)) return "h1";
|
|
418
|
+
if (/Heading2/i.test(cls)) return "h2";
|
|
419
|
+
if (/Heading3/i.test(cls)) return "h3";
|
|
420
|
+
if (/Heading4/i.test(cls)) return "h4";
|
|
421
|
+
if (/Heading5/i.test(cls)) return "h5";
|
|
422
|
+
if (/Heading6/i.test(cls)) return "h6";
|
|
423
|
+
const fontSize = style.match(/font-size:\s*([\d.]+)pt/i);
|
|
424
|
+
if (fontSize) {
|
|
425
|
+
const pt = parseFloat(fontSize[1]);
|
|
426
|
+
if (pt >= 22) return "h1";
|
|
427
|
+
if (pt >= 18) return "h2";
|
|
428
|
+
if (pt >= 15) return "h3";
|
|
429
|
+
if (pt >= 13) return "h4";
|
|
430
|
+
}
|
|
431
|
+
const fw = style.match(/font-weight:\s*(bold|[7-9]00)/i);
|
|
432
|
+
if (fw && /Title/i.test(cls)) return "h1";
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
function wrapSemantic(el, tag) {
|
|
436
|
+
const wrapped = el.ownerDocument.createElement(tag);
|
|
437
|
+
wrapped.innerHTML = el.innerHTML;
|
|
438
|
+
el.replaceWith(wrapped);
|
|
439
|
+
return wrapped;
|
|
440
|
+
}
|
|
441
|
+
var KEEP_TAGS = /* @__PURE__ */ new Set([
|
|
442
|
+
"p",
|
|
443
|
+
"br",
|
|
444
|
+
"strong",
|
|
445
|
+
"b",
|
|
446
|
+
"em",
|
|
447
|
+
"i",
|
|
448
|
+
"u",
|
|
449
|
+
"s",
|
|
450
|
+
"strike",
|
|
451
|
+
"del",
|
|
452
|
+
"a",
|
|
453
|
+
"h1",
|
|
454
|
+
"h2",
|
|
455
|
+
"h3",
|
|
456
|
+
"h4",
|
|
457
|
+
"h5",
|
|
458
|
+
"h6",
|
|
459
|
+
"ul",
|
|
460
|
+
"ol",
|
|
461
|
+
"li",
|
|
462
|
+
"blockquote",
|
|
463
|
+
"pre",
|
|
464
|
+
"code",
|
|
465
|
+
"sub",
|
|
466
|
+
"sup",
|
|
467
|
+
"img",
|
|
468
|
+
"figure",
|
|
469
|
+
"figcaption",
|
|
470
|
+
"table",
|
|
471
|
+
"thead",
|
|
472
|
+
"tbody",
|
|
473
|
+
"tr",
|
|
474
|
+
"th",
|
|
475
|
+
"td",
|
|
476
|
+
"hr",
|
|
477
|
+
"iframe",
|
|
478
|
+
"div",
|
|
479
|
+
"span"
|
|
480
|
+
]);
|
|
481
|
+
function walkAndClean(parent) {
|
|
482
|
+
const nodes = Array.from(parent.childNodes);
|
|
483
|
+
for (const node of nodes) {
|
|
484
|
+
if (node.nodeType === Node.TEXT_NODE) continue;
|
|
485
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
486
|
+
node.remove();
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const el = node;
|
|
490
|
+
let tag = el.tagName.toLowerCase();
|
|
491
|
+
if (tag.includes(":")) {
|
|
492
|
+
unwrap(el);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (tag === "p") {
|
|
496
|
+
const heading = wordParagraphHeadingTag(el);
|
|
497
|
+
if (heading) {
|
|
498
|
+
const h = el.ownerDocument.createElement(heading);
|
|
499
|
+
h.innerHTML = el.innerHTML;
|
|
500
|
+
el.replaceWith(h);
|
|
501
|
+
walkAndClean(h);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (tag === "span") {
|
|
506
|
+
const style = el.getAttribute("style") || "";
|
|
507
|
+
let current = el;
|
|
508
|
+
if (/font-weight:\s*(bold|[7-9]00)/i.test(style)) {
|
|
509
|
+
current = wrapSemantic(current, "strong");
|
|
510
|
+
}
|
|
511
|
+
if (/font-style:\s*italic/i.test(style)) {
|
|
512
|
+
current = wrapSemantic(current, "em");
|
|
513
|
+
}
|
|
514
|
+
if (/text-decoration:\s*underline/i.test(style) || /text-decoration-line:\s*underline/i.test(style)) {
|
|
515
|
+
current = wrapSemantic(current, "u");
|
|
516
|
+
}
|
|
517
|
+
if (current === el) unwrap(el);
|
|
518
|
+
else walkAndClean(current);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (tag === "b") {
|
|
522
|
+
const strong = el.ownerDocument.createElement("strong");
|
|
523
|
+
strong.innerHTML = el.innerHTML;
|
|
524
|
+
el.replaceWith(strong);
|
|
525
|
+
walkAndClean(strong);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (tag === "i") {
|
|
529
|
+
const em = el.ownerDocument.createElement("em");
|
|
530
|
+
em.innerHTML = el.innerHTML;
|
|
531
|
+
el.replaceWith(em);
|
|
532
|
+
walkAndClean(em);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (tag === "div") {
|
|
536
|
+
if (el.querySelector("table, ul, ol, h1, h2, h3, h4, h5, h6, img, iframe, blockquote, pre")) {
|
|
537
|
+
unwrap(el);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const p = el.ownerDocument.createElement("p");
|
|
541
|
+
p.innerHTML = el.innerHTML;
|
|
542
|
+
el.replaceWith(p);
|
|
543
|
+
walkAndClean(p);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
el.removeAttribute("style");
|
|
547
|
+
el.removeAttribute("class");
|
|
548
|
+
el.removeAttribute("id");
|
|
549
|
+
el.removeAttribute("lang");
|
|
550
|
+
el.removeAttribute("align");
|
|
551
|
+
el.removeAttribute("valign");
|
|
552
|
+
if (tag === "a") {
|
|
553
|
+
const href = el.getAttribute("href");
|
|
554
|
+
[...el.attributes].forEach((a) => {
|
|
555
|
+
if (a.name !== "href") el.removeAttribute(a.name);
|
|
556
|
+
});
|
|
557
|
+
if (!href || href.startsWith("file:")) {
|
|
558
|
+
unwrap(el);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (tag === "img") {
|
|
563
|
+
const src = el.getAttribute("src") || "";
|
|
564
|
+
[...el.attributes].forEach((a) => el.removeAttribute(a.name));
|
|
565
|
+
if (src && (src.startsWith("http") || src.startsWith("data:image") || src.startsWith("/"))) {
|
|
566
|
+
el.setAttribute("src", src);
|
|
567
|
+
if (!el.getAttribute("alt")) el.setAttribute("alt", "");
|
|
568
|
+
} else {
|
|
569
|
+
el.remove();
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
tag = el.tagName.toLowerCase();
|
|
574
|
+
if (!KEEP_TAGS.has(tag)) {
|
|
575
|
+
unwrap(el);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
walkAndClean(el);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function unwrap(el) {
|
|
582
|
+
const parent = el.parentNode;
|
|
583
|
+
if (!parent) return;
|
|
584
|
+
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
585
|
+
parent.removeChild(el);
|
|
586
|
+
}
|
|
587
|
+
function insertHtmlAtCursor(html) {
|
|
588
|
+
if (!html) return;
|
|
589
|
+
document.execCommand("insertHTML", false, html);
|
|
590
|
+
}
|
|
591
|
+
function pastedHtmlToBlocks(raw) {
|
|
592
|
+
const cleaned = cleanPastedHtml(raw);
|
|
593
|
+
if (!cleaned) return [];
|
|
594
|
+
return htmlToBlocks(cleaned);
|
|
595
|
+
}
|
|
596
|
+
function isMultiBlockPaste(blocks) {
|
|
597
|
+
if (blocks.length > 1) return true;
|
|
598
|
+
const b = blocks[0];
|
|
599
|
+
if (!b) return false;
|
|
600
|
+
return ["list", "image", "embed", "html", "separator", "code"].includes(b.type);
|
|
601
|
+
}
|
|
602
|
+
function inlineHtmlFromBlocks(blocks) {
|
|
603
|
+
if (!blocks.length) return "";
|
|
604
|
+
const b = blocks[0];
|
|
605
|
+
if (b.type === "paragraph" || b.type === "heading" || b.type === "quote") return b.html || "";
|
|
606
|
+
if (b.type === "list") return (b.items || []).join("");
|
|
607
|
+
return "";
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/components/BlockToolbar.tsx
|
|
611
|
+
var import_react2 = require("react");
|
|
612
|
+
var import_react_dom = require("react-dom");
|
|
613
|
+
var import_lucide_react = require("lucide-react");
|
|
614
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
615
|
+
var TRANSFORM_OPTIONS = [
|
|
616
|
+
{ type: "paragraph", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Pilcrow, { size: 16 }) },
|
|
617
|
+
{ type: "heading", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Heading, { size: 16 }) },
|
|
618
|
+
{ type: "list", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.List, { size: 16 }), label: "Bullet list" },
|
|
619
|
+
{ type: "quote", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Quote, { size: 16 }) },
|
|
620
|
+
{ type: "image", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Image, { size: 16 }) },
|
|
621
|
+
{ type: "embed", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Video, { size: 16 }) },
|
|
622
|
+
{ type: "html", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.FileCode2, { size: 16 }) },
|
|
623
|
+
{ type: "code", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Code2, { size: 16 }) },
|
|
624
|
+
{ type: "separator", icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Minus, { size: 16 }) }
|
|
625
|
+
];
|
|
626
|
+
function FloatingMenu({
|
|
627
|
+
open,
|
|
628
|
+
onClose,
|
|
629
|
+
anchorRef,
|
|
630
|
+
children,
|
|
631
|
+
width = 200
|
|
632
|
+
}) {
|
|
633
|
+
const [pos, setPos] = (0, import_react2.useState)({ top: 0, left: 0 });
|
|
634
|
+
(0, import_react2.useEffect)(() => {
|
|
635
|
+
if (!open || !anchorRef.current) return;
|
|
636
|
+
const rect = anchorRef.current.getBoundingClientRect();
|
|
637
|
+
setPos({
|
|
638
|
+
top: rect.bottom + 6,
|
|
639
|
+
left: Math.min(rect.left, window.innerWidth - width - 12)
|
|
640
|
+
});
|
|
641
|
+
}, [open, anchorRef, width]);
|
|
642
|
+
if (!open || typeof document === "undefined") return null;
|
|
643
|
+
return (0, import_react_dom.createPortal)(
|
|
644
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
645
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "fixed inset-0 z-[200]", onClick: onClose, "aria-hidden": true }),
|
|
646
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
647
|
+
"div",
|
|
648
|
+
{
|
|
649
|
+
className: "fixed z-[201] bg-white text-gray-800 border border-gray-200 rounded-lg shadow-2xl py-2 max-h-[320px] overflow-y-auto",
|
|
650
|
+
style: { top: pos.top, left: pos.left, width },
|
|
651
|
+
children
|
|
652
|
+
}
|
|
653
|
+
)
|
|
654
|
+
] }),
|
|
655
|
+
document.body
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
function BlockToolbar({
|
|
659
|
+
block,
|
|
660
|
+
onTransform,
|
|
661
|
+
onSetHeadingLevel,
|
|
662
|
+
onToggleOrderedList,
|
|
663
|
+
onMoveUp,
|
|
664
|
+
onMoveDown,
|
|
665
|
+
onDelete,
|
|
666
|
+
onFormat,
|
|
667
|
+
canMoveUp,
|
|
668
|
+
canMoveDown,
|
|
669
|
+
dragHandleProps
|
|
670
|
+
}) {
|
|
671
|
+
const [showTransform, setShowTransform] = (0, import_react2.useState)(false);
|
|
672
|
+
const [showHeadings, setShowHeadings] = (0, import_react2.useState)(false);
|
|
673
|
+
const transformBtnRef = (0, import_react2.useRef)(null);
|
|
674
|
+
const headingBtnRef = (0, import_react2.useRef)(null);
|
|
675
|
+
const isTextBlock = block.type === "paragraph" || block.type === "heading" || block.type === "quote" || block.type === "code";
|
|
676
|
+
const currentLabel = TRANSFORM_OPTIONS.find((o) => o.type === block.type);
|
|
677
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
678
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "absolute -top-12 left-10 z-30 inline-flex w-max max-w-[min(100%,calc(100vw-3rem))] items-center gap-0.5 bg-[#1e1e1e] text-white rounded-md shadow-lg px-1 py-1 flex-wrap", children: [
|
|
679
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
680
|
+
"button",
|
|
681
|
+
{
|
|
682
|
+
type: "button",
|
|
683
|
+
className: "p-1.5 hover:bg-white/10 rounded cursor-grab shrink-0",
|
|
684
|
+
title: "Drag to reorder",
|
|
685
|
+
...dragHandleProps?.attributes,
|
|
686
|
+
...dragHandleProps?.listeners,
|
|
687
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.GripVertical, { size: 16 })
|
|
688
|
+
}
|
|
689
|
+
),
|
|
690
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
691
|
+
"button",
|
|
692
|
+
{
|
|
693
|
+
ref: transformBtnRef,
|
|
694
|
+
type: "button",
|
|
695
|
+
onClick: () => {
|
|
696
|
+
setShowHeadings(false);
|
|
697
|
+
setShowTransform((v) => !v);
|
|
698
|
+
},
|
|
699
|
+
className: "flex items-center gap-1.5 px-2 py-1.5 hover:bg-white/10 rounded text-sm shrink-0",
|
|
700
|
+
children: [
|
|
701
|
+
currentLabel?.icon,
|
|
702
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "hidden sm:inline text-xs", children: currentLabel?.label || BLOCK_LABELS[block.type] }),
|
|
703
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.ChevronDown, { size: 14 })
|
|
704
|
+
]
|
|
705
|
+
}
|
|
706
|
+
),
|
|
707
|
+
block.type === "heading" && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
708
|
+
"button",
|
|
709
|
+
{
|
|
710
|
+
ref: headingBtnRef,
|
|
711
|
+
type: "button",
|
|
712
|
+
onClick: () => {
|
|
713
|
+
setShowTransform(false);
|
|
714
|
+
setShowHeadings((v) => !v);
|
|
715
|
+
},
|
|
716
|
+
className: "flex items-center gap-1 px-2 py-1.5 hover:bg-white/10 rounded text-sm font-semibold shrink-0",
|
|
717
|
+
children: [
|
|
718
|
+
"H",
|
|
719
|
+
block.level || 2,
|
|
720
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.ChevronDown, { size: 14 })
|
|
721
|
+
]
|
|
722
|
+
}
|
|
723
|
+
),
|
|
724
|
+
block.type === "list" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
725
|
+
"button",
|
|
726
|
+
{
|
|
727
|
+
type: "button",
|
|
728
|
+
onClick: onToggleOrderedList,
|
|
729
|
+
className: "p-1.5 hover:bg-white/10 rounded shrink-0",
|
|
730
|
+
title: block.ordered ? "Bullet list" : "Numbered list",
|
|
731
|
+
children: block.ordered ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.List, { size: 15 }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.ListOrdered, { size: 15 })
|
|
732
|
+
}
|
|
733
|
+
),
|
|
734
|
+
isTextBlock && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
735
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "w-px h-5 bg-white/20 mx-0.5 shrink-0" }),
|
|
736
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => onFormat("bold"), className: "p-1.5 hover:bg-white/10 rounded shrink-0", title: "Bold", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Bold, { size: 15 }) }),
|
|
737
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => onFormat("italic"), className: "p-1.5 hover:bg-white/10 rounded shrink-0", title: "Italic", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Italic, { size: 15 }) }),
|
|
738
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => onFormat("underline"), className: "p-1.5 hover:bg-white/10 rounded shrink-0", title: "Underline", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Underline, { size: 15 }) }),
|
|
739
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => onFormat("link"), className: "p-1.5 hover:bg-white/10 rounded shrink-0", title: "Link", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Link2, { size: 15 }) })
|
|
740
|
+
] }),
|
|
741
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "w-px h-5 bg-white/20 mx-0.5 shrink-0" }),
|
|
742
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", disabled: !canMoveUp, onClick: onMoveUp, className: "p-1.5 hover:bg-white/10 rounded disabled:opacity-30 shrink-0", title: "Move up", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.ArrowUp, { size: 15 }) }),
|
|
743
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", disabled: !canMoveDown, onClick: onMoveDown, className: "p-1.5 hover:bg-white/10 rounded disabled:opacity-30 shrink-0", title: "Move down", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.ArrowDown, { size: 15 }) }),
|
|
744
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", onClick: onDelete, className: "p-1.5 hover:bg-red-500/80 rounded shrink-0", title: "Delete block", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.Trash2, { size: 15 }) })
|
|
745
|
+
] }),
|
|
746
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(FloatingMenu, { open: showTransform, onClose: () => setShowTransform(false), anchorRef: transformBtnRef, width: 220, children: [
|
|
747
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "px-3 pb-1 text-[11px] font-semibold text-gray-400 uppercase tracking-wide", children: "Transform to" }),
|
|
748
|
+
TRANSFORM_OPTIONS.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
749
|
+
"button",
|
|
750
|
+
{
|
|
751
|
+
type: "button",
|
|
752
|
+
onClick: () => {
|
|
753
|
+
onTransform(opt.type);
|
|
754
|
+
setShowTransform(false);
|
|
755
|
+
},
|
|
756
|
+
className: `w-full flex items-center gap-3 px-3 py-2 text-sm hover:bg-blue-50 ${block.type === opt.type ? "bg-blue-50 text-blue-700" : ""}`,
|
|
757
|
+
children: [
|
|
758
|
+
opt.icon,
|
|
759
|
+
opt.label || BLOCK_LABELS[opt.type]
|
|
760
|
+
]
|
|
761
|
+
},
|
|
762
|
+
opt.type
|
|
763
|
+
))
|
|
764
|
+
] }),
|
|
765
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FloatingMenu, { open: showHeadings, onClose: () => setShowHeadings(false), anchorRef: headingBtnRef, width: 160, children: [1, 2, 3, 4, 5, 6].map((level) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
766
|
+
"button",
|
|
767
|
+
{
|
|
768
|
+
type: "button",
|
|
769
|
+
onClick: () => {
|
|
770
|
+
onSetHeadingLevel(level);
|
|
771
|
+
setShowHeadings(false);
|
|
772
|
+
},
|
|
773
|
+
className: `w-full text-left px-3 py-2 text-sm hover:bg-blue-50 ${block.level === level ? "bg-blue-50 text-blue-700 font-semibold" : ""}`,
|
|
774
|
+
children: HEADING_LABELS[level]
|
|
775
|
+
},
|
|
776
|
+
level
|
|
777
|
+
)) })
|
|
778
|
+
] });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/components/BlockCanvas.tsx
|
|
782
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
783
|
+
var SLASH_OPTIONS = [
|
|
784
|
+
"paragraph",
|
|
785
|
+
"heading",
|
|
786
|
+
"list",
|
|
787
|
+
"quote",
|
|
788
|
+
"image",
|
|
789
|
+
"embed",
|
|
790
|
+
"html",
|
|
791
|
+
"code",
|
|
792
|
+
"separator"
|
|
793
|
+
];
|
|
794
|
+
function ListItem({
|
|
795
|
+
html,
|
|
796
|
+
onChange,
|
|
797
|
+
onFocus,
|
|
798
|
+
onEnter,
|
|
799
|
+
onBackspaceEmpty,
|
|
800
|
+
shouldFocus,
|
|
801
|
+
onPaste
|
|
802
|
+
}) {
|
|
803
|
+
const ref = (0, import_react3.useRef)(null);
|
|
804
|
+
(0, import_react3.useEffect)(() => {
|
|
805
|
+
if (ref.current && document.activeElement !== ref.current) {
|
|
806
|
+
ref.current.innerHTML = html || "";
|
|
807
|
+
}
|
|
808
|
+
}, [html]);
|
|
809
|
+
(0, import_react3.useEffect)(() => {
|
|
810
|
+
if (!shouldFocus || !ref.current) return;
|
|
811
|
+
ref.current.focus();
|
|
812
|
+
const range = document.createRange();
|
|
813
|
+
range.selectNodeContents(ref.current);
|
|
814
|
+
range.collapse(false);
|
|
815
|
+
const sel = window.getSelection();
|
|
816
|
+
sel?.removeAllRanges();
|
|
817
|
+
sel?.addRange(range);
|
|
818
|
+
}, [shouldFocus]);
|
|
819
|
+
const isEmpty = (el) => !(el.textContent || "").trim();
|
|
820
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
821
|
+
"div",
|
|
822
|
+
{
|
|
823
|
+
ref,
|
|
824
|
+
contentEditable: true,
|
|
825
|
+
suppressContentEditableWarning: true,
|
|
826
|
+
className: "outline-none text-[16px] text-gray-800 min-h-[24px]",
|
|
827
|
+
onFocus,
|
|
828
|
+
onBlur: (e) => onChange(e.currentTarget.innerHTML),
|
|
829
|
+
onPaste,
|
|
830
|
+
onKeyDown: (e) => {
|
|
831
|
+
const el = e.currentTarget;
|
|
832
|
+
if (e.key === "Enter") {
|
|
833
|
+
e.preventDefault();
|
|
834
|
+
onChange(el.innerHTML);
|
|
835
|
+
onEnter();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (e.key === "Backspace" && isEmpty(el)) {
|
|
839
|
+
e.preventDefault();
|
|
840
|
+
onChange("");
|
|
841
|
+
onBackspaceEmpty();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
) });
|
|
846
|
+
}
|
|
847
|
+
function RichBlock({
|
|
848
|
+
block,
|
|
849
|
+
placeholder,
|
|
850
|
+
className,
|
|
851
|
+
onChange,
|
|
852
|
+
onKeyDown,
|
|
853
|
+
onFocus,
|
|
854
|
+
onPaste
|
|
855
|
+
}) {
|
|
856
|
+
const ref = (0, import_react3.useRef)(null);
|
|
857
|
+
(0, import_react3.useEffect)(() => {
|
|
858
|
+
if (ref.current && document.activeElement !== ref.current) {
|
|
859
|
+
ref.current.innerHTML = block.html || "";
|
|
860
|
+
}
|
|
861
|
+
}, [block.id, block.html]);
|
|
862
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
863
|
+
"div",
|
|
864
|
+
{
|
|
865
|
+
ref,
|
|
866
|
+
contentEditable: true,
|
|
867
|
+
suppressContentEditableWarning: true,
|
|
868
|
+
"data-placeholder": placeholder,
|
|
869
|
+
className: `outline-none empty:before:content-[attr(data-placeholder)] empty:before:text-gray-400 ${className}`,
|
|
870
|
+
onFocus,
|
|
871
|
+
onBlur: (e) => onChange(e.currentTarget.innerHTML),
|
|
872
|
+
onPaste,
|
|
873
|
+
onKeyDown
|
|
874
|
+
}
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
function HtmlBlockEditor({
|
|
878
|
+
blockId,
|
|
879
|
+
rawHtml,
|
|
880
|
+
onChange,
|
|
881
|
+
onFocus
|
|
882
|
+
}) {
|
|
883
|
+
const previewRef = (0, import_react3.useRef)(null);
|
|
884
|
+
(0, import_react3.useEffect)(() => {
|
|
885
|
+
if (previewRef.current && document.activeElement !== previewRef.current) {
|
|
886
|
+
previewRef.current.innerHTML = rawHtml || "";
|
|
887
|
+
}
|
|
888
|
+
}, [blockId, rawHtml]);
|
|
889
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-3", children: [
|
|
890
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-gray-500", children: "Click content to edit directly \u2014 tables, divs, headings, links" }),
|
|
891
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
892
|
+
"div",
|
|
893
|
+
{
|
|
894
|
+
ref: previewRef,
|
|
895
|
+
contentEditable: true,
|
|
896
|
+
suppressContentEditableWarning: true,
|
|
897
|
+
onFocus,
|
|
898
|
+
onBlur: (e) => onChange(e.currentTarget.innerHTML),
|
|
899
|
+
onPaste: (e) => {
|
|
900
|
+
const html = e.clipboardData.getData("text/html");
|
|
901
|
+
const text = e.clipboardData.getData("text/plain");
|
|
902
|
+
if (!html && !text) return;
|
|
903
|
+
e.preventDefault();
|
|
904
|
+
const raw = html || text.replace(/\n/g, "<br>");
|
|
905
|
+
const cleaned = html ? cleanPastedHtml(raw) : raw.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
906
|
+
const pastedBlocks = pastedHtmlToBlocks(cleaned);
|
|
907
|
+
const inline = isMultiBlockPaste(pastedBlocks) ? pastedBlocks.map((b) => {
|
|
908
|
+
if (b.type === "paragraph" || b.type === "heading" || b.type === "quote") return `<p>${b.html || ""}</p>`;
|
|
909
|
+
if (b.type === "list") return `<ul>${(b.items || []).map((i) => `<li>${i}</li>`).join("")}</ul>`;
|
|
910
|
+
return inlineHtmlFromBlocks([b]);
|
|
911
|
+
}).join("") : inlineHtmlFromBlocks(pastedBlocks) || cleaned;
|
|
912
|
+
insertHtmlAtCursor(inline);
|
|
913
|
+
requestAnimationFrame(() => {
|
|
914
|
+
if (previewRef.current) onChange(previewRef.current.innerHTML);
|
|
915
|
+
});
|
|
916
|
+
},
|
|
917
|
+
className: "blog-content min-h-[160px] outline-none border border-gray-200 rounded-lg p-4 bg-white focus:ring-1 focus:ring-blue-400 empty:before:content-[attr(data-placeholder)] empty:before:text-gray-400",
|
|
918
|
+
"data-placeholder": "Click here to type or paste content..."
|
|
919
|
+
}
|
|
920
|
+
),
|
|
921
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("details", { className: "text-sm", children: [
|
|
922
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("summary", { className: "cursor-pointer text-gray-500 hover:text-gray-700 text-xs", children: "View / edit HTML source" }),
|
|
923
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
924
|
+
"textarea",
|
|
925
|
+
{
|
|
926
|
+
value: rawHtml || "",
|
|
927
|
+
onChange: (e) => onChange(e.target.value),
|
|
928
|
+
rows: 6,
|
|
929
|
+
className: "mt-2 w-full border border-gray-200 rounded px-3 py-2 text-xs font-mono bg-gray-50",
|
|
930
|
+
spellCheck: false
|
|
931
|
+
}
|
|
932
|
+
)
|
|
933
|
+
] })
|
|
934
|
+
] });
|
|
935
|
+
}
|
|
936
|
+
var IMAGE_SIZE_PRESETS = [
|
|
937
|
+
{ label: "Auto", value: "auto" },
|
|
938
|
+
{ label: "25%", value: 25 },
|
|
939
|
+
{ label: "50%", value: 50 },
|
|
940
|
+
{ label: "75%", value: 75 },
|
|
941
|
+
{ label: "100%", value: 100 }
|
|
942
|
+
];
|
|
943
|
+
function imageDisplayStyle(block) {
|
|
944
|
+
if (block.imageWidthPercent) return { width: `${block.imageWidthPercent}%`, height: block.imageHeight ? `${block.imageHeight}px` : "auto" };
|
|
945
|
+
if (block.imageWidth) return { width: `${block.imageWidth}px`, height: block.imageHeight ? `${block.imageHeight}px` : "auto" };
|
|
946
|
+
if (block.imageHeight) return { height: `${block.imageHeight}px`, width: "auto" };
|
|
947
|
+
return { maxWidth: "100%", height: "auto" };
|
|
948
|
+
}
|
|
949
|
+
function ImageBlockEditor({
|
|
950
|
+
block,
|
|
951
|
+
isActive,
|
|
952
|
+
onChange,
|
|
953
|
+
onFocus,
|
|
954
|
+
onUpload,
|
|
955
|
+
canUpload
|
|
956
|
+
}) {
|
|
957
|
+
const imgRef = (0, import_react3.useRef)(null);
|
|
958
|
+
const resizeRef = (0, import_react3.useRef)(null);
|
|
959
|
+
const activePreset = block.imageWidthPercent === 25 ? 25 : block.imageWidthPercent === 50 ? 50 : block.imageWidthPercent === 75 ? 75 : block.imageWidthPercent === 100 ? 100 : !block.imageWidth && !block.imageHeight && !block.imageWidthPercent ? "auto" : null;
|
|
960
|
+
const startResize = (e) => {
|
|
961
|
+
e.preventDefault();
|
|
962
|
+
e.stopPropagation();
|
|
963
|
+
const img = imgRef.current;
|
|
964
|
+
if (!img) return;
|
|
965
|
+
const rect = img.getBoundingClientRect();
|
|
966
|
+
resizeRef.current = {
|
|
967
|
+
startX: e.clientX,
|
|
968
|
+
startY: e.clientY,
|
|
969
|
+
startW: rect.width,
|
|
970
|
+
startH: rect.height,
|
|
971
|
+
aspect: rect.width / Math.max(rect.height, 1)
|
|
972
|
+
};
|
|
973
|
+
const onMove = (ev) => {
|
|
974
|
+
const data = resizeRef.current;
|
|
975
|
+
if (!data) return;
|
|
976
|
+
const dx = ev.clientX - data.startX;
|
|
977
|
+
const dy = ev.clientY - data.startY;
|
|
978
|
+
const newW = Math.max(80, Math.round(data.startW + dx));
|
|
979
|
+
const newH = Math.max(60, Math.round(data.startH + dy));
|
|
980
|
+
if (ev.shiftKey) {
|
|
981
|
+
onChange({ imageWidth: newW, imageHeight: newH, imageWidthPercent: void 0 });
|
|
982
|
+
} else {
|
|
983
|
+
onChange({ imageWidth: newW, imageHeight: void 0, imageWidthPercent: void 0 });
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
const onUp = () => {
|
|
987
|
+
resizeRef.current = null;
|
|
988
|
+
document.removeEventListener("mousemove", onMove);
|
|
989
|
+
document.removeEventListener("mouseup", onUp);
|
|
990
|
+
};
|
|
991
|
+
document.addEventListener("mousemove", onMove);
|
|
992
|
+
document.addEventListener("mouseup", onUp);
|
|
993
|
+
};
|
|
994
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-3", onFocus, children: [
|
|
995
|
+
block.src ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
996
|
+
"div",
|
|
997
|
+
{
|
|
998
|
+
className: `relative inline-block max-w-full ${isActive ? "ring-2 ring-blue-400 ring-offset-2 rounded-lg" : ""}`,
|
|
999
|
+
onClick: onFocus,
|
|
1000
|
+
children: [
|
|
1001
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1002
|
+
"img",
|
|
1003
|
+
{
|
|
1004
|
+
ref: imgRef,
|
|
1005
|
+
src: block.src,
|
|
1006
|
+
alt: block.alt || "",
|
|
1007
|
+
style: imageDisplayStyle(block),
|
|
1008
|
+
className: "rounded-lg border border-gray-200 block",
|
|
1009
|
+
draggable: false
|
|
1010
|
+
}
|
|
1011
|
+
),
|
|
1012
|
+
isActive && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1013
|
+
"button",
|
|
1014
|
+
{
|
|
1015
|
+
type: "button",
|
|
1016
|
+
"aria-label": "Resize image",
|
|
1017
|
+
onMouseDown: startResize,
|
|
1018
|
+
className: "absolute bottom-1 right-1 w-3.5 h-3.5 bg-white border-2 border-blue-500 rounded-sm cursor-se-resize shadow-sm hover:bg-blue-50"
|
|
1019
|
+
}
|
|
1020
|
+
)
|
|
1021
|
+
]
|
|
1022
|
+
}
|
|
1023
|
+
) : canUpload ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "flex flex-col items-center justify-center h-48 border-2 border-dashed border-gray-200 rounded-lg cursor-pointer be-hover-border-primary hover:bg-gray-50 transition-colors", children: [
|
|
1024
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-gray-500 text-sm", children: "Click to upload image" }),
|
|
1025
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1026
|
+
"input",
|
|
1027
|
+
{
|
|
1028
|
+
type: "file",
|
|
1029
|
+
accept: "image/*",
|
|
1030
|
+
className: "hidden",
|
|
1031
|
+
onChange: (e) => {
|
|
1032
|
+
const file = e.target.files?.[0];
|
|
1033
|
+
if (file) onUpload(file);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
)
|
|
1037
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center h-32 border-2 border-dashed border-gray-200 rounded-lg bg-gray-50 px-4 text-center", children: [
|
|
1038
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-gray-500 text-sm", children: "Paste an image URL below" }),
|
|
1039
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "text-gray-400 text-xs mt-1", children: [
|
|
1040
|
+
"Pass ",
|
|
1041
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "bg-gray-100 px-1 rounded", children: "onUploadImage" }),
|
|
1042
|
+
" to enable file upload"
|
|
1043
|
+
] })
|
|
1044
|
+
] }),
|
|
1045
|
+
block.src && isActive && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-wrap gap-1.5 items-center", children: [
|
|
1046
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-xs text-gray-500 mr-1", children: "Size:" }),
|
|
1047
|
+
IMAGE_SIZE_PRESETS.map((preset) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1048
|
+
"button",
|
|
1049
|
+
{
|
|
1050
|
+
type: "button",
|
|
1051
|
+
onClick: () => {
|
|
1052
|
+
if (preset.value === "auto") {
|
|
1053
|
+
onChange({ imageWidth: void 0, imageHeight: void 0, imageWidthPercent: void 0 });
|
|
1054
|
+
} else {
|
|
1055
|
+
onChange({ imageWidthPercent: preset.value, imageWidth: void 0, imageHeight: void 0 });
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
className: `px-2 py-0.5 text-xs rounded border ${activePreset === preset.value ? "bg-blue-600 text-white border-blue-600" : "bg-white text-gray-600 border-gray-200 hover:border-gray-400"}`,
|
|
1059
|
+
children: preset.label
|
|
1060
|
+
},
|
|
1061
|
+
preset.label
|
|
1062
|
+
))
|
|
1063
|
+
] }),
|
|
1064
|
+
block.src && isActive && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-wrap gap-3 items-center text-sm", children: [
|
|
1065
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
|
|
1066
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-xs shrink-0", children: "Width" }),
|
|
1067
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1068
|
+
"input",
|
|
1069
|
+
{
|
|
1070
|
+
type: "number",
|
|
1071
|
+
min: 40,
|
|
1072
|
+
max: 2e3,
|
|
1073
|
+
placeholder: "auto",
|
|
1074
|
+
value: block.imageWidthPercent ? "" : block.imageWidth ?? "",
|
|
1075
|
+
disabled: Boolean(block.imageWidthPercent),
|
|
1076
|
+
onChange: (e) => {
|
|
1077
|
+
const v = e.target.value;
|
|
1078
|
+
if (!v) onChange({ imageWidth: void 0 });
|
|
1079
|
+
else onChange({ imageWidth: parseInt(v, 10) || void 0, imageWidthPercent: void 0 });
|
|
1080
|
+
},
|
|
1081
|
+
className: "w-20 border border-gray-200 rounded px-2 py-1 text-xs disabled:bg-gray-50"
|
|
1082
|
+
}
|
|
1083
|
+
),
|
|
1084
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-xs text-gray-400", children: "px" })
|
|
1085
|
+
] }),
|
|
1086
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
|
|
1087
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-xs shrink-0", children: "Height" }),
|
|
1088
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1089
|
+
"input",
|
|
1090
|
+
{
|
|
1091
|
+
type: "number",
|
|
1092
|
+
min: 40,
|
|
1093
|
+
max: 2e3,
|
|
1094
|
+
placeholder: "auto",
|
|
1095
|
+
value: block.imageHeight ?? "",
|
|
1096
|
+
onChange: (e) => {
|
|
1097
|
+
const v = e.target.value;
|
|
1098
|
+
if (!v) onChange({ imageHeight: void 0 });
|
|
1099
|
+
else onChange({ imageHeight: parseInt(v, 10) || void 0, imageWidthPercent: void 0 });
|
|
1100
|
+
},
|
|
1101
|
+
className: "w-20 border border-gray-200 rounded px-2 py-1 text-xs"
|
|
1102
|
+
}
|
|
1103
|
+
),
|
|
1104
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-xs text-gray-400", children: "px" })
|
|
1105
|
+
] }),
|
|
1106
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[10px] text-gray-400", children: "Drag corner to resize \xB7 Shift+drag for custom height" })
|
|
1107
|
+
] }),
|
|
1108
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1109
|
+
"input",
|
|
1110
|
+
{
|
|
1111
|
+
type: "url",
|
|
1112
|
+
value: block.src || "",
|
|
1113
|
+
onChange: (e) => onChange({ src: e.target.value }),
|
|
1114
|
+
onFocus,
|
|
1115
|
+
placeholder: "Image URL (https://...)",
|
|
1116
|
+
className: "w-full border border-gray-200 rounded px-3 py-2 text-sm"
|
|
1117
|
+
}
|
|
1118
|
+
),
|
|
1119
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1120
|
+
"input",
|
|
1121
|
+
{
|
|
1122
|
+
type: "text",
|
|
1123
|
+
value: block.alt || "",
|
|
1124
|
+
onChange: (e) => onChange({ alt: e.target.value }),
|
|
1125
|
+
onFocus,
|
|
1126
|
+
placeholder: "Alt text (optional)",
|
|
1127
|
+
className: "w-full border border-gray-200 rounded px-3 py-2 text-sm"
|
|
1128
|
+
}
|
|
1129
|
+
),
|
|
1130
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1131
|
+
"input",
|
|
1132
|
+
{
|
|
1133
|
+
type: "text",
|
|
1134
|
+
value: block.caption || "",
|
|
1135
|
+
onChange: (e) => onChange({ caption: e.target.value }),
|
|
1136
|
+
onFocus,
|
|
1137
|
+
placeholder: "Caption (optional)",
|
|
1138
|
+
className: "w-full border border-gray-200 rounded px-3 py-2 text-sm"
|
|
1139
|
+
}
|
|
1140
|
+
)
|
|
1141
|
+
] });
|
|
1142
|
+
}
|
|
1143
|
+
function SortableBlockShell({
|
|
1144
|
+
id,
|
|
1145
|
+
children
|
|
1146
|
+
}) {
|
|
1147
|
+
const { attributes, listeners, setNodeRef, transform, transition } = (0, import_sortable.useSortable)({ id });
|
|
1148
|
+
const style = {
|
|
1149
|
+
transform: import_utilities.CSS.Transform.toString(transform),
|
|
1150
|
+
transition
|
|
1151
|
+
};
|
|
1152
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: children({ setNodeRef, style, dragHandleProps: { listeners, attributes } }) });
|
|
1153
|
+
}
|
|
1154
|
+
function BlockCanvas({
|
|
1155
|
+
blocks,
|
|
1156
|
+
onChange,
|
|
1157
|
+
activeId,
|
|
1158
|
+
onActiveIdChange
|
|
1159
|
+
}) {
|
|
1160
|
+
const { onUploadImage } = useEditorContext();
|
|
1161
|
+
const canUpload = Boolean(onUploadImage);
|
|
1162
|
+
const setActiveId = onActiveIdChange;
|
|
1163
|
+
const [slashMenu, setSlashMenu] = (0, import_react3.useState)(null);
|
|
1164
|
+
const [focusListItem, setFocusListItem] = (0, import_react3.useState)(null);
|
|
1165
|
+
const updateBlock = (id, patch) => {
|
|
1166
|
+
onChange(blocks.map((b) => b.id === id ? { ...b, ...patch } : b));
|
|
1167
|
+
};
|
|
1168
|
+
const handleListBackspace = (block, liIndex) => {
|
|
1169
|
+
const items = [...block.items || [""]];
|
|
1170
|
+
if (items.length > 1) {
|
|
1171
|
+
items.splice(liIndex, 1);
|
|
1172
|
+
updateBlock(block.id, { items });
|
|
1173
|
+
setFocusListItem({ blockId: block.id, index: Math.max(0, liIndex - 1) });
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const html = items[0] || "";
|
|
1177
|
+
onChange(
|
|
1178
|
+
blocks.map(
|
|
1179
|
+
(b, i) => b.id === block.id ? { ...createBlock("paragraph"), id: b.id, html } : b
|
|
1180
|
+
)
|
|
1181
|
+
);
|
|
1182
|
+
setActiveId(block.id);
|
|
1183
|
+
};
|
|
1184
|
+
(0, import_react3.useEffect)(() => {
|
|
1185
|
+
if (!focusListItem) return;
|
|
1186
|
+
const t = window.setTimeout(() => setFocusListItem(null), 50);
|
|
1187
|
+
return () => window.clearTimeout(t);
|
|
1188
|
+
}, [focusListItem, blocks]);
|
|
1189
|
+
const plainText = (b) => (b.html || b.rawHtml || "").replace(/<[^>]+>/g, "");
|
|
1190
|
+
const transformBlock = (id, type) => {
|
|
1191
|
+
onChange(
|
|
1192
|
+
blocks.map((b) => {
|
|
1193
|
+
if (b.id !== id) return b;
|
|
1194
|
+
const text = plainText(b);
|
|
1195
|
+
const next = createBlock(type);
|
|
1196
|
+
if (type === "heading") return { ...next, html: b.html || text, level: b.level || 2 };
|
|
1197
|
+
if (type === "list") return { ...next, items: [b.html || text || ""], ordered: b.ordered };
|
|
1198
|
+
if (type === "quote" || type === "paragraph") return { ...next, html: b.html || text };
|
|
1199
|
+
if (type === "code") return { ...next, html: text };
|
|
1200
|
+
if (type === "embed") return { ...next, embedUrl: b.embedUrl || b.src || "" };
|
|
1201
|
+
if (type === "image") return { ...next, src: b.src || "", alt: b.alt || "" };
|
|
1202
|
+
if (type === "html") return { ...next, rawHtml: b.rawHtml || b.html || "<div></div>" };
|
|
1203
|
+
return next;
|
|
1204
|
+
})
|
|
1205
|
+
);
|
|
1206
|
+
};
|
|
1207
|
+
const insertAfter = (index, type = "paragraph") => {
|
|
1208
|
+
const next = [...blocks];
|
|
1209
|
+
next.splice(index + 1, 0, createBlock(type));
|
|
1210
|
+
onChange(next);
|
|
1211
|
+
setActiveId(next[index + 1].id);
|
|
1212
|
+
};
|
|
1213
|
+
const removeBlock = (id) => {
|
|
1214
|
+
if (blocks.length <= 1) {
|
|
1215
|
+
onChange([createBlock("paragraph")]);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
onChange(blocks.filter((b) => b.id !== id));
|
|
1219
|
+
};
|
|
1220
|
+
const moveBlock = (index, dir) => {
|
|
1221
|
+
const target = index + dir;
|
|
1222
|
+
if (target < 0 || target >= blocks.length) return;
|
|
1223
|
+
const next = [...blocks];
|
|
1224
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
1225
|
+
onChange(next);
|
|
1226
|
+
};
|
|
1227
|
+
const applyFormat = (0, import_react3.useCallback)((cmd, value) => {
|
|
1228
|
+
if (cmd === "link") {
|
|
1229
|
+
const url = window.prompt("Enter URL");
|
|
1230
|
+
if (url) document.execCommand("createLink", false, url);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (cmd === "formatBlock" && value) {
|
|
1234
|
+
document.execCommand("formatBlock", false, value);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
document.execCommand(cmd, false);
|
|
1238
|
+
}, []);
|
|
1239
|
+
const handleTextKeyDown = (e, block, index) => {
|
|
1240
|
+
const el = e.currentTarget;
|
|
1241
|
+
const text = el.textContent || "";
|
|
1242
|
+
if (e.key === "/" && text === "") {
|
|
1243
|
+
e.preventDefault();
|
|
1244
|
+
const rect = el.getBoundingClientRect();
|
|
1245
|
+
setSlashMenu({
|
|
1246
|
+
blockId: block.id,
|
|
1247
|
+
index,
|
|
1248
|
+
top: rect.bottom + 6,
|
|
1249
|
+
left: rect.left
|
|
1250
|
+
});
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (e.key === "Enter" && !e.shiftKey && block.type === "paragraph") {
|
|
1254
|
+
e.preventDefault();
|
|
1255
|
+
updateBlock(block.id, { html: el.innerHTML });
|
|
1256
|
+
insertAfter(index);
|
|
1257
|
+
}
|
|
1258
|
+
if (e.key === "Backspace" && text === "" && blocks.length > 1) {
|
|
1259
|
+
e.preventDefault();
|
|
1260
|
+
removeBlock(block.id);
|
|
1261
|
+
const prev = blocks[index - 1];
|
|
1262
|
+
if (prev) setActiveId(prev.id);
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
const pickSlashOption = (type) => {
|
|
1266
|
+
if (!slashMenu) return;
|
|
1267
|
+
transformBlock(slashMenu.blockId, type);
|
|
1268
|
+
setSlashMenu(null);
|
|
1269
|
+
};
|
|
1270
|
+
const handleBlockPaste = (e, block, blockIndex, onChangeHtml) => {
|
|
1271
|
+
const html = e.clipboardData.getData("text/html");
|
|
1272
|
+
const text = e.clipboardData.getData("text/plain");
|
|
1273
|
+
if (!html && !text) return;
|
|
1274
|
+
e.preventDefault();
|
|
1275
|
+
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\n\n/g, "</p><p>").replace(/\n/g, "<br>");
|
|
1276
|
+
const raw = html || (text ? `<p>${escaped}</p>` : "");
|
|
1277
|
+
const pastedBlocks = pastedHtmlToBlocks(raw);
|
|
1278
|
+
if (!pastedBlocks.length) return;
|
|
1279
|
+
const el = e.currentTarget;
|
|
1280
|
+
const isEmpty = !(el.textContent || "").trim();
|
|
1281
|
+
if (isEmpty && pastedBlocks.length === 1 && pastedBlocks[0].type !== "paragraph") {
|
|
1282
|
+
updateBlock(block.id, { ...pastedBlocks[0], id: block.id });
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (isMultiBlockPaste(pastedBlocks)) {
|
|
1286
|
+
const next = [...blocks];
|
|
1287
|
+
const toInsert = pastedBlocks.map((b, i) => i === 0 && isEmpty ? { ...b, id: block.id } : b);
|
|
1288
|
+
if (isEmpty) next.splice(blockIndex, 1, ...toInsert);
|
|
1289
|
+
else next.splice(blockIndex + 1, 0, ...pastedBlocks);
|
|
1290
|
+
onChange(next);
|
|
1291
|
+
setActiveId(toInsert[toInsert.length - 1].id);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const inline = inlineHtmlFromBlocks(pastedBlocks);
|
|
1295
|
+
if (!inline) return;
|
|
1296
|
+
insertHtmlAtCursor(inline);
|
|
1297
|
+
requestAnimationFrame(() => onChangeHtml(el.innerHTML));
|
|
1298
|
+
};
|
|
1299
|
+
const handleListPaste = (e, onChangeHtml) => {
|
|
1300
|
+
const html = e.clipboardData.getData("text/html");
|
|
1301
|
+
const text = e.clipboardData.getData("text/plain");
|
|
1302
|
+
if (!html && !text) return;
|
|
1303
|
+
e.preventDefault();
|
|
1304
|
+
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1305
|
+
const raw = html || (text ? `<span>${escaped}</span>` : "");
|
|
1306
|
+
const pastedBlocks = pastedHtmlToBlocks(raw);
|
|
1307
|
+
const inline = inlineHtmlFromBlocks(pastedBlocks) || escaped;
|
|
1308
|
+
insertHtmlAtCursor(inline);
|
|
1309
|
+
requestAnimationFrame(() => onChangeHtml(e.currentTarget.innerHTML));
|
|
1310
|
+
};
|
|
1311
|
+
const uploadImage = async (blockId, file) => {
|
|
1312
|
+
if (!onUploadImage) return;
|
|
1313
|
+
const url = await onUploadImage(file);
|
|
1314
|
+
if (url) updateBlock(blockId, { src: url });
|
|
1315
|
+
};
|
|
1316
|
+
const { setNodeRef: setDropRef, isOver } = (0, import_core.useDroppable)({ id: "canvas-drop" });
|
|
1317
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1318
|
+
"div",
|
|
1319
|
+
{
|
|
1320
|
+
ref: setDropRef,
|
|
1321
|
+
className: `max-w-[840px] mx-auto px-6 py-8 min-h-[240px] rounded-xl transition-colors ${isOver ? "bg-green-50/50 ring-2 ring-dashed ring-[color-mix(in_srgb,var(--be-primary)_25%,transparent)]" : ""}`,
|
|
1322
|
+
children: [
|
|
1323
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sortable.SortableContext, { items: blocks.map((b) => b.id), strategy: import_sortable.verticalListSortingStrategy, children: blocks.map((block, index) => {
|
|
1324
|
+
const isActive = activeId === block.id;
|
|
1325
|
+
const level = block.level || 2;
|
|
1326
|
+
const ListWrapper = block.ordered ? "ol" : "ul";
|
|
1327
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SortableBlockShell, { id: block.id, children: ({ setNodeRef, style, dragHandleProps }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1328
|
+
"div",
|
|
1329
|
+
{
|
|
1330
|
+
ref: setNodeRef,
|
|
1331
|
+
style,
|
|
1332
|
+
className: `relative group mb-4 pt-2 ${isActive ? "ring-1 ring-blue-400 rounded-sm z-10" : ""}`,
|
|
1333
|
+
onClick: () => setActiveId(block.id),
|
|
1334
|
+
children: [
|
|
1335
|
+
isActive && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1336
|
+
BlockToolbar,
|
|
1337
|
+
{
|
|
1338
|
+
block,
|
|
1339
|
+
dragHandleProps,
|
|
1340
|
+
onTransform: (type) => transformBlock(block.id, type),
|
|
1341
|
+
onSetHeadingLevel: (lvl) => updateBlock(block.id, { level: lvl }),
|
|
1342
|
+
onToggleOrderedList: () => updateBlock(block.id, { ordered: !block.ordered }),
|
|
1343
|
+
onMoveUp: () => moveBlock(index, -1),
|
|
1344
|
+
onMoveDown: () => moveBlock(index, 1),
|
|
1345
|
+
onDelete: () => removeBlock(block.id),
|
|
1346
|
+
onFormat: applyFormat,
|
|
1347
|
+
canMoveUp: index > 0,
|
|
1348
|
+
canMoveDown: index < blocks.length - 1
|
|
1349
|
+
}
|
|
1350
|
+
),
|
|
1351
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex gap-2 items-start", children: [
|
|
1352
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1353
|
+
"button",
|
|
1354
|
+
{
|
|
1355
|
+
type: "button",
|
|
1356
|
+
onClick: () => insertAfter(index),
|
|
1357
|
+
className: "opacity-0 group-hover:opacity-100 mt-2 p-1 rounded bg-[#1e1e1e] text-white shrink-0 transition-opacity",
|
|
1358
|
+
title: "Add block",
|
|
1359
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Plus, { size: 16 })
|
|
1360
|
+
}
|
|
1361
|
+
),
|
|
1362
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex-1 min-w-0 py-1", children: [
|
|
1363
|
+
block.type === "paragraph" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1364
|
+
RichBlock,
|
|
1365
|
+
{
|
|
1366
|
+
block,
|
|
1367
|
+
placeholder: "Type / to choose a block \u2014 paste from Word to keep bold, headings & lists",
|
|
1368
|
+
className: "text-[16px] leading-relaxed text-gray-800 min-h-[28px]",
|
|
1369
|
+
onChange: (html) => updateBlock(block.id, { html }),
|
|
1370
|
+
onFocus: () => setActiveId(block.id),
|
|
1371
|
+
onKeyDown: (e) => handleTextKeyDown(e, block, index),
|
|
1372
|
+
onPaste: (e) => handleBlockPaste(e, block, index, (html) => updateBlock(block.id, { html }))
|
|
1373
|
+
}
|
|
1374
|
+
),
|
|
1375
|
+
block.type === "heading" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1376
|
+
RichBlock,
|
|
1377
|
+
{
|
|
1378
|
+
block,
|
|
1379
|
+
placeholder: BLOCK_LABELS.heading,
|
|
1380
|
+
className: `font-bold text-gray-900 min-h-[36px] ${HEADING_CLASSES[level]}`,
|
|
1381
|
+
onChange: (html) => updateBlock(block.id, { html }),
|
|
1382
|
+
onFocus: () => setActiveId(block.id),
|
|
1383
|
+
onKeyDown: (e) => handleTextKeyDown(e, block, index),
|
|
1384
|
+
onPaste: (e) => handleBlockPaste(e, block, index, (html) => updateBlock(block.id, { html }))
|
|
1385
|
+
}
|
|
1386
|
+
),
|
|
1387
|
+
block.type === "quote" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "border-l-4 border-gray-300 pl-4", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1388
|
+
RichBlock,
|
|
1389
|
+
{
|
|
1390
|
+
block,
|
|
1391
|
+
placeholder: "Quote",
|
|
1392
|
+
className: "text-lg italic text-gray-600 min-h-[28px]",
|
|
1393
|
+
onChange: (html) => updateBlock(block.id, { html }),
|
|
1394
|
+
onFocus: () => setActiveId(block.id),
|
|
1395
|
+
onKeyDown: (e) => handleTextKeyDown(e, block, index),
|
|
1396
|
+
onPaste: (e) => handleBlockPaste(e, block, index, (html) => updateBlock(block.id, { html }))
|
|
1397
|
+
}
|
|
1398
|
+
) }),
|
|
1399
|
+
block.type === "list" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(ListWrapper, { className: `${block.ordered ? "list-decimal" : "list-disc"} pl-6 space-y-2`, children: [
|
|
1400
|
+
(block.items || [""]).map((item, liIndex) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1401
|
+
ListItem,
|
|
1402
|
+
{
|
|
1403
|
+
html: item,
|
|
1404
|
+
shouldFocus: focusListItem?.blockId === block.id && focusListItem.index === liIndex,
|
|
1405
|
+
onChange: (html) => {
|
|
1406
|
+
const items = [...block.items || [""]];
|
|
1407
|
+
items[liIndex] = html;
|
|
1408
|
+
updateBlock(block.id, { items });
|
|
1409
|
+
},
|
|
1410
|
+
onFocus: () => setActiveId(block.id),
|
|
1411
|
+
onEnter: () => {
|
|
1412
|
+
const items = [...block.items || [""]];
|
|
1413
|
+
items.splice(liIndex + 1, 0, "");
|
|
1414
|
+
updateBlock(block.id, { items });
|
|
1415
|
+
setFocusListItem({ blockId: block.id, index: liIndex + 1 });
|
|
1416
|
+
},
|
|
1417
|
+
onBackspaceEmpty: () => handleListBackspace(block, liIndex),
|
|
1418
|
+
onPaste: (e) => handleListPaste(e, (html) => {
|
|
1419
|
+
const items = [...block.items || [""]];
|
|
1420
|
+
items[liIndex] = html;
|
|
1421
|
+
updateBlock(block.id, { items });
|
|
1422
|
+
})
|
|
1423
|
+
},
|
|
1424
|
+
`${block.id}-${liIndex}`
|
|
1425
|
+
)),
|
|
1426
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1427
|
+
"button",
|
|
1428
|
+
{
|
|
1429
|
+
type: "button",
|
|
1430
|
+
onClick: () => updateBlock(block.id, { items: [...block.items || [], ""] }),
|
|
1431
|
+
className: "text-sm text-blue-600 hover:underline",
|
|
1432
|
+
children: "+ Add list item"
|
|
1433
|
+
}
|
|
1434
|
+
) })
|
|
1435
|
+
] }),
|
|
1436
|
+
block.type === "image" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1437
|
+
ImageBlockEditor,
|
|
1438
|
+
{
|
|
1439
|
+
block,
|
|
1440
|
+
isActive: activeId === block.id,
|
|
1441
|
+
onChange: (patch) => updateBlock(block.id, patch),
|
|
1442
|
+
onFocus: () => setActiveId(block.id),
|
|
1443
|
+
onUpload: (file) => uploadImage(block.id, file),
|
|
1444
|
+
canUpload
|
|
1445
|
+
}
|
|
1446
|
+
),
|
|
1447
|
+
block.type === "embed" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-3", children: [
|
|
1448
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1449
|
+
"input",
|
|
1450
|
+
{
|
|
1451
|
+
type: "url",
|
|
1452
|
+
value: block.embedUrl || "",
|
|
1453
|
+
onChange: (e) => updateBlock(block.id, { embedUrl: e.target.value }),
|
|
1454
|
+
placeholder: "Embed URL (YouTube, Vimeo, Google Maps, etc.)",
|
|
1455
|
+
className: "w-full border border-gray-200 rounded px-3 py-2 text-sm"
|
|
1456
|
+
}
|
|
1457
|
+
),
|
|
1458
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex gap-3 items-center", children: [
|
|
1459
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "text-sm text-gray-500 shrink-0", children: "Height" }),
|
|
1460
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1461
|
+
"input",
|
|
1462
|
+
{
|
|
1463
|
+
type: "number",
|
|
1464
|
+
min: 200,
|
|
1465
|
+
max: 1200,
|
|
1466
|
+
value: block.embedHeight || 400,
|
|
1467
|
+
onChange: (e) => updateBlock(block.id, { embedHeight: parseInt(e.target.value, 10) || 400 }),
|
|
1468
|
+
className: "w-24 border border-gray-200 rounded px-3 py-2 text-sm"
|
|
1469
|
+
}
|
|
1470
|
+
),
|
|
1471
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-xs text-gray-400", children: "px" })
|
|
1472
|
+
] }),
|
|
1473
|
+
block.embedUrl ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "rounded-lg overflow-hidden border border-gray-200 bg-gray-50", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1474
|
+
"iframe",
|
|
1475
|
+
{
|
|
1476
|
+
src: block.embedUrl,
|
|
1477
|
+
width: "100%",
|
|
1478
|
+
height: block.embedHeight || 400,
|
|
1479
|
+
className: "border-0",
|
|
1480
|
+
title: "Embed preview",
|
|
1481
|
+
loading: "lazy"
|
|
1482
|
+
}
|
|
1483
|
+
) }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-gray-400", children: "Paste iframe src URL \u2014 YouTube, Vimeo, Loom, maps, etc." }),
|
|
1484
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("details", { className: "text-sm", children: [
|
|
1485
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("summary", { className: "cursor-pointer text-gray-500 hover:text-gray-700", children: "Advanced: raw iframe HTML" }),
|
|
1486
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1487
|
+
"textarea",
|
|
1488
|
+
{
|
|
1489
|
+
value: block.rawHtml || "",
|
|
1490
|
+
onChange: (e) => updateBlock(block.id, { rawHtml: e.target.value }),
|
|
1491
|
+
rows: 4,
|
|
1492
|
+
placeholder: '<iframe src="..." width="100%" height="400"></iframe>',
|
|
1493
|
+
className: "mt-2 w-full border border-gray-200 rounded px-3 py-2 text-xs font-mono"
|
|
1494
|
+
}
|
|
1495
|
+
)
|
|
1496
|
+
] })
|
|
1497
|
+
] }),
|
|
1498
|
+
block.type === "html" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1499
|
+
HtmlBlockEditor,
|
|
1500
|
+
{
|
|
1501
|
+
blockId: block.id,
|
|
1502
|
+
rawHtml: block.rawHtml || "",
|
|
1503
|
+
onChange: (html) => updateBlock(block.id, { rawHtml: html }),
|
|
1504
|
+
onFocus: () => setActiveId(block.id)
|
|
1505
|
+
}
|
|
1506
|
+
),
|
|
1507
|
+
block.type === "code" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1508
|
+
"textarea",
|
|
1509
|
+
{
|
|
1510
|
+
value: block.html || "",
|
|
1511
|
+
onChange: (e) => updateBlock(block.id, { html: e.target.value }),
|
|
1512
|
+
rows: 6,
|
|
1513
|
+
placeholder: "Paste code here...",
|
|
1514
|
+
className: "w-full border border-gray-200 rounded-lg px-4 py-3 text-sm font-mono bg-[#1e1e1e] text-green-400",
|
|
1515
|
+
spellCheck: false,
|
|
1516
|
+
onFocus: () => setActiveId(block.id)
|
|
1517
|
+
}
|
|
1518
|
+
),
|
|
1519
|
+
block.type === "separator" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("hr", { className: "border-gray-300 my-4" })
|
|
1520
|
+
] })
|
|
1521
|
+
] }),
|
|
1522
|
+
slashMenu?.blockId === block.id && typeof document !== "undefined" && (0, import_react_dom2.createPortal)(
|
|
1523
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
1524
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fixed inset-0 z-[200]", onClick: () => setSlashMenu(null) }),
|
|
1525
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1526
|
+
"div",
|
|
1527
|
+
{
|
|
1528
|
+
className: "fixed z-[201] bg-white border border-gray-200 rounded-lg shadow-2xl py-2 w-[220px] max-h-[280px] overflow-y-auto",
|
|
1529
|
+
style: { top: slashMenu.top, left: slashMenu.left },
|
|
1530
|
+
children: [
|
|
1531
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "px-3 pb-1 text-[11px] font-semibold text-gray-400 uppercase", children: "Blocks" }),
|
|
1532
|
+
SLASH_OPTIONS.map((type) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1533
|
+
"button",
|
|
1534
|
+
{
|
|
1535
|
+
type: "button",
|
|
1536
|
+
onClick: () => pickSlashOption(type),
|
|
1537
|
+
className: "w-full text-left px-3 py-2 text-sm hover:bg-blue-50",
|
|
1538
|
+
children: BLOCK_LABELS[type]
|
|
1539
|
+
},
|
|
1540
|
+
type
|
|
1541
|
+
))
|
|
1542
|
+
]
|
|
1543
|
+
}
|
|
1544
|
+
)
|
|
1545
|
+
] }),
|
|
1546
|
+
document.body
|
|
1547
|
+
)
|
|
1548
|
+
]
|
|
1549
|
+
}
|
|
1550
|
+
) }, block.id);
|
|
1551
|
+
}) }),
|
|
1552
|
+
blocks.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-center text-gray-400 text-sm py-12", children: "Drag blocks from the Block panel \u2192 or type / to add" })
|
|
1553
|
+
]
|
|
1554
|
+
}
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/components/PostSidebar.tsx
|
|
1559
|
+
var import_react4 = require("react");
|
|
1560
|
+
var import_lucide_react5 = require("lucide-react");
|
|
1561
|
+
|
|
1562
|
+
// src/components/BlockLibrary.tsx
|
|
1563
|
+
var import_core2 = require("@dnd-kit/core");
|
|
1564
|
+
var import_lucide_react3 = require("lucide-react");
|
|
1565
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1566
|
+
var BLOCK_LIBRARY = [
|
|
1567
|
+
{ type: "paragraph", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Pilcrow, { size: 18 }), desc: "Start with plain text" },
|
|
1568
|
+
{ type: "heading", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Heading, { size: 18 }), desc: "H1\u2013H6 section title" },
|
|
1569
|
+
{ type: "list", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.List, { size: 18 }), desc: "Bullet or numbered list" },
|
|
1570
|
+
{ type: "quote", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Quote, { size: 18 }), desc: "Highlighted quote" },
|
|
1571
|
+
{ type: "image", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Image, { size: 18 }), desc: "Upload or URL image" },
|
|
1572
|
+
{ type: "embed", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Video, { size: 18 }), desc: "YouTube, iframe embed" },
|
|
1573
|
+
{ type: "html", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.FileCode2, { size: 18 }), desc: "Custom HTML block" },
|
|
1574
|
+
{ type: "code", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Code2, { size: 18 }), desc: "Code snippet" },
|
|
1575
|
+
{ type: "separator", icon: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Minus, { size: 18 }), desc: "Horizontal divider line" }
|
|
1576
|
+
];
|
|
1577
|
+
function DraggableBlockItem({
|
|
1578
|
+
type,
|
|
1579
|
+
icon,
|
|
1580
|
+
label,
|
|
1581
|
+
desc,
|
|
1582
|
+
onClick
|
|
1583
|
+
}) {
|
|
1584
|
+
const { attributes, listeners, setNodeRef, isDragging } = (0, import_core2.useDraggable)({
|
|
1585
|
+
id: `library-${type}`,
|
|
1586
|
+
data: { source: "library", type }
|
|
1587
|
+
});
|
|
1588
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1589
|
+
"button",
|
|
1590
|
+
{
|
|
1591
|
+
ref: setNodeRef,
|
|
1592
|
+
type: "button",
|
|
1593
|
+
onClick,
|
|
1594
|
+
className: `w-full flex items-start gap-3 p-3 rounded-lg border border-gray-200 bg-white be-hover-border-primary hover:bg-green-50/40 text-left transition-colors cursor-grab active:cursor-grabbing ${isDragging ? "opacity-40" : ""}`,
|
|
1595
|
+
...listeners,
|
|
1596
|
+
...attributes,
|
|
1597
|
+
children: [
|
|
1598
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "mt-0.5 text-gray-600 shrink-0", children: icon }),
|
|
1599
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "min-w-0", children: [
|
|
1600
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "block text-sm font-medium text-gray-900", children: label }),
|
|
1601
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "block text-[11px] text-gray-500 mt-0.5 leading-snug", children: desc })
|
|
1602
|
+
] })
|
|
1603
|
+
]
|
|
1604
|
+
}
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
function BlockLibrary({ onAddBlock }) {
|
|
1608
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "space-y-2", children: [
|
|
1609
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-xs text-gray-500 px-1", children: "Drag into editor or click to add at the end." }),
|
|
1610
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "space-y-2", children: BLOCK_LIBRARY.map((item) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1611
|
+
DraggableBlockItem,
|
|
1612
|
+
{
|
|
1613
|
+
type: item.type,
|
|
1614
|
+
icon: item.icon,
|
|
1615
|
+
label: BLOCK_LABELS[item.type],
|
|
1616
|
+
desc: item.desc,
|
|
1617
|
+
onClick: () => onAddBlock(item.type)
|
|
1618
|
+
},
|
|
1619
|
+
item.type
|
|
1620
|
+
)) })
|
|
1621
|
+
] });
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// src/components/SeoPanel.tsx
|
|
1625
|
+
var import_lucide_react4 = require("lucide-react");
|
|
1626
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1627
|
+
function SeoPanel({
|
|
1628
|
+
post,
|
|
1629
|
+
contentHtml,
|
|
1630
|
+
onChange,
|
|
1631
|
+
onClose
|
|
1632
|
+
}) {
|
|
1633
|
+
const { siteName, baseUrl } = useEditorContext();
|
|
1634
|
+
const seo = computeSeoScore(post, contentHtml);
|
|
1635
|
+
const preview = getSeoPreview(post, siteName, baseUrl);
|
|
1636
|
+
const titleLen = (post.metaTitle || post.title).length;
|
|
1637
|
+
const descLen = (post.metaDescription || post.excerpt).length;
|
|
1638
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col h-full", children: [
|
|
1639
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "sticky top-0 bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between z-10 shrink-0", children: [
|
|
1640
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "min-w-0", children: [
|
|
1641
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h2", { className: "text-sm font-semibold text-gray-900", children: "SEO Settings" }),
|
|
1642
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[11px] text-gray-500 truncate", children: "Search preview & meta" })
|
|
1643
|
+
] }),
|
|
1644
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
1645
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1646
|
+
"span",
|
|
1647
|
+
{
|
|
1648
|
+
className: `text-xs font-bold px-2 py-0.5 rounded-full ${seo.score >= 70 ? "bg-green-100 text-green-700" : seo.score >= 40 ? "bg-amber-100 text-amber-700" : "bg-red-100 text-red-700"}`,
|
|
1649
|
+
children: [
|
|
1650
|
+
seo.score,
|
|
1651
|
+
"/100"
|
|
1652
|
+
]
|
|
1653
|
+
}
|
|
1654
|
+
),
|
|
1655
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: onClose, className: "p-1.5 hover:bg-gray-100 rounded-lg text-gray-500", "aria-label": "Close SEO panel", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react4.X, { size: 18 }) })
|
|
1656
|
+
] })
|
|
1657
|
+
] }),
|
|
1658
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
|
|
1659
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
|
|
1660
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[11px] font-semibold text-gray-500 uppercase tracking-wide mb-2", children: "Google Preview" }),
|
|
1661
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "rounded-lg border border-gray-200 p-3 bg-gray-50 space-y-1", children: [
|
|
1662
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[#1a0dab] text-sm font-medium leading-snug line-clamp-2", children: preview.title }),
|
|
1663
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-[#006621] text-xs truncate", children: preview.url }),
|
|
1664
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-gray-600 text-xs leading-relaxed line-clamp-4", children: preview.description })
|
|
1665
|
+
] })
|
|
1666
|
+
] }),
|
|
1667
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
|
|
1668
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "text-xs font-medium text-gray-700", children: "SEO Title" }),
|
|
1669
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1670
|
+
"input",
|
|
1671
|
+
{
|
|
1672
|
+
value: post.metaTitle || "",
|
|
1673
|
+
onChange: (e) => onChange({ metaTitle: e.target.value }),
|
|
1674
|
+
placeholder: post.title || "Defaults to post title",
|
|
1675
|
+
className: "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 be-ring-primary"
|
|
1676
|
+
}
|
|
1677
|
+
),
|
|
1678
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-1.5 rounded-full bg-gray-100 overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: `h-full transition-all ${titleLengthColor(titleLen)}`, style: { width: `${Math.min(100, titleLen / 60 * 100)}%` } }) }),
|
|
1679
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "text-[10px] text-gray-400", children: [
|
|
1680
|
+
titleLen,
|
|
1681
|
+
" / 60 chars"
|
|
1682
|
+
] })
|
|
1683
|
+
] }),
|
|
1684
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
|
|
1685
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "text-xs font-medium text-gray-700", children: "Meta Description" }),
|
|
1686
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1687
|
+
"textarea",
|
|
1688
|
+
{
|
|
1689
|
+
value: post.metaDescription || "",
|
|
1690
|
+
onChange: (e) => onChange({ metaDescription: e.target.value }),
|
|
1691
|
+
placeholder: post.excerpt || "Defaults to excerpt",
|
|
1692
|
+
rows: 4,
|
|
1693
|
+
className: "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm resize-none focus:outline-none focus:ring-1 be-ring-primary"
|
|
1694
|
+
}
|
|
1695
|
+
),
|
|
1696
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "h-1.5 rounded-full bg-gray-100 overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: `h-full transition-all ${descriptionLengthColor(descLen)}`, style: { width: `${Math.min(100, descLen / 160 * 100)}%` } }) }),
|
|
1697
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "text-[10px] text-gray-400", children: [
|
|
1698
|
+
descLen,
|
|
1699
|
+
" / 160 chars"
|
|
1700
|
+
] })
|
|
1701
|
+
] }),
|
|
1702
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
|
|
1703
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "text-xs font-medium text-gray-700", children: "Focus Keyword" }),
|
|
1704
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1705
|
+
"input",
|
|
1706
|
+
{
|
|
1707
|
+
value: post.focusKeyword || "",
|
|
1708
|
+
onChange: (e) => onChange({ focusKeyword: e.target.value }),
|
|
1709
|
+
placeholder: "e.g. AI machine learning",
|
|
1710
|
+
className: "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 be-ring-primary"
|
|
1711
|
+
}
|
|
1712
|
+
)
|
|
1713
|
+
] }),
|
|
1714
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "space-y-1", children: [
|
|
1715
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("label", { className: "text-xs font-medium text-gray-700", children: "Permalink / Slug" }),
|
|
1716
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1717
|
+
"input",
|
|
1718
|
+
{
|
|
1719
|
+
value: post.slug,
|
|
1720
|
+
onChange: (e) => onChange({ slug: e.target.value }),
|
|
1721
|
+
className: "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 be-ring-primary"
|
|
1722
|
+
}
|
|
1723
|
+
)
|
|
1724
|
+
] }),
|
|
1725
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "border-t border-gray-100 pt-3", children: [
|
|
1726
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs font-semibold text-gray-700 mb-2", children: "Basic SEO Checklist" }),
|
|
1727
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "space-y-2", children: seo.checks.map((check) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-start gap-2 text-xs", children: [
|
|
1728
|
+
check.passed ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react4.Check, { size: 14, className: "text-green-600 shrink-0 mt-0.5" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react4.X, { size: 14, className: "text-red-400 shrink-0 mt-0.5" }),
|
|
1729
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: check.passed ? "text-gray-700" : "text-gray-500", children: check.label })
|
|
1730
|
+
] }, check.id)) })
|
|
1731
|
+
] })
|
|
1732
|
+
] })
|
|
1733
|
+
] });
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// src/components/PostSidebar.tsx
|
|
1737
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1738
|
+
function Section({ title, children, defaultOpen = true }) {
|
|
1739
|
+
const [open, setOpen] = (0, import_react4.useState)(defaultOpen);
|
|
1740
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "border-b border-gray-200", children: [
|
|
1741
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("button", { type: "button", onClick: () => setOpen((v) => !v), className: "w-full flex items-center justify-between px-4 py-3 text-sm font-semibold text-gray-800 hover:bg-gray-50", children: [
|
|
1742
|
+
title,
|
|
1743
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react5.ChevronUp, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react5.ChevronDown, { size: 16 })
|
|
1744
|
+
] }),
|
|
1745
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "px-4 pb-4 space-y-3", children })
|
|
1746
|
+
] });
|
|
1747
|
+
}
|
|
1748
|
+
function BlockSettings({
|
|
1749
|
+
block,
|
|
1750
|
+
onChange
|
|
1751
|
+
}) {
|
|
1752
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "space-y-3 pt-2 border-t border-gray-100", children: [
|
|
1753
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide", children: "Selected block" }),
|
|
1754
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-sm font-medium text-gray-900", children: BLOCK_LABELS[block.type] }),
|
|
1755
|
+
block.type === "heading" && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "space-y-1", children: [
|
|
1756
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("label", { className: "text-xs text-gray-500", children: "Heading level" }),
|
|
1757
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1758
|
+
"select",
|
|
1759
|
+
{
|
|
1760
|
+
value: block.level || 2,
|
|
1761
|
+
onChange: (e) => onChange({ level: parseInt(e.target.value, 10) }),
|
|
1762
|
+
className: "w-full border border-gray-200 rounded px-2 py-1.5 text-sm",
|
|
1763
|
+
children: [1, 2, 3, 4, 5, 6].map((lvl) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: lvl, children: HEADING_LABELS[lvl] }, lvl))
|
|
1764
|
+
}
|
|
1765
|
+
)
|
|
1766
|
+
] }),
|
|
1767
|
+
block.type === "list" && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center gap-2 text-sm text-gray-700", children: [
|
|
1768
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "checkbox", checked: Boolean(block.ordered), onChange: (e) => onChange({ ordered: e.target.checked }) }),
|
|
1769
|
+
"Numbered list"
|
|
1770
|
+
] }),
|
|
1771
|
+
block.type === "image" && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
1772
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1773
|
+
"input",
|
|
1774
|
+
{
|
|
1775
|
+
type: "url",
|
|
1776
|
+
value: block.src || "",
|
|
1777
|
+
onChange: (e) => onChange({ src: e.target.value }),
|
|
1778
|
+
placeholder: "Image URL",
|
|
1779
|
+
className: "w-full border border-gray-200 rounded px-2 py-1.5 text-xs"
|
|
1780
|
+
}
|
|
1781
|
+
),
|
|
1782
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1783
|
+
"input",
|
|
1784
|
+
{
|
|
1785
|
+
type: "text",
|
|
1786
|
+
value: block.alt || "",
|
|
1787
|
+
onChange: (e) => onChange({ alt: e.target.value }),
|
|
1788
|
+
placeholder: "Alt text",
|
|
1789
|
+
className: "w-full border border-gray-200 rounded px-2 py-1.5 text-xs"
|
|
1790
|
+
}
|
|
1791
|
+
),
|
|
1792
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "grid grid-cols-2 gap-2", children: [
|
|
1793
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "text-xs text-gray-500", children: [
|
|
1794
|
+
"Width (px)",
|
|
1795
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1796
|
+
"input",
|
|
1797
|
+
{
|
|
1798
|
+
type: "number",
|
|
1799
|
+
min: 40,
|
|
1800
|
+
placeholder: "auto",
|
|
1801
|
+
value: block.imageWidthPercent ? "" : block.imageWidth ?? "",
|
|
1802
|
+
disabled: Boolean(block.imageWidthPercent),
|
|
1803
|
+
onChange: (e) => {
|
|
1804
|
+
const v = e.target.value;
|
|
1805
|
+
if (!v) onChange({ imageWidth: void 0 });
|
|
1806
|
+
else onChange({ imageWidth: parseInt(v, 10) || void 0, imageWidthPercent: void 0 });
|
|
1807
|
+
},
|
|
1808
|
+
className: "mt-0.5 w-full border border-gray-200 rounded px-2 py-1 text-xs disabled:bg-gray-50"
|
|
1809
|
+
}
|
|
1810
|
+
)
|
|
1811
|
+
] }),
|
|
1812
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "text-xs text-gray-500", children: [
|
|
1813
|
+
"Height (px)",
|
|
1814
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1815
|
+
"input",
|
|
1816
|
+
{
|
|
1817
|
+
type: "number",
|
|
1818
|
+
min: 40,
|
|
1819
|
+
placeholder: "auto",
|
|
1820
|
+
value: block.imageHeight ?? "",
|
|
1821
|
+
onChange: (e) => {
|
|
1822
|
+
const v = e.target.value;
|
|
1823
|
+
if (!v) onChange({ imageHeight: void 0 });
|
|
1824
|
+
else onChange({ imageHeight: parseInt(v, 10) || void 0, imageWidthPercent: void 0 });
|
|
1825
|
+
},
|
|
1826
|
+
className: "mt-0.5 w-full border border-gray-200 rounded px-2 py-1 text-xs"
|
|
1827
|
+
}
|
|
1828
|
+
)
|
|
1829
|
+
] })
|
|
1830
|
+
] }),
|
|
1831
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1832
|
+
"select",
|
|
1833
|
+
{
|
|
1834
|
+
value: block.imageWidthPercent ?? "custom",
|
|
1835
|
+
onChange: (e) => {
|
|
1836
|
+
const v = e.target.value;
|
|
1837
|
+
if (v === "custom") return;
|
|
1838
|
+
if (v === "auto") onChange({ imageWidth: void 0, imageHeight: void 0, imageWidthPercent: void 0 });
|
|
1839
|
+
else onChange({ imageWidthPercent: parseInt(v, 10), imageWidth: void 0, imageHeight: void 0 });
|
|
1840
|
+
},
|
|
1841
|
+
className: "w-full border border-gray-200 rounded px-2 py-1.5 text-xs",
|
|
1842
|
+
children: [
|
|
1843
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "auto", children: "Auto size" }),
|
|
1844
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "25", children: "25% width" }),
|
|
1845
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "50", children: "50% width" }),
|
|
1846
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "75", children: "75% width" }),
|
|
1847
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "100", children: "100% width" }),
|
|
1848
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "custom", children: "Custom (px)" })
|
|
1849
|
+
]
|
|
1850
|
+
}
|
|
1851
|
+
)
|
|
1852
|
+
] }),
|
|
1853
|
+
block.type === "embed" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1854
|
+
"input",
|
|
1855
|
+
{
|
|
1856
|
+
type: "url",
|
|
1857
|
+
value: block.embedUrl || "",
|
|
1858
|
+
onChange: (e) => onChange({ embedUrl: e.target.value }),
|
|
1859
|
+
placeholder: "Embed URL",
|
|
1860
|
+
className: "w-full border border-gray-200 rounded px-2 py-1.5 text-xs"
|
|
1861
|
+
}
|
|
1862
|
+
)
|
|
1863
|
+
] });
|
|
1864
|
+
}
|
|
1865
|
+
function PostSidebar({
|
|
1866
|
+
post,
|
|
1867
|
+
contentHtml,
|
|
1868
|
+
onChange,
|
|
1869
|
+
onAddBlock,
|
|
1870
|
+
activeBlock,
|
|
1871
|
+
onUpdateBlock,
|
|
1872
|
+
seoMode,
|
|
1873
|
+
onCloseSeo
|
|
1874
|
+
}) {
|
|
1875
|
+
const { onUploadImage } = useEditorContext();
|
|
1876
|
+
const canUpload = Boolean(onUploadImage);
|
|
1877
|
+
const [tab, setTab] = (0, import_react4.useState)("post");
|
|
1878
|
+
const [uploading, setUploading] = (0, import_react4.useState)(false);
|
|
1879
|
+
const [tagInput, setTagInput] = (0, import_react4.useState)(post.tags.join(", "));
|
|
1880
|
+
const uploadFeatured = async (file) => {
|
|
1881
|
+
if (!onUploadImage) return;
|
|
1882
|
+
setUploading(true);
|
|
1883
|
+
try {
|
|
1884
|
+
const url = await onUploadImage(file);
|
|
1885
|
+
if (url) onChange({ featuredImage: url });
|
|
1886
|
+
} finally {
|
|
1887
|
+
setUploading(false);
|
|
1888
|
+
}
|
|
1889
|
+
};
|
|
1890
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("aside", { className: "w-[300px] shrink-0 border-l border-gray-200 bg-white flex flex-col h-full overflow-hidden", children: seoMode ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SeoPanel, { post, contentHtml, onChange, onClose: onCloseSeo ?? (() => {
|
|
1891
|
+
}) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
1892
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex border-b border-gray-200 sticky top-0 bg-white z-10 shrink-0", children: [
|
|
1893
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1894
|
+
"button",
|
|
1895
|
+
{
|
|
1896
|
+
type: "button",
|
|
1897
|
+
onClick: () => setTab("post"),
|
|
1898
|
+
className: `flex-1 py-3 text-sm font-semibold ${tab === "post" ? "be-text-primary border-b-2 be-border-primary" : "text-gray-500 hover:text-gray-700"}`,
|
|
1899
|
+
children: "Post"
|
|
1900
|
+
}
|
|
1901
|
+
),
|
|
1902
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1903
|
+
"button",
|
|
1904
|
+
{
|
|
1905
|
+
type: "button",
|
|
1906
|
+
onClick: () => setTab("block"),
|
|
1907
|
+
className: `flex-1 py-3 text-sm font-semibold ${tab === "block" ? "be-text-primary border-b-2 be-border-primary" : "text-gray-500 hover:text-gray-700"}`,
|
|
1908
|
+
children: "Block"
|
|
1909
|
+
}
|
|
1910
|
+
)
|
|
1911
|
+
] }),
|
|
1912
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex-1 overflow-y-auto", children: [
|
|
1913
|
+
tab === "post" && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
1914
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Section, { title: "Featured image", children: post.featuredImage ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "space-y-2", children: [
|
|
1915
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { src: post.featuredImage, alt: "", className: "w-full rounded border border-gray-200 object-cover aspect-video" }),
|
|
1916
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: () => onChange({ featuredImage: "" }), className: "text-xs text-red-500 hover:underline", children: "Remove image" })
|
|
1917
|
+
] }) : canUpload ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex flex-col items-center justify-center h-32 border border-gray-200 rounded cursor-pointer be-hover-border-primary hover:bg-gray-50", children: [
|
|
1918
|
+
uploading ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react5.Loader2, { size: 20, className: "animate-spin text-gray-400" }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react5.ImageIcon, { size: 24, className: "text-gray-400 mb-1" }),
|
|
1919
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-xs text-gray-500", children: "Set featured image" }),
|
|
1920
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "file", accept: "image/*", className: "hidden", onChange: (e) => e.target.files?.[0] && uploadFeatured(e.target.files[0]), disabled: uploading })
|
|
1921
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-xs text-gray-400 text-center py-4 border border-dashed border-gray-200 rounded", children: [
|
|
1922
|
+
"Pass ",
|
|
1923
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("code", { className: "bg-gray-100 px-1 rounded", children: "onUploadImage" }),
|
|
1924
|
+
" to enable uploads"
|
|
1925
|
+
] }) }),
|
|
1926
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Section, { title: "Excerpt", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1927
|
+
"textarea",
|
|
1928
|
+
{
|
|
1929
|
+
value: post.excerpt,
|
|
1930
|
+
onChange: (e) => onChange({ excerpt: e.target.value }),
|
|
1931
|
+
rows: 3,
|
|
1932
|
+
placeholder: "Short summary for blog cards & SEO fallback",
|
|
1933
|
+
className: "w-full border border-gray-200 rounded px-3 py-2 text-sm resize-none focus:outline-none focus:ring-1 be-ring-primary"
|
|
1934
|
+
}
|
|
1935
|
+
) }),
|
|
1936
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Section, { title: "Post settings", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "space-y-3 text-sm", children: [
|
|
1937
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
|
|
1938
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("label", { className: "text-xs text-gray-500 block mb-1", children: "Status" }),
|
|
1939
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("select", { value: post.status, onChange: (e) => onChange({ status: e.target.value }), className: "w-full border border-gray-200 rounded px-2 py-1.5 text-sm", children: [
|
|
1940
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "draft", children: "Draft" }),
|
|
1941
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "published", children: "Published" })
|
|
1942
|
+
] })
|
|
1943
|
+
] }),
|
|
1944
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
|
|
1945
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("label", { className: "text-xs text-gray-500 block mb-1", children: "Publish date" }),
|
|
1946
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "datetime-local", value: post.publishedAt.slice(0, 16), onChange: (e) => onChange({ publishedAt: new Date(e.target.value).toISOString() }), className: "w-full border border-gray-200 rounded px-2 py-1.5 text-xs" })
|
|
1947
|
+
] }),
|
|
1948
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
|
|
1949
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("label", { className: "text-xs text-gray-500 block mb-1", children: "Permalink / Slug" }),
|
|
1950
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { value: post.slug, onChange: (e) => onChange({ slug: e.target.value }), className: "w-full border border-gray-200 rounded px-2 py-1.5 text-xs" })
|
|
1951
|
+
] }),
|
|
1952
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
|
|
1953
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("label", { className: "text-xs text-gray-500 block mb-1", children: "Author" }),
|
|
1954
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { value: post.author, onChange: (e) => onChange({ author: e.target.value }), className: "w-full border border-gray-200 rounded px-2 py-1.5 text-sm", placeholder: "Author name" })
|
|
1955
|
+
] })
|
|
1956
|
+
] }) }),
|
|
1957
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Section, { title: "Tags", defaultOpen: true, children: [
|
|
1958
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1959
|
+
"input",
|
|
1960
|
+
{
|
|
1961
|
+
value: tagInput,
|
|
1962
|
+
onChange: (e) => {
|
|
1963
|
+
setTagInput(e.target.value);
|
|
1964
|
+
onChange({ tags: e.target.value.split(",").map((t) => t.trim()).filter(Boolean) });
|
|
1965
|
+
},
|
|
1966
|
+
placeholder: "ai, startup, product",
|
|
1967
|
+
className: "w-full border border-gray-200 rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 be-ring-primary"
|
|
1968
|
+
}
|
|
1969
|
+
),
|
|
1970
|
+
post.tags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex flex-wrap gap-1.5 pt-1", children: post.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-xs bg-gray-100 text-gray-700 px-2 py-0.5 rounded-full", children: tag }, tag)) }),
|
|
1971
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-gray-400", children: "Separate with commas." })
|
|
1972
|
+
] })
|
|
1973
|
+
] }),
|
|
1974
|
+
tab === "block" && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "p-4 space-y-4", children: [
|
|
1975
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BlockLibrary, { onAddBlock }),
|
|
1976
|
+
activeBlock && onUpdateBlock && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BlockSettings, { block: activeBlock, onChange: onUpdateBlock }),
|
|
1977
|
+
!activeBlock && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-xs text-gray-400 border-t border-gray-100 pt-3", children: "Click a block in the editor to see its settings here." })
|
|
1978
|
+
] })
|
|
1979
|
+
] })
|
|
1980
|
+
] }) });
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/components/BlogEditor.tsx
|
|
1984
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1985
|
+
function BlogEditorInner({
|
|
1986
|
+
initial,
|
|
1987
|
+
originalSlug,
|
|
1988
|
+
onSave,
|
|
1989
|
+
onNavigate,
|
|
1990
|
+
backHref = "#",
|
|
1991
|
+
previewUrl,
|
|
1992
|
+
newPostSlug = "new-post",
|
|
1993
|
+
className = ""
|
|
1994
|
+
}) {
|
|
1995
|
+
const [post, setPost] = (0, import_react5.useState)(initial);
|
|
1996
|
+
const initialBlocks = (0, import_react5.useMemo)(() => htmlToBlocks(initial.content), [initial.content]);
|
|
1997
|
+
const [blocks, setBlocks] = (0, import_react5.useState)(initialBlocks);
|
|
1998
|
+
const [blocksHistory, setBlocksHistory] = (0, import_react5.useState)([initialBlocks]);
|
|
1999
|
+
const [historyIndex, setHistoryIndex] = (0, import_react5.useState)(0);
|
|
2000
|
+
const [activeId, setActiveId] = (0, import_react5.useState)(null);
|
|
2001
|
+
const [draggingType, setDraggingType] = (0, import_react5.useState)(null);
|
|
2002
|
+
const [saving, setSaving] = (0, import_react5.useState)(false);
|
|
2003
|
+
const [message, setMessage] = (0, import_react5.useState)("");
|
|
2004
|
+
const [sidebarOpen, setSidebarOpen] = (0, import_react5.useState)(true);
|
|
2005
|
+
const [seoOpen, setSeoOpen] = (0, import_react5.useState)(false);
|
|
2006
|
+
const sensors = (0, import_core3.useSensors)((0, import_core3.useSensor)(import_core3.PointerSensor, { activationConstraint: { distance: 6 } }));
|
|
2007
|
+
const isNew = (originalSlug ?? initial.slug) === newPostSlug;
|
|
2008
|
+
const editSlug = originalSlug ?? initial.slug;
|
|
2009
|
+
const contentHtml = (0, import_react5.useMemo)(() => blocksToHtml(blocks), [blocks]);
|
|
2010
|
+
const seoScore = (0, import_react5.useMemo)(() => computeSeoScore(post, contentHtml).score, [post, contentHtml]);
|
|
2011
|
+
const activeBlock = blocks.find((b) => b.id === activeId);
|
|
2012
|
+
const updatePost = (patch) => setPost((p) => ({ ...p, ...patch }));
|
|
2013
|
+
const handleBlocksChange = (next) => {
|
|
2014
|
+
setBlocksHistory((hist) => [...hist.slice(0, historyIndex + 1), next]);
|
|
2015
|
+
setHistoryIndex((i) => i + 1);
|
|
2016
|
+
setBlocks(next);
|
|
2017
|
+
};
|
|
2018
|
+
const updateActiveBlock = (patch) => {
|
|
2019
|
+
if (!activeId) return;
|
|
2020
|
+
handleBlocksChange(blocks.map((b) => b.id === activeId ? { ...b, ...patch } : b));
|
|
2021
|
+
};
|
|
2022
|
+
const addBlock = (type, afterIndex) => {
|
|
2023
|
+
const newBlock = createBlock(type);
|
|
2024
|
+
const next = [...blocks];
|
|
2025
|
+
const at = afterIndex !== void 0 && afterIndex >= 0 ? afterIndex + 1 : next.length;
|
|
2026
|
+
next.splice(at, 0, newBlock);
|
|
2027
|
+
handleBlocksChange(next);
|
|
2028
|
+
setActiveId(newBlock.id);
|
|
2029
|
+
};
|
|
2030
|
+
const handleDragStart = (event) => {
|
|
2031
|
+
const data = event.active.data.current;
|
|
2032
|
+
if (data?.source === "library" && data.type) setDraggingType(data.type);
|
|
2033
|
+
};
|
|
2034
|
+
const handleDragEnd = (event) => {
|
|
2035
|
+
setDraggingType(null);
|
|
2036
|
+
const { active, over } = event;
|
|
2037
|
+
if (!over) return;
|
|
2038
|
+
const data = active.data.current;
|
|
2039
|
+
if (data?.source === "library" && data.type) {
|
|
2040
|
+
const overId = String(over.id);
|
|
2041
|
+
if (overId === "canvas-drop") {
|
|
2042
|
+
addBlock(data.type);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
const idx = blocks.findIndex((b) => b.id === overId);
|
|
2046
|
+
addBlock(data.type, idx >= 0 ? idx : blocks.length - 1);
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
if (active.id !== over.id) {
|
|
2050
|
+
const oldIndex = blocks.findIndex((b) => b.id === active.id);
|
|
2051
|
+
const newIndex = blocks.findIndex((b) => b.id === over.id);
|
|
2052
|
+
if (oldIndex >= 0 && newIndex >= 0) {
|
|
2053
|
+
handleBlocksChange((0, import_sortable2.arrayMove)(blocks, oldIndex, newIndex));
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
const undo = () => {
|
|
2058
|
+
if (historyIndex <= 0) return;
|
|
2059
|
+
const idx = historyIndex - 1;
|
|
2060
|
+
setHistoryIndex(idx);
|
|
2061
|
+
setBlocks(blocksHistory[idx]);
|
|
2062
|
+
};
|
|
2063
|
+
const redo = () => {
|
|
2064
|
+
if (historyIndex >= blocksHistory.length - 1) return;
|
|
2065
|
+
const idx = historyIndex + 1;
|
|
2066
|
+
setHistoryIndex(idx);
|
|
2067
|
+
setBlocks(blocksHistory[idx]);
|
|
2068
|
+
};
|
|
2069
|
+
const resolvedTitle = (0, import_react5.useMemo)(() => post.title.trim() || "No title", [post.title]);
|
|
2070
|
+
const save = async (publish) => {
|
|
2071
|
+
setSaving(true);
|
|
2072
|
+
setMessage("");
|
|
2073
|
+
try {
|
|
2074
|
+
const title = post.title.trim() || "Untitled Post";
|
|
2075
|
+
let slug = post.slug.trim();
|
|
2076
|
+
if (isNew || slug === newPostSlug) slug = slugifyTitle(title);
|
|
2077
|
+
const payload = {
|
|
2078
|
+
...post,
|
|
2079
|
+
title,
|
|
2080
|
+
slug,
|
|
2081
|
+
content: contentHtml,
|
|
2082
|
+
status: publish ? "published" : post.status,
|
|
2083
|
+
publishedAt: publish ? (/* @__PURE__ */ new Date()).toISOString() : post.publishedAt
|
|
2084
|
+
};
|
|
2085
|
+
const result = await onSave(payload, { publish, contentHtml });
|
|
2086
|
+
const newSlug = result?.slug ?? slug;
|
|
2087
|
+
setPost({ ...payload, slug: newSlug });
|
|
2088
|
+
setMessage(publish ? "Published!" : "Draft saved");
|
|
2089
|
+
if (onNavigate && (isNew || newSlug !== editSlug)) {
|
|
2090
|
+
onNavigate(newSlug);
|
|
2091
|
+
}
|
|
2092
|
+
} catch {
|
|
2093
|
+
setMessage("Failed to save");
|
|
2094
|
+
} finally {
|
|
2095
|
+
setSaving(false);
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
const previewHref = previewUrl && post.slug !== newPostSlug ? previewUrl(post.slug) : void 0;
|
|
2099
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_core3.DndContext, { sensors, collisionDetection: import_core3.closestCenter, onDragStart: handleDragStart, onDragEnd: handleDragEnd, children: [
|
|
2100
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `flex flex-col h-screen bg-[var(--be-bg)] ${className}`, children: [
|
|
2101
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("header", { className: "h-14 bg-[var(--be-surface)] border-b border-gray-200 flex items-center justify-between px-4 shrink-0 z-30", children: [
|
|
2102
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
2103
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("a", { href: backHref, className: "p-2 hover:bg-gray-100 rounded", title: "Back to posts", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react6.ArrowLeft, { size: 18, className: "text-gray-600" }) }),
|
|
2104
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: undo, disabled: historyIndex <= 0, className: "p-2 hover:bg-gray-100 rounded disabled:opacity-30", title: "Undo", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react6.Undo2, { size: 18, className: "text-gray-600" }) }),
|
|
2105
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: redo, disabled: historyIndex >= blocksHistory.length - 1, className: "p-2 hover:bg-gray-100 rounded disabled:opacity-30", title: "Redo", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react6.Redo2, { size: 18, className: "text-gray-600" }) }),
|
|
2106
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "hidden md:inline text-sm text-gray-500 ml-2 max-w-[240px] truncate", children: [
|
|
2107
|
+
resolvedTitle,
|
|
2108
|
+
" \u2014 ",
|
|
2109
|
+
post.status === "published" ? "Post" : "Draft"
|
|
2110
|
+
] })
|
|
2111
|
+
] }),
|
|
2112
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
2113
|
+
message && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-xs be-text-primary hidden sm:inline", children: message }),
|
|
2114
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2115
|
+
"button",
|
|
2116
|
+
{
|
|
2117
|
+
type: "button",
|
|
2118
|
+
onClick: () => {
|
|
2119
|
+
setSidebarOpen(true);
|
|
2120
|
+
setSeoOpen(true);
|
|
2121
|
+
},
|
|
2122
|
+
className: `flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-lg border transition-colors ${seoOpen ? "be-border-primary be-bg-primary-muted be-text-primary ring-1 ring-[color-mix(in_srgb,var(--be-primary)_30%,transparent)]" : seoScore >= 70 ? "border-green-200 bg-green-50 text-green-700 hover:bg-green-100" : seoScore >= 40 ? "border-amber-200 bg-amber-50 text-amber-700 hover:bg-amber-100" : "border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100"}`,
|
|
2123
|
+
children: [
|
|
2124
|
+
"SEO",
|
|
2125
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-xs font-bold", children: seoScore })
|
|
2126
|
+
]
|
|
2127
|
+
}
|
|
2128
|
+
),
|
|
2129
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => save(false), disabled: saving, className: "text-sm text-gray-700 be-text-primary hover:opacity-80 px-3 py-1.5 disabled:opacity-50", children: "Save draft" }),
|
|
2130
|
+
previewHref && post.status === "published" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("a", { href: previewHref, target: "_blank", rel: "noopener noreferrer", className: "p-2 hover:bg-gray-100 rounded text-gray-600", title: "Preview", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react6.ExternalLink, { size: 18 }) }),
|
|
2131
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2132
|
+
"button",
|
|
2133
|
+
{
|
|
2134
|
+
type: "button",
|
|
2135
|
+
onClick: () => {
|
|
2136
|
+
setSidebarOpen((v) => !v);
|
|
2137
|
+
if (sidebarOpen) setSeoOpen(false);
|
|
2138
|
+
},
|
|
2139
|
+
className: `p-2 rounded transition-colors ${sidebarOpen ? "bg-gray-100 be-text-primary" : "hover:bg-gray-100 text-gray-600"}`,
|
|
2140
|
+
title: sidebarOpen ? "Collapse sidebar" : "Expand sidebar",
|
|
2141
|
+
children: sidebarOpen ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react6.PanelRightClose, { size: 18 }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react6.PanelRightOpen, { size: 18 })
|
|
2142
|
+
}
|
|
2143
|
+
),
|
|
2144
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2145
|
+
"button",
|
|
2146
|
+
{
|
|
2147
|
+
type: "button",
|
|
2148
|
+
onClick: () => save(true),
|
|
2149
|
+
disabled: saving,
|
|
2150
|
+
className: "be-btn-primary text-sm font-medium px-4 py-2 rounded disabled:opacity-50 flex items-center gap-2",
|
|
2151
|
+
children: [
|
|
2152
|
+
saving && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react6.Loader2, { size: 14, className: "animate-spin" }),
|
|
2153
|
+
post.status === "published" ? "Update" : "Publish"
|
|
2154
|
+
]
|
|
2155
|
+
}
|
|
2156
|
+
)
|
|
2157
|
+
] })
|
|
2158
|
+
] }),
|
|
2159
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-1 overflow-hidden", children: [
|
|
2160
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 overflow-y-auto overflow-x-hidden bg-[var(--be-surface)]", children: [
|
|
2161
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "max-w-[840px] mx-auto pt-12 pb-24 px-6", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2162
|
+
"input",
|
|
2163
|
+
{
|
|
2164
|
+
type: "text",
|
|
2165
|
+
value: post.title,
|
|
2166
|
+
onChange: (e) => updatePost({ title: e.target.value }),
|
|
2167
|
+
placeholder: "Add title",
|
|
2168
|
+
className: "w-full text-4xl md:text-5xl font-serif text-gray-900 placeholder:text-gray-300 border-none outline-none bg-transparent mb-8"
|
|
2169
|
+
}
|
|
2170
|
+
) }),
|
|
2171
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(BlockCanvas, { blocks, onChange: handleBlocksChange, activeId, onActiveIdChange: setActiveId })
|
|
2172
|
+
] }),
|
|
2173
|
+
sidebarOpen && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2174
|
+
PostSidebar,
|
|
2175
|
+
{
|
|
2176
|
+
post,
|
|
2177
|
+
contentHtml,
|
|
2178
|
+
onChange: updatePost,
|
|
2179
|
+
onAddBlock: (type) => addBlock(type),
|
|
2180
|
+
activeBlock,
|
|
2181
|
+
onUpdateBlock: updateActiveBlock,
|
|
2182
|
+
seoMode: seoOpen,
|
|
2183
|
+
onCloseSeo: () => setSeoOpen(false)
|
|
2184
|
+
}
|
|
2185
|
+
)
|
|
2186
|
+
] })
|
|
2187
|
+
] }),
|
|
2188
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_core3.DragOverlay, { children: draggingType ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "px-4 py-2 bg-white be-border-primary border rounded-lg shadow-lg text-sm font-medium text-gray-800", children: BLOCK_LABELS[draggingType] }) : null })
|
|
2189
|
+
] });
|
|
2190
|
+
}
|
|
2191
|
+
function BlogEditor({
|
|
2192
|
+
theme,
|
|
2193
|
+
onUploadImage,
|
|
2194
|
+
uploadFile,
|
|
2195
|
+
siteName,
|
|
2196
|
+
baseUrl,
|
|
2197
|
+
...props
|
|
2198
|
+
}) {
|
|
2199
|
+
const resolved = resolveTheme(theme);
|
|
2200
|
+
const imageUpload = onUploadImage ?? uploadFile;
|
|
2201
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(EditorProvider, { onUploadImage: imageUpload, theme: resolved, siteName, baseUrl, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "blog-editor-root", style: themeToCssVars(resolved), children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(BlogEditorInner, { ...props }) }) });
|
|
2202
|
+
}
|
|
2203
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2204
|
+
0 && (module.exports = {
|
|
2205
|
+
BLOCK_LABELS,
|
|
2206
|
+
BlogEditor,
|
|
2207
|
+
EditorProvider,
|
|
2208
|
+
HEADING_CLASSES,
|
|
2209
|
+
HEADING_LABELS,
|
|
2210
|
+
blocksToHtml,
|
|
2211
|
+
cleanPastedHtml,
|
|
2212
|
+
computeSeoScore,
|
|
2213
|
+
createBlock,
|
|
2214
|
+
defaultTheme,
|
|
2215
|
+
descriptionLengthColor,
|
|
2216
|
+
getSeoPreview,
|
|
2217
|
+
htmlToBlocks,
|
|
2218
|
+
inlineHtmlFromBlocks,
|
|
2219
|
+
insertHtmlAtCursor,
|
|
2220
|
+
isMultiBlockPaste,
|
|
2221
|
+
pastedHtmlToBlocks,
|
|
2222
|
+
resolveTheme,
|
|
2223
|
+
slugifyTitle,
|
|
2224
|
+
themeToCssVars,
|
|
2225
|
+
titleLengthColor,
|
|
2226
|
+
useEditorContext
|
|
2227
|
+
});
|
|
2228
|
+
//# sourceMappingURL=index.cjs.map
|