@brutalcomponent/react
Version:
Brutalist React components
911 lines (908 loc) • 32.6 kB
JavaScript
import { Tabs, TabsList, TabsTrigger, TabsContent, Card, Toggle, Tooltip } from '../../chunk-JDHAADYK.mjs';
import { Input, Textarea, Select } from '../../chunk-YLIM27QY.mjs';
import { Button } from '../../chunk-E2BWEC67.mjs';
import { Icon } from '../../chunk-QB4EPFXT.mjs';
import '../../chunk-KMHX64YN.mjs';
import '../../chunk-7QJK2SK5.mjs';
import React4, { useState, useRef, useCallback, useEffect } from 'react';
import { clsx } from 'clsx';
import { FaBold, FaItalic, FaHeading, FaListUl, FaLink, FaChevronDown, FaListOl, FaQuoteLeft, FaCode, FaImage, FaSave, FaPen, FaEye, FaCog, FaClock, FaUnderline, FaStrikethrough, FaAlignLeft, FaAlignCenter, FaAlignRight, FaTable, FaPalette, FaHighlighter, FaIndent, FaOutdent, FaUndo, FaRedo, FaPlus, FaTimes, FaCompress, FaExpand } from 'react-icons/fa';
/**
* @brutalcomponent/react
* (c) David Heffler (https://dvh.sh)
* Licensed under MIT
*/
var MarkdownToolbar = ({
onAction,
brutal = true,
showMobile = true,
className
}) => {
const [showMore, setShowMore] = useState(false);
const primaryActions = [
{ icon: FaBold, label: "Bold", markdown: "**text**" },
{ icon: FaItalic, label: "Italic", markdown: "*text*" },
{ icon: FaHeading, label: "Heading", markdown: "## " },
{ icon: FaListUl, label: "List", markdown: "- " },
{ icon: FaLink, label: "Link", markdown: "[text](url)" }
];
const secondaryActions = [
{ icon: FaListOl, label: "Numbered", markdown: "1. " },
{ icon: FaQuoteLeft, label: "Quote", markdown: "> " },
{ icon: FaCode, label: "Code", markdown: "`code`" },
{ icon: FaImage, label: "Image", markdown: "" }
];
return /* @__PURE__ */ React4.createElement(
"div",
{
className: clsx(
"border-b-2 border-brutal-black bg-brutal-gray-50",
className
)
},
/* @__PURE__ */ React4.createElement("div", { className: "flex items-center" }, /* @__PURE__ */ React4.createElement("div", { className: "flex items-center" }, primaryActions.map((action, index) => /* @__PURE__ */ React4.createElement(
"button",
{
key: index,
type: "button",
onClick: () => onAction(action.markdown),
className: clsx(
"p-3 hover:bg-brutal-gray-200 transition-colors",
"border-r border-brutal-gray-300"
),
title: action.label
},
/* @__PURE__ */ React4.createElement(Icon, { icon: action.icon, size: "sm" })
))), showMobile && /* @__PURE__ */ React4.createElement(
"button",
{
type: "button",
onClick: () => setShowMore(!showMore),
className: clsx(
"p-3 hover:bg-brutal-gray-200 transition-colors",
"flex items-center gap-1 lg:hidden"
)
},
/* @__PURE__ */ React4.createElement("span", { className: "text-xs font-bold uppercase" }, "More"),
/* @__PURE__ */ React4.createElement(
Icon,
{
icon: FaChevronDown,
size: "xs",
className: clsx("transition-transform", showMore && "rotate-180")
}
)
), /* @__PURE__ */ React4.createElement("div", { className: "hidden lg:flex items-center border-l-2 border-brutal-black" }, secondaryActions.map((action, index) => /* @__PURE__ */ React4.createElement(
"button",
{
key: index,
type: "button",
onClick: () => onAction(action.markdown),
className: clsx(
"p-3 hover:bg-brutal-gray-200 transition-colors",
"border-r border-brutal-gray-300"
),
title: action.label
},
/* @__PURE__ */ React4.createElement(Icon, { icon: action.icon, size: "sm" })
)))),
showMore && showMobile && /* @__PURE__ */ React4.createElement("div", { className: "flex items-center border-t border-brutal-gray-300 lg:hidden" }, secondaryActions.map((action, index) => /* @__PURE__ */ React4.createElement(
"button",
{
key: index,
type: "button",
onClick: () => onAction(action.markdown),
className: clsx(
"p-3 hover:bg-brutal-gray-200 transition-colors",
"border-r border-brutal-gray-300"
),
title: action.label
},
/* @__PURE__ */ React4.createElement(Icon, { icon: action.icon, size: "sm" })
)))
);
};
// src/modules/editor/MarkdownEditor/MarkdownEditor.tsx
var MarkdownEditor = ({
value,
onChange,
placeholder = "Write your content in Markdown...",
minHeight = "400px",
showToolbar = true,
brutal = true,
className,
textareaClassName
}) => {
const textareaRef = useRef(null);
const insertMarkdown = useCallback(
(markdown) => {
const textarea = textareaRef.current;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newContent = value.substring(0, start) + markdown + value.substring(end);
onChange(newContent);
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(
start + markdown.length,
start + markdown.length
);
}, 0);
},
[value, onChange]
);
return /* @__PURE__ */ React4.createElement(
"div",
{
className: clsx(
brutal && "border-4 border-brutal-black",
!brutal && "border-2 border-brutal-gray-300",
"bg-brutal-white",
className
)
},
showToolbar && /* @__PURE__ */ React4.createElement(MarkdownToolbar, { onAction: insertMarkdown, brutal }),
/* @__PURE__ */ React4.createElement("div", { className: "relative", style: { minHeight } }, /* @__PURE__ */ React4.createElement(
"textarea",
{
ref: textareaRef,
value,
onChange: (e) => onChange(e.target.value),
placeholder,
className: clsx(
"w-full h-full p-4 resize-none",
"font-mono text-sm bg-transparent",
"focus:outline-none",
textareaClassName
),
style: { minHeight }
}
), /* @__PURE__ */ React4.createElement("div", { className: "absolute bottom-2 right-2 text-xs text-brutal-gray-500" }, value.length, " characters"))
);
};
var MarkdownPreview = ({
content,
className,
parser
}) => {
const [html, setHtml] = useState("");
const [error, setError] = useState(null);
useEffect(() => {
const parseContent = async () => {
if (!content) {
setHtml("");
return;
}
try {
if (parser) {
const parsed = await parser(content);
setHtml(parsed);
} else {
setHtml(`<pre>${content}</pre>`);
console.warn(
"No markdown parser provided. Install marked: npm install marked"
);
}
setError(null);
} catch (err) {
console.error("Error parsing markdown:", err);
setError("Error rendering markdown");
}
};
parseContent();
}, [content, parser]);
if (!content) {
return /* @__PURE__ */ React4.createElement("div", { className: "text-brutal-gray-400 text-sm italic p-4" }, "Preview will appear here...");
}
if (error) {
return /* @__PURE__ */ React4.createElement("div", { className: "text-brutal-coral text-sm p-4" }, error);
}
return /* @__PURE__ */ React4.createElement(
"div",
{
className: clsx(
"prose prose-brutal max-w-none",
// Brutal prose styles
"prose-headings:font-black prose-headings:uppercase prose-headings:tracking-wider",
"prose-h1:text-4xl prose-h1:text-brutal-black prose-h1:transform prose-h1:-skew-x-2",
"prose-h2:text-3xl prose-h2:text-brutal-black",
"prose-h3:text-2xl prose-h3:text-brutal-black",
// Paragraphs
"prose-p:text-brutal-gray-700 prose-p:font-mono prose-p:leading-relaxed",
// Links
"prose-a:text-brutal-pink prose-a:no-underline prose-a:font-bold",
"prose-a:border-b-2 prose-a:border-brutal-pink",
"prose-a:hover:text-brutal-peach prose-a:hover:border-brutal-peach",
// Lists
"prose-ul:list-none prose-ul:space-y-2",
'prose-li:before:content-["\u2192"] prose-li:before:text-brutal-pink',
"prose-li:before:font-black prose-li:before:mr-2",
// Code
"prose-code:bg-brutal-gray-200 prose-code:text-brutal-black",
"prose-code:px-2 prose-code:py-1",
"prose-pre:bg-brutal-black prose-pre:text-brutal-white",
"prose-pre:border-4 prose-pre:border-brutal-black",
className
),
dangerouslySetInnerHTML: { __html: html }
}
);
};
var BlogEditor = ({
initialData = {},
onSave,
onCancel,
categories = [],
authors = [],
loading = false,
brutal = true,
className,
markdownParser
}) => {
const [formData, setFormData] = useState({
title: initialData.title || "",
slug: initialData.slug || "",
content: initialData.content || "",
excerpt: initialData.excerpt || "",
status: initialData.status || "draft",
tags: initialData.tags || "",
category: initialData.category || "",
featuredImage: initialData.featuredImage || "",
publishedAt: initialData.publishedAt || "",
isFeatured: initialData.isFeatured || false,
author: initialData.author || ""
});
const [autoSave, setAutoSave] = useState(true);
const [lastSaved, setLastSaved] = useState(null);
const generateSlug = (title) => {
return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").trim();
};
const handleTitleChange = (e) => {
const title = e.target.value;
setFormData((prev) => ({
...prev,
title,
slug: initialData.slug ? prev.slug : generateSlug(title)
}));
};
const handleSave = async () => {
if (onSave) {
await onSave(formData);
setLastSaved(/* @__PURE__ */ new Date());
}
};
const getWordCount = () => {
return formData.content.split(/\s+/).filter(Boolean).length;
};
return /* @__PURE__ */ React4.createElement("div", { className: clsx("min-h-screen bg-brutal-white", className) }, /* @__PURE__ */ React4.createElement(Tabs, { defaultValue: "editor", brutal }, /* @__PURE__ */ React4.createElement(
"div",
{
className: clsx(
"sticky top-0 z-40",
"border-b-4 border-brutal-black bg-brutal-white",
"px-4 py-3"
)
},
/* @__PURE__ */ React4.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React4.createElement("div", { className: "flex-1 min-w-0 mr-4" }, /* @__PURE__ */ React4.createElement("h1", { className: "text-sm lg:text-base font-bold uppercase truncate" }, initialData.title ? "Edit Post" : "New Post", formData.title && /* @__PURE__ */ React4.createElement("span", { className: "hidden sm:inline text-brutal-gray-600" }, " ", "\u2014 ", formData.title)), /* @__PURE__ */ React4.createElement("div", { className: "flex items-center gap-2 mt-1 text-xs" }, /* @__PURE__ */ React4.createElement(
"span",
{
className: clsx(
"flex items-center gap-1",
formData.status === "published" ? "text-brutal-mint" : "text-brutal-yellow"
)
},
/* @__PURE__ */ React4.createElement("span", { className: "w-2 h-2 rounded-full bg-current" }),
formData.status
), lastSaved && /* @__PURE__ */ React4.createElement("span", { className: "text-brutal-gray-500" }, "Saved ", lastSaved.toLocaleTimeString()))), /* @__PURE__ */ React4.createElement("div", { className: "flex items-center gap-2" }, onCancel && /* @__PURE__ */ React4.createElement(
Button,
{
variant: "secondary",
size: "sm",
onClick: onCancel,
disabled: loading,
brutal,
className: "hidden sm:flex"
},
"Cancel"
), /* @__PURE__ */ React4.createElement(
Button,
{
variant: "primary",
size: "sm",
onClick: handleSave,
disabled: loading,
loading,
brutal,
leftIcon: () => /* @__PURE__ */ React4.createElement(FaSave, null)
},
/* @__PURE__ */ React4.createElement("span", { className: "hidden sm:inline" }, "Save")
)))
), /* @__PURE__ */ React4.createElement(TabsList, { brutal, stretch: true }, /* @__PURE__ */ React4.createElement(TabsTrigger, { value: "editor", icon: FaPen, stretch: true }, "Write"), /* @__PURE__ */ React4.createElement(TabsTrigger, { value: "preview", icon: FaEye, stretch: true }, "Preview"), /* @__PURE__ */ React4.createElement(TabsTrigger, { value: "settings", icon: FaCog, stretch: true }, "Settings")), /* @__PURE__ */ React4.createElement(TabsContent, { value: "editor", className: "p-4 lg:p-6" }, /* @__PURE__ */ React4.createElement("div", { className: "max-w-4xl mx-auto space-y-6" }, /* @__PURE__ */ React4.createElement("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4" }, /* @__PURE__ */ React4.createElement(
Input,
{
label: "Title",
value: formData.title,
onChange: handleTitleChange,
placeholder: "Post title...",
brutal,
required: true
}
), /* @__PURE__ */ React4.createElement(
Input,
{
label: "Slug",
value: formData.slug,
onChange: (e) => setFormData((prev) => ({
...prev,
slug: e.target.value
})),
placeholder: "post-slug",
brutal,
required: true
}
)), /* @__PURE__ */ React4.createElement(
Textarea,
{
label: "Excerpt",
value: formData.excerpt,
onChange: (e) => setFormData((prev) => ({
...prev,
excerpt: e.target.value
})),
placeholder: "Brief description of your post...",
rows: 3,
brutal,
hint: `${formData.excerpt.length}/300 characters`
}
), /* @__PURE__ */ React4.createElement("div", null, /* @__PURE__ */ React4.createElement("label", { className: "block text-xs font-bold uppercase mb-2" }, "Content"), /* @__PURE__ */ React4.createElement(
MarkdownEditor,
{
value: formData.content,
onChange: (content) => setFormData((prev) => ({
...prev,
content
})),
brutal,
minHeight: "500px"
}
)))), /* @__PURE__ */ React4.createElement(TabsContent, { value: "preview", className: "p-4 lg:p-6" }, /* @__PURE__ */ React4.createElement("div", { className: "max-w-4xl mx-auto" }, /* @__PURE__ */ React4.createElement(Card, { brutal, variant: "raised" }, /* @__PURE__ */ React4.createElement("article", null, /* @__PURE__ */ React4.createElement("header", { className: "mb-8" }, /* @__PURE__ */ React4.createElement("h1", { className: "text-4xl font-black uppercase tracking-wider mb-4" }, formData.title || "Untitled Post"), /* @__PURE__ */ React4.createElement("p", { className: "text-lg text-brutal-gray-600 font-mono" }, formData.excerpt || "No excerpt provided"), /* @__PURE__ */ React4.createElement("div", { className: "flex items-center gap-4 mt-4 text-xs text-brutal-gray-500" }, formData.author && /* @__PURE__ */ React4.createElement("span", null, "By", " ", authors.find((a) => a.value === formData.author)?.label), formData.category && /* @__PURE__ */ React4.createElement("span", null, categories.find((c) => c.value === formData.category)?.label), /* @__PURE__ */ React4.createElement("span", null, getWordCount(), " words"))), /* @__PURE__ */ React4.createElement(
MarkdownPreview,
{
content: formData.content,
parser: markdownParser
}
))))), /* @__PURE__ */ React4.createElement(TabsContent, { value: "settings", className: "p-4 lg:p-6" }, /* @__PURE__ */ React4.createElement("div", { className: "max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-6" }, /* @__PURE__ */ React4.createElement(Card, { brutal }, /* @__PURE__ */ React4.createElement("h3", { className: "text-sm font-bold uppercase mb-4" }, "Post Settings"), /* @__PURE__ */ React4.createElement("div", { className: "space-y-4" }, /* @__PURE__ */ React4.createElement(
Select,
{
label: "Category",
options: [
{ value: "", label: "Select Category" },
...categories
],
value: formData.category,
onChange: (e) => setFormData((prev) => ({
...prev,
category: e.target.value
})),
brutal
}
), /* @__PURE__ */ React4.createElement(
Select,
{
label: "Author",
options: [{ value: "", label: "Select Author" }, ...authors],
value: formData.author,
onChange: (e) => setFormData((prev) => ({
...prev,
author: e.target.value
})),
brutal
}
), /* @__PURE__ */ React4.createElement(
Input,
{
label: "Tags",
value: formData.tags,
onChange: (e) => setFormData((prev) => ({
...prev,
tags: e.target.value
})),
placeholder: "tag1, tag2, tag3",
brutal,
hint: "Separate tags with commas"
}
), /* @__PURE__ */ React4.createElement(
Select,
{
label: "Status",
options: [
{ value: "draft", label: "Draft" },
{ value: "published", label: "Published" }
],
value: formData.status,
onChange: (e) => setFormData((prev) => ({
...prev,
status: e.target.value
})),
brutal
}
), /* @__PURE__ */ React4.createElement(
Toggle,
{
label: "Featured Post",
checked: formData.isFeatured,
onChange: (checked) => setFormData((prev) => ({
...prev,
isFeatured: checked
})),
brutal
}
))), /* @__PURE__ */ React4.createElement(Card, { brutal }, /* @__PURE__ */ React4.createElement("h3", { className: "text-sm font-bold uppercase mb-4" }, "Editor Settings"), /* @__PURE__ */ React4.createElement("div", { className: "space-y-4" }, /* @__PURE__ */ React4.createElement(
Toggle,
{
label: "Auto-save",
checked: autoSave,
onChange: setAutoSave,
brutal
}
), /* @__PURE__ */ React4.createElement("div", null, /* @__PURE__ */ React4.createElement("label", { className: "block text-xs font-bold uppercase mb-2" }, "Featured Image"), formData.featuredImage ? /* @__PURE__ */ React4.createElement("div", { className: "space-y-2" }, /* @__PURE__ */ React4.createElement("div", { className: "border-4 border-brutal-black p-2" }, /* @__PURE__ */ React4.createElement(
"img",
{
src: formData.featuredImage,
alt: "Featured",
className: "w-full h-48 object-cover"
}
)), /* @__PURE__ */ React4.createElement(
Button,
{
variant: "danger",
size: "sm",
className: "w-full",
onClick: () => setFormData((prev) => ({
...prev,
featuredImage: ""
})),
brutal
},
"Remove Image"
)) : /* @__PURE__ */ React4.createElement(
Button,
{
variant: "secondary",
size: "sm",
className: "w-full",
leftIcon: () => /* @__PURE__ */ React4.createElement(FaImage, null),
brutal
},
"Upload Image"
)), /* @__PURE__ */ React4.createElement(
Input,
{
label: "Publish Date",
type: "datetime-local",
value: formData.publishedAt,
onChange: (e) => setFormData((prev) => ({
...prev,
publishedAt: e.target.value
})),
brutal,
leftIcon: FaClock
}
))), /* @__PURE__ */ React4.createElement(Card, { brutal, className: "md:col-span-2" }, /* @__PURE__ */ React4.createElement("h3", { className: "text-sm font-bold uppercase mb-4" }, "Statistics"), /* @__PURE__ */ React4.createElement("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4 text-sm" }, /* @__PURE__ */ React4.createElement("div", null, /* @__PURE__ */ React4.createElement("span", { className: "text-xs uppercase text-brutal-gray-600" }, "Words"), /* @__PURE__ */ React4.createElement("p", { className: "font-bold text-lg" }, getWordCount())), /* @__PURE__ */ React4.createElement("div", null, /* @__PURE__ */ React4.createElement("span", { className: "text-xs uppercase text-brutal-gray-600" }, "Characters"), /* @__PURE__ */ React4.createElement("p", { className: "font-bold text-lg" }, formData.content.length)), /* @__PURE__ */ React4.createElement("div", null, /* @__PURE__ */ React4.createElement("span", { className: "text-xs uppercase text-brutal-gray-600" }, "Paragraphs"), /* @__PURE__ */ React4.createElement("p", { className: "font-bold text-lg" }, formData.content.split("\n\n").filter(Boolean).length)), /* @__PURE__ */ React4.createElement("div", null, /* @__PURE__ */ React4.createElement("span", { className: "text-xs uppercase text-brutal-gray-600" }, "Reading Time"), /* @__PURE__ */ React4.createElement("p", { className: "font-bold text-lg" }, "~", Math.ceil(getWordCount() / 200), " min"))))))));
};
var EditorToolbar = ({
actions,
onAction,
onFormatText,
fullscreen = false,
onFullscreenToggle,
brutal = true,
variant = "default",
showGroups = ["text", "paragraph", "insert", "tools"],
className,
customActions
}) => {
const [showColorPicker, setShowColorPicker] = useState(false);
const [showLinkDialog, setShowLinkDialog] = useState(false);
const [linkUrl, setLinkUrl] = useState("");
const colorPickerRef = useRef(null);
const defaultActions = [
// Text formatting group
{
id: "bold",
icon: FaBold,
label: "Bold",
shortcut: "Ctrl+B",
action: () => onFormatText?.("bold"),
group: "text"
},
{
id: "italic",
icon: FaItalic,
label: "Italic",
shortcut: "Ctrl+I",
action: () => onFormatText?.("italic"),
group: "text"
},
{
id: "underline",
icon: FaUnderline,
label: "Underline",
shortcut: "Ctrl+U",
action: () => onFormatText?.("underline"),
group: "text"
},
{
id: "strike",
icon: FaStrikethrough,
label: "Strikethrough",
action: () => onFormatText?.("strikethrough"),
group: "text"
},
// Paragraph formatting group
{
id: "heading",
icon: FaHeading,
label: "Heading",
action: () => onFormatText?.("heading"),
group: "paragraph"
},
{
id: "unordered-list",
icon: FaListUl,
label: "Bullet List",
action: () => onFormatText?.("unorderedList"),
group: "paragraph"
},
{
id: "ordered-list",
icon: FaListOl,
label: "Numbered List",
action: () => onFormatText?.("orderedList"),
group: "paragraph"
},
{
id: "quote",
icon: FaQuoteLeft,
label: "Quote",
action: () => onFormatText?.("quote"),
group: "paragraph"
},
{
id: "code",
icon: FaCode,
label: "Code Block",
action: () => onFormatText?.("code"),
group: "paragraph"
},
// Alignment group
{
id: "align-left",
icon: FaAlignLeft,
label: "Align Left",
action: () => onFormatText?.("alignLeft"),
group: "align"
},
{
id: "align-center",
icon: FaAlignCenter,
label: "Align Center",
action: () => onFormatText?.("alignCenter"),
group: "align"
},
{
id: "align-right",
icon: FaAlignRight,
label: "Align Right",
action: () => onFormatText?.("alignRight"),
group: "align"
},
// Insert group
{
id: "link",
icon: FaLink,
label: "Insert Link",
shortcut: "Ctrl+K",
action: () => setShowLinkDialog(true),
group: "insert"
},
{
id: "image",
icon: FaImage,
label: "Insert Image",
action: () => onFormatText?.("image"),
group: "insert"
},
{
id: "table",
icon: FaTable,
label: "Insert Table",
action: () => onFormatText?.("table"),
group: "insert"
},
// Tools group
{
id: "color",
icon: FaPalette,
label: "Text Color",
action: () => setShowColorPicker(!showColorPicker),
group: "tools"
},
{
id: "highlight",
icon: FaHighlighter,
label: "Highlight",
action: () => onFormatText?.("highlight"),
group: "tools"
},
{
id: "indent",
icon: FaIndent,
label: "Increase Indent",
action: () => onFormatText?.("indent"),
group: "tools"
},
{
id: "outdent",
icon: FaOutdent,
label: "Decrease Indent",
action: () => onFormatText?.("outdent"),
group: "tools"
},
// History group
{
id: "undo",
icon: FaUndo,
label: "Undo",
shortcut: "Ctrl+Z",
action: () => onFormatText?.("undo"),
group: "history"
},
{
id: "redo",
icon: FaRedo,
label: "Redo",
shortcut: "Ctrl+Y",
action: () => onFormatText?.("redo"),
group: "history"
}
];
const toolbarActions = actions || defaultActions;
const groupedActions = toolbarActions.reduce(
(acc, action) => {
const group = action.group || "default";
if (!acc[group]) acc[group] = [];
acc[group].push(action);
return acc;
},
{}
);
const handleAction = (action) => {
if (action.disabled) return;
action.action();
if (onAction) onAction(action.id);
};
const handleLinkInsert = () => {
if (linkUrl && onFormatText) {
onFormatText("link", linkUrl);
setLinkUrl("");
setShowLinkDialog(false);
}
};
const colors = [
"#000000",
"#FFFFFF",
"#FFD6E8",
"#FFE5D6",
"#FFF3D6",
"#D6FFE5",
"#D6EDFF",
"#E8D6FF",
"#FFD6D6",
"#737373",
"#404040",
"#171717"
];
const variantClasses = {
default: clsx(
"flex flex-wrap items-center gap-1 p-2",
brutal && "border-b-4 border-brutal-black bg-brutal-gray-50",
!brutal && "border-b-2 border-brutal-gray-200 bg-brutal-gray-50"
),
minimal: "flex items-center gap-1 p-1",
floating: clsx(
"inline-flex items-center gap-1 p-2",
"bg-brutal-white rounded-lg",
brutal && "border-2 border-brutal-black shadow-brutal",
!brutal && "border border-brutal-gray-300 shadow-lg"
)
};
return /* @__PURE__ */ React4.createElement("div", { className: clsx(variantClasses[variant], className) }, showGroups.map((groupName) => {
const actions2 = groupedActions[groupName];
if (!actions2 || actions2.length === 0) return null;
return /* @__PURE__ */ React4.createElement("div", { key: groupName, className: "flex items-center" }, actions2.map((action) => /* @__PURE__ */ React4.createElement(
Tooltip,
{
key: action.id,
content: /* @__PURE__ */ React4.createElement("div", null, action.label, action.shortcut && /* @__PURE__ */ React4.createElement("span", { className: "ml-2 text-xs opacity-75" }, action.shortcut))
},
/* @__PURE__ */ React4.createElement(
"button",
{
type: "button",
onClick: () => handleAction(action),
disabled: action.disabled,
className: clsx(
"p-2 transition-all duration-200",
"hover:bg-brutal-gray-200",
action.active && "bg-brutal-black text-brutal-white",
action.disabled && "opacity-50 cursor-not-allowed"
),
"aria-label": action.label
},
/* @__PURE__ */ React4.createElement(Icon, { icon: action.icon, size: "sm" })
)
)), showGroups.indexOf(groupName) < showGroups.length - 1 && /* @__PURE__ */ React4.createElement("div", { className: "mx-1 w-px h-6 bg-brutal-gray-300" }));
}), showColorPicker && /* @__PURE__ */ React4.createElement(
"div",
{
ref: colorPickerRef,
className: clsx(
"absolute top-full left-0 mt-1 z-50",
"bg-brutal-white p-2",
brutal && "border-4 border-brutal-black shadow-brutal",
!brutal && "border-2 border-brutal-gray-300 shadow-lg"
)
},
/* @__PURE__ */ React4.createElement("div", { className: "grid grid-cols-6 gap-1" }, colors.map((color) => /* @__PURE__ */ React4.createElement(
"button",
{
key: color,
type: "button",
onClick: () => {
onFormatText?.("color", color);
setShowColorPicker(false);
},
className: clsx(
"w-8 h-8 border-2 border-brutal-black",
"hover:scale-110 transition-transform"
),
style: { backgroundColor: color },
"aria-label": `Color ${color}`
}
)))
), showLinkDialog && /* @__PURE__ */ React4.createElement(
"div",
{
className: clsx(
"absolute top-full left-0 mt-1 z-50",
"bg-brutal-white p-4",
brutal && "border-4 border-brutal-black shadow-brutal",
!brutal && "border-2 border-brutal-gray-300 shadow-lg"
)
},
/* @__PURE__ */ React4.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React4.createElement(
"input",
{
type: "url",
value: linkUrl,
onChange: (e) => setLinkUrl(e.target.value),
placeholder: "Enter URL...",
className: clsx(
"px-3 py-2 text-sm",
brutal && "border-2 border-brutal-black",
!brutal && "border border-brutal-gray-300",
"focus:outline-none"
),
onKeyDown: (e) => {
if (e.key === "Enter") handleLinkInsert();
if (e.key === "Escape") setShowLinkDialog(false);
},
autoFocus: true
}
), /* @__PURE__ */ React4.createElement(Button, { size: "sm", onClick: handleLinkInsert, brutal }, /* @__PURE__ */ React4.createElement(FaPlus, null), "Insert"), /* @__PURE__ */ React4.createElement(
Button,
{
size: "sm",
variant: "ghost",
onClick: () => setShowLinkDialog(false),
brutal
},
/* @__PURE__ */ React4.createElement(FaTimes, null)
))
), customActions && /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement("div", { className: "mx-1 w-px h-6 bg-brutal-gray-300" }), customActions), onFullscreenToggle && /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement("div", { className: "mx-1 w-px h-6 bg-brutal-gray-300" }), /* @__PURE__ */ React4.createElement(
Tooltip,
{
content: fullscreen ? "Exit Fullscreen" : "Enter Fullscreen"
},
/* @__PURE__ */ React4.createElement(
"button",
{
type: "button",
onClick: onFullscreenToggle,
className: clsx(
"p-2 transition-all duration-200",
"hover:bg-brutal-gray-200",
fullscreen && "bg-brutal-black text-brutal-white"
),
"aria-label": fullscreen ? "Exit Fullscreen" : "Enter Fullscreen"
},
/* @__PURE__ */ React4.createElement(Icon, { icon: fullscreen ? FaCompress : FaExpand, size: "sm" })
)
)));
};
/**
* @file src/modules/editor/MarkdownToolbar/MarkdownToolbar.tsx
* @author David (https://dvh.sh)
* @license MIT
*
* @created Fri Sep 12 2025
* @updated Fri Sep 12 2025
*
* @description
* Toolbar for markdown editor with common formatting actions
* @client This component requires client-side JavaScript
*/
/**
* @file src/modules/editor/MarkdownEditor/MarkdownEditor.tsx
* @author David (https://dvh.sh)
* @license MIT
*
* @created Fri Sep 12 2025
* @updated Fri Sep 12 2025
*
* @description
* Lightweight markdown editor component without preview
* @client This component requires client-side JavaScript
*/
/**
* @file src/modules/editor/MarkdownPreview/MarkdownPreview.tsx
* @author David (https://dvh.sh)
* @license MIT
*
* @created Fri Sep 12 2025
* @updated Fri Sep 12 2025
*
* @description
* Markdown preview component (requires marked or similar parser)
* @client This component requires client-side JavaScript
*/
/**
* @file src/modules/editor/BlogEditor/BlogEditor.tsx
* @author David (https://dvh.sh)
* @license MIT
*
* @created Fri Sep 12 2025
* @updated Fri Sep 12 2025
*
* @description
* Complete blog editor with tabs for write/preview/settings
* @client This component requires client-side JavaScript
*/
/**
* @file src/modules/editor/EditorToolbar/EditorToolbar.tsx
* @author David (https://dvh.sh)
* @license MIT
*
* @created Fri Sep 12 2025
* @updated Fri Sep 12 2025
*
* @description
* Advanced editor toolbar with formatting options and custom actions
* @client This component requires client-side JavaScript
*/
/**
* @file src/modules/editor/index.ts
* @author David (https://dvh.sh)
* @license MIT
*
* @created Thu Sep 12 2025
* @updated Fri Sep 12 2025
*
* @description
*
*/
export { BlogEditor, EditorToolbar, MarkdownEditor, MarkdownPreview, MarkdownToolbar };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map