Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion electron/lib/media/imageMedia.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,68 @@ function buildPdfExportHtml({ title, markdownContent, baseHref, sourceDir, downs
return renderImageHtmlWithAnnotation(defaultImage(tokens, idx, options, env, self), annotation);
};

const bodyHtml = markdown.render(markdownContent || "");
function processCallouts(html) {
if (!html || typeof html !== "string" || !html.includes("<blockquote")) return html;

const calloutConfigs = {
NOTE: { title: "Note", icon: "ℹ️", class: "callout-note" },
INFO: { title: "Info", icon: "ℹ️", class: "callout-note" },
WARNING: { title: "Warning", icon: "⚠️", class: "callout-warning" },
TIP: { title: "Tip", icon: "💡", class: "callout-tip" },
HINT: { title: "Hint", icon: "💡", class: "callout-tip" },
TODO: { title: "Todo", icon: "📝", class: "callout-todo" },
IMPORTANT: { title: "Important", icon: "🌟", class: "callout-important" },
CAUTION: { title: "Caution", icon: "🚫", class: "callout-caution" },
DANGER: { title: "Danger", icon: "🚫", class: "callout-caution" },
ERROR: { title: "Error", icon: "🚫", class: "callout-caution" },
BUG: { title: "Bug", icon: "🐛", class: "callout-caution" },
SUCCESS: { title: "Success", icon: "✅", class: "callout-tip" },
QUESTION: { title: "Question", icon: "❓", class: "callout-todo" },
QUOTE: { title: "Quote", icon: "💬", class: "callout-note" },
ABSTRACT: { title: "Abstract", icon: "📋", class: "callout-important" },
SUMMARY: { title: "Summary", icon: "📋", class: "callout-important" },
EXAMPLE: { title: "Example", icon: "🔍", class: "callout-note" },
};

const bqRegex = /<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi;

return html.replace(bqRegex, (fullMatch, innerContent) => {
const headerMatch = innerContent.match(/^\s*(?:<p[^>]*>\s*)?\[!([A-Za-z0-9_-]+)\]([^\n<]*)(?:<br\s*\/?>)?/i);
if (!headerMatch) return fullMatch;

const rawType = headerMatch[1].toUpperCase();
const customTitle = headerMatch[2].trim();
const config = calloutConfigs[rawType] || {
title: rawType.charAt(0) + rawType.slice(1).toLowerCase(),
icon: "📌",
class: "callout-note",
};

const displayTitle = customTitle || config.title;

let bodyContent = innerContent.replace(headerMatch[0], "").trim();
bodyContent = bodyContent.replace(/^<\/p>/i, "").trim();

if (bodyContent && !bodyContent.startsWith("<p>") && !bodyContent.startsWith("<div") && !bodyContent.startsWith("<ul") && !bodyContent.startsWith("<ol")) {
bodyContent = `<p>${bodyContent}`;
}
if (bodyContent && bodyContent.startsWith("<p>") && !bodyContent.endsWith("</p>")) {
bodyContent = `${bodyContent}</p>`;
}

return `<div class="notely-callout ${config.class}">
<div class="notely-callout-header">
<span class="notely-callout-icon">${config.icon}</span>
<span class="notely-callout-title">${displayTitle}</span>
</div>
<div class="notely-callout-body">
${bodyContent}
</div>
</div>`;
});
}

const bodyHtml = processCallouts(markdown.render(markdownContent || ""));

return `<!doctype html>
<html lang="en">
Expand Down
46 changes: 46 additions & 0 deletions electron/lib/media/pdf.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,52 @@ function buildPdfStyles({ compact = false } = {}) {

.notely-image-annotation { left: 10px; top: 10px; }

/* Callouts styling for PDF export */
.notely-callout {
margin: 14px 0;
padding: 12px 16px;
border-radius: 6px;
border-left: 4px solid #3b82f6;
background-color: rgba(59, 130, 246, 0.08);
break-inside: avoid-page;
page-break-inside: avoid;
}
.notely-callout-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 6px;
color: #1e40af;
}
.notely-callout-icon {
font-size: 14px;
line-height: 1;
}
.notely-callout-body {
font-size: 13px;
line-height: 1.6;
color: #0d1f26;
}
.notely-callout-body p:first-child { margin-top: 0; }
.notely-callout-body p:last-child { margin-bottom: 0; }

.notely-callout.callout-note { border-left-color: #3b82f6; background-color: rgba(59, 130, 246, 0.08); }
.notely-callout.callout-note .notely-callout-header { color: #2563eb; }
.notely-callout.callout-warning { border-left-color: #f59e0b; background-color: rgba(245, 158, 11, 0.08); }
.notely-callout.callout-warning .notely-callout-header { color: #d97706; }
.notely-callout.callout-tip { border-left-color: #10b981; background-color: rgba(16, 185, 129, 0.08); }
.notely-callout.callout-tip .notely-callout-header { color: #059669; }
.notely-callout.callout-todo { border-left-color: #8b5cf6; background-color: rgba(139, 92, 246, 0.08); }
.notely-callout.callout-todo .notely-callout-header { color: #7c3aed; }
.notely-callout.callout-important { border-left-color: #06b6d4; background-color: rgba(6, 182, 212, 0.08); }
.notely-callout.callout-important .notely-callout-header { color: #0891b2; }
.notely-callout.callout-caution { border-left-color: #ef4444; background-color: rgba(239, 68, 68, 0.08); }
.notely-callout.callout-caution .notely-callout-header { color: #dc2626; }

@media print {
pre,
.markdown-code-pre {
Expand Down
89 changes: 85 additions & 4 deletions electron/lib/web/websiteRenderer.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,75 @@
activeSection = "cleansed";
}

let bodyHtml = `<div class="doc-hero"><h1>${escapeHtml(parsed.title || path.basename(relMdPath, ".md"))}</h1><p class="doc-breadcrumb">${escapeHtml(relMdPath)}</p></div><div class="doc-body prose">${markdown.render(normalizedContent, { relMdPath })}</div>`;
function processCallouts(html) {
if (!html || typeof html !== "string" || !html.includes("<blockquote")) return html;

const calloutConfigs = {
NOTE: { title: "Note", icon: "ℹ️", class: "callout-note" },
INFO: { title: "Info", icon: "ℹ️", class: "callout-note" },
WARNING: { title: "Warning", icon: "⚠️", class: "callout-warning" },
TIP: { title: "Tip", icon: "💡", class: "callout-tip" },
HINT: { title: "Hint", icon: "💡", class: "callout-tip" },
TODO: { title: "Todo", icon: "📝", class: "callout-todo" },
IMPORTANT: { title: "Important", icon: "🌟", class: "callout-important" },
CAUTION: { title: "Caution", icon: "🚫", class: "callout-caution" },
DANGER: { title: "Danger", icon: "🚫", class: "callout-caution" },
ERROR: { title: "Error", icon: "🚫", class: "callout-caution" },
BUG: { title: "Bug", icon: "🐛", class: "callout-caution" },
SUCCESS: { title: "Success", icon: "✅", class: "callout-tip" },
QUESTION: { title: "Question", icon: "❓", class: "callout-todo" },
QUOTE: { title: "Quote", icon: "💬", class: "callout-note" },
ABSTRACT: { title: "Abstract", icon: "📋", class: "callout-important" },
SUMMARY: { title: "Summary", icon: "📋", class: "callout-important" },
EXAMPLE: { title: "Example", icon: "🔍", class: "callout-note" },
};

const bqRegex = /<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi;

return html.replace(bqRegex, (fullMatch, innerContent) => {
const headerMatch = innerContent.match(/^\s*(?:<p[^>]*>\s*)?\[!([A-Za-z0-9_-]+)\]([^\n<]*)(?:<br\s*\/?>)?/i);
if (!headerMatch) return fullMatch;

const rawType = headerMatch[1].toUpperCase();
const customTitle = headerMatch[2].trim();
const config = calloutConfigs[rawType] || {
title: rawType.charAt(0) + rawType.slice(1).toLowerCase(),
icon: "📌",
class: "callout-note",
};

const displayTitle = customTitle || config.title;

let bodyContent = innerContent.replace(headerMatch[0], "").trim();
bodyContent = bodyContent.replace(/^<\/p>/i, "").trim();

if (bodyContent && !bodyContent.startsWith("<p>") && !bodyContent.startsWith("<div") && !bodyContent.startsWith("<ul") && !bodyContent.startsWith("<ol")) {
bodyContent = `<p>${bodyContent}`;
}
if (bodyContent && bodyContent.startsWith("<p>") && !bodyContent.endsWith("</p>")) {
bodyContent = `${bodyContent}</p>`;
}

return `<div class="notely-callout ${config.class}">
<div class="notely-callout-header">
<span class="notely-callout-icon">${config.icon}</span>
<span class="notely-callout-title">${displayTitle}</span>
</div>
<div class="notely-callout-body">
${bodyContent}
</div>
</div>`;
});
}

let bodyHtml = `<div class="doc-hero"><h1>${escapeHtml(parsed.title || path.basename(relMdPath, ".md"))}</h1><p class="doc-breadcrumb">${escapeHtml(relMdPath)}</p></div><div class="doc-body prose">${processCallouts(markdown.render(normalizedContent, { relMdPath }))}</div>`;
if (hasTabbedSections) {
const headerHtml = buildMetadataHtml(parsed);
const rawHtml = parsed.rawNotes
? markdown.render(parsed.rawNotes, { relMdPath })
? processCallouts(markdown.render(parsed.rawNotes, { relMdPath }))
: `<p class="tab-empty">No raw notes captured yet.</p>`;
const cleansedHtml = parsed.cleansed
? markdown.render(parsed.cleansed, { relMdPath })
? processCallouts(markdown.render(parsed.cleansed, { relMdPath }))
: `<p class="tab-empty">No cleansed notes captured yet.</p>`;

bodyHtml = `
Expand Down Expand Up @@ -499,6 +560,26 @@
.content hr { border: 0; border-top: 1px solid var(--border); margin: 24px 0; }
.empty { color: var(--ink-3); font-style: italic; }

/* Callout styling for website & pdf note views */
.notely-callout { margin: 16px 0; padding: 12px 16px; border-radius: 8px; border-left: 4px solid #3b82f6; background-color: rgba(59, 130, 246, 0.08); box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.notely-callout-header { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 0.9em; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px; color: #1e40af; }
.notely-callout-icon { font-size: 1.1em; line-height: 1; }
.notely-callout-body { font-size: 0.95em; line-height: 1.6; color: inherit; }
.notely-callout-body p:first-child { margin-top: 0; }
.notely-callout-body p:last-child { margin-bottom: 0; }
.notely-callout.callout-note { border-left-color: #3b82f6; background-color: rgba(59, 130, 246, 0.08); }
.notely-callout.callout-note .notely-callout-header { color: #2563eb; }
.notely-callout.callout-warning { border-left-color: #f59e0b; background-color: rgba(245, 158, 11, 0.08); }
.notely-callout.callout-warning .notely-callout-header { color: #d97706; }
.notely-callout.callout-tip { border-left-color: #10b981; background-color: rgba(16, 185, 129, 0.08); }
.notely-callout.callout-tip .notely-callout-header { color: #059669; }
.notely-callout.callout-todo { border-left-color: #8b5cf6; background-color: rgba(139, 92, 246, 0.08); }
.notely-callout.callout-todo .notely-callout-header { color: #7c3aed; }
.notely-callout.callout-important { border-left-color: #06b6d4; background-color: rgba(6, 182, 212, 0.08); }
.notely-callout.callout-important .notely-callout-header { color: #0891b2; }
.notely-callout.callout-caution { border-left-color: #ef4444; background-color: rgba(239, 68, 68, 0.08); }
.notely-callout.callout-caution .notely-callout-header { color: #dc2626; }

@page { margin: 18mm 16mm; }
@media print {
.page { padding: 0; max-width: none; }
Expand All @@ -508,7 +589,7 @@
<body>
<main class="page">
<article class="content">
${contentToRender ? markdown.render(contentToRender, { relMdPath }) : `<p class="empty">${emptyMessage}</p>`}
${contentToRender ? processCallouts(markdown.render(contentToRender, { relMdPath })) : `<p class="empty">${emptyMessage}</p>`}

Check failure on line 592 in electron/lib/web/websiteRenderer.cjs

View workflow job for this annotation

GitHub Actions / build-and-test

'processCallouts' is not defined
</article>
</main>
</body>
Expand Down
135 changes: 86 additions & 49 deletions src/components/MarkdownToolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -932,65 +932,102 @@ export function MarkdownToolbar({
</AppIconButton>
{snippet.key === "callout" && showCalloutPicker && (
<div
className="editor-context-menu callout-picker-menu"
className="callout-picker-popover"
ref={calloutPopoverRef}
style={{
position: "absolute",
top: "100%",
left: 0,
marginTop: "4px",
zIndex: 9999,
background: "var(--surface-bg, #ffffff)",
backdropFilter: "blur(12px)",
border: "1px solid var(--border-soft, #e2e8f0)",
borderRadius: "8px",
boxShadow: "0 10px 25px -5px rgba(0, 0, 0, 0.18), 0 8px 10px -6px rgba(0, 0, 0, 0.1)",
padding: "6px",
minWidth: "170px",
background: "var(--surface-elevated)",
border: "1px solid var(--border-soft)",
borderRadius: "var(--radius-xl, 12px)",
boxShadow: "var(--shadow-xl)",
padding: "8px",
zIndex: 100,
width: "280px",
}}
role="menu"
>
<div style={{ fontSize: "10px", fontWeight: 700, textTransform: "uppercase", padding: "4px 8px 6px 8px", color: "var(--text-muted)", letterSpacing: "0.06em", borderBottom: "1px solid var(--border-soft, #f1f5f9)", marginBottom: "4px" }}>
Callout Box
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
fontSize: "10px",
fontWeight: 700,
textTransform: "uppercase",
padding: "2px 6px 6px",
color: "var(--text-muted)",
letterSpacing: "0.06em",
borderBottom: "1px solid var(--border-soft)",
marginBottom: "6px",
}}
>
<span>Callout Box</span>
<span style={{ fontSize: "9px", opacity: 0.75, fontWeight: 500 }}>17 types</span>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "3px",
maxHeight: "260px",
overflowY: "auto",
paddingRight: "2px",
}}
>
{[
{ label: "Note", icon: "ℹ️", before: "> [!NOTE]\n> " },
{ label: "Info", icon: "ℹ️", before: "> [!INFO]\n> " },
{ label: "Warning", icon: "⚠️", before: "> [!WARNING]\n> " },
{ label: "Tip", icon: "💡", before: "> [!TIP]\n> " },
{ label: "Hint", icon: "💡", before: "> [!HINT]\n> " },
{ label: "Todo", icon: "📝", before: "> [!TODO]\n> " },
{ label: "Important", icon: "🌟", before: "> [!IMPORTANT]\n> " },
{ label: "Caution", icon: "🚫", before: "> [!CAUTION]\n> " },
{ label: "Danger", icon: "🚫", before: "> [!DANGER]\n> " },
{ label: "Error", icon: "🚫", before: "> [!ERROR]\n> " },
{ label: "Bug", icon: "🐛", before: "> [!BUG]\n> " },
{ label: "Success", icon: "✅", before: "> [!SUCCESS]\n> " },
{ label: "Question", icon: "❓", before: "> [!QUESTION]\n> " },
{ label: "Quote", icon: "💬", before: "> [!QUOTE]\n> " },
{ label: "Abstract", icon: "📋", before: "> [!ABSTRACT]\n> " },
{ label: "Summary", icon: "📋", before: "> [!SUMMARY]\n> " },
{ label: "Example", icon: "🔍", before: "> [!EXAMPLE]\n> " },
].map((item) => (
<button
key={item.label}
type="button"
role="menuitem"
onClick={() => {
applySnippet(value, onChange, textareaRef, item.before, "", "Callout content");
setShowCalloutPicker(false);
}}
className="callout-picker-item"
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: "7px",
width: "100%",
textAlign: "left",
padding: "5px 8px",
border: "none",
borderRadius: "var(--radius-md, 6px)",
background: "transparent",
color: "var(--app-text)",
cursor: "pointer",
fontSize: "12px",
fontWeight: 500,
transition: "background var(--motion-fast)",
}}
>
<span style={{ fontSize: "13px", lineHeight: 1 }}>{item.icon}</span>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{item.label}</span>
</button>
))}
</div>
{[
{ label: "Note", icon: "ℹ️", before: "> [!NOTE]\n> " },
{ label: "Warning", icon: "⚠️", before: "> [!WARNING]\n> " },
{ label: "Tip", icon: "💡", before: "> [!TIP]\n> " },
{ label: "Todo", icon: "📝", before: "> [!TODO]\n> " },
{ label: "Important", icon: "🌟", before: "> [!IMPORTANT]\n> " },
{ label: "Caution", icon: "🚫", before: "> [!CAUTION]\n> " },
].map((item) => (
<button
key={item.label}
type="button"
role="menuitem"
onClick={() => {
applySnippet(value, onChange, textareaRef, item.before, "", "Callout content");
setShowCalloutPicker(false);
}}
className="callout-picker-item"
style={{
display: "flex",
alignItems: "center",
gap: "10px",
width: "100%",
textAlign: "left",
padding: "7px 10px",
border: "none",
borderRadius: "6px",
background: "transparent",
color: "var(--app-text)",
cursor: "pointer",
fontSize: "13px",
fontWeight: 500,
transition: "all 0.12s ease",
}}
>
<span style={{ fontSize: "15px", lineHeight: 1 }}>{item.icon}</span>
<span>{item.label}</span>
</button>
))}
</div>
)}
</div>
Expand Down
Loading
Loading