From 18ffaebb3cc529abd181f74361d93328c1a39db4 Mon Sep 17 00:00:00 2001 From: Michal Vrba Date: Tue, 29 Sep 2026 13:10:20 +0200 Subject: [PATCH] styling --- .gitignore | 2 + Notemark/Components/Pages/Editor.razor | 204 +++++- Notemark/Components/Pages/Editor.razor.css | 671 +++++++++++++++++- Notemark/Components/Pages/SharedPreview.razor | 8 +- .../Components/Pages/SharedPreview.razor.css | 471 +++++++++++- Notemark/Models/DocumentMetadata.cs | 1 + Notemark/Program.cs | 1 + Notemark/Services/AutoStyleService.cs | 473 ++++++++++++ Notemark/Services/DocumentService.cs | 4 +- Notemark/Services/IAutoStyleService.cs | 9 + Notemark/Services/IDocumentService.cs | 2 +- Notemark/Services/MarkdownService.cs | 120 +++- Notemark/data/documents.json | 11 - Notemark/wwwroot/app.css | 131 ++++ Notemark/wwwroot/js/editor.js | 381 +++++++++- 15 files changed, 2427 insertions(+), 62 deletions(-) create mode 100644 Notemark/Services/AutoStyleService.cs create mode 100644 Notemark/Services/IAutoStyleService.cs delete mode 100644 Notemark/data/documents.json diff --git a/.gitignore b/.gitignore index 6f4ad01..c3c5bff 100644 --- a/.gitignore +++ b/.gitignore @@ -363,3 +363,5 @@ MigrationBackup/ FodyWeavers.xsd /Notemark/data/shares /Notemark/data/sessions/b5acffbe-706e-405e-b006-3415508a59a7.md +/Notemark/data/sessions +/Notemark/data/documents.json diff --git a/Notemark/Components/Pages/Editor.razor b/Notemark/Components/Pages/Editor.razor index 051c857..adb3d55 100644 --- a/Notemark/Components/Pages/Editor.razor +++ b/Notemark/Components/Pages/Editor.razor @@ -8,6 +8,7 @@ @inject ISessionService SessionService @inject IDocumentService DocumentService @inject IMarkdownService MarkdownService +@inject IAutoStyleService AutoStyleService @inject IShareService ShareService @inject IJSRuntime JSRuntime @inject IHttpContextAccessor HttpContextAccessor @@ -101,6 +102,55 @@ + +
+ + + @if (showStyleDropdown) + { + +
+ + + + + +
+ } +
+
+ + @if (!string.IsNullOrEmpty(autoStyleToastMessage)) + { + + } +
- + +
@@ -257,7 +338,7 @@ {
- Live Preview + Live Preview (@GetDocStyleDisplayName(documentStyle))
-
+
@((MarkupString)renderedHtml)
@@ -303,6 +384,13 @@ private bool isFlagged = false; private int flaggedCount = 0; private List currentTags = new(); + private string documentStyle = "modern"; + private bool showStyleDropdown = false; + private bool isAutoStyling = false; + private bool isAutoStyledSuccess = false; + private string? autoStyleToastMessage = null; + private string autoStyleToastType = "success"; + private Timer? toastDismissTimer; private string markdownContent = ""; private string renderedHtml = ""; private bool showPreview = true; @@ -367,12 +455,14 @@ documentTitle = meta.Title; isFlagged = meta.IsFlagged; currentTags = meta.Tags; + documentStyle = !string.IsNullOrWhiteSpace(meta.DocStyle) ? meta.DocStyle : "modern"; } else { documentTitle = DocumentService.ExtractTitle(markdownContent); isFlagged = false; currentTags = DocumentService.ExtractTags(markdownContent); + documentStyle = "modern"; } undoStack.Clear(); @@ -537,7 +627,7 @@ try { await SessionService.SaveSessionContentAsync(sessionId, markdownContent); - await DocumentService.SaveDocumentAsync(sessionId, markdownContent, isFlagged, documentTitle, currentTags); + await DocumentService.SaveDocumentAsync(sessionId, markdownContent, isFlagged, documentTitle, currentTags, documentStyle); hasUnsavedChanges = false; saveStatus = "Saved"; await UpdateFlaggedCount(); @@ -599,6 +689,101 @@ } } + private async Task AutoStyleDocument() + { + if (string.IsNullOrWhiteSpace(markdownContent)) + { + ShowToast("Document is empty", "info"); + return; + } + + isAutoStyling = true; + StateHasChanged(); + await Task.Delay(250); // Visual cue for user that action is running + + try + { + var formatted = AutoStyleService.FormatMarkdown(markdownContent); + if (formatted != markdownContent) + { + undoStack.Add(markdownContent); + if (undoStack.Count > 50) undoStack.RemoveAt(0); + redoStack.Clear(); + + markdownContent = formatted; + hasUnsavedChanges = true; + UpdateStatistics(); + if (showPreview) + { + renderedHtml = MarkdownService.RenderToHtml(markdownContent); + } + await SaveContent(); + isAutoStyledSuccess = true; + ShowToast("Document styled & formatted! ✨", "success"); + } + else + { + ShowToast("Document is already well formatted! ✓", "info"); + } + } + catch (Exception ex) + { + ShowToast($"Formatting error: {ex.Message}", "error"); + } + finally + { + isAutoStyling = false; + StateHasChanged(); + + _ = Task.Run(async () => + { + await Task.Delay(2200); + isAutoStyledSuccess = false; + await InvokeAsync(StateHasChanged); + }); + } + } + + private void ShowToast(string message, string type = "success") + { + autoStyleToastMessage = message; + autoStyleToastType = type; + toastDismissTimer?.Dispose(); + toastDismissTimer = new Timer(_ => + { + autoStyleToastMessage = null; + InvokeAsync(StateHasChanged); + }, null, 3000, Timeout.Infinite); + } + + private void ToggleStyleDropdown() + { + showStyleDropdown = !showStyleDropdown; + } + + private void CloseStyleDropdown() + { + showStyleDropdown = false; + } + + private async Task SetDocumentStyle(string style) + { + documentStyle = style; + showStyleDropdown = false; + hasUnsavedChanges = true; + await SaveContent(); + ShowToast($"Style set to {GetDocStyleDisplayName(style)}! 🎨", "success"); + StateHasChanged(); + } + + private static string GetDocStyleDisplayName(string style) => style switch + { + "academic" => "Academic", + "technical" => "Technical", + "executive" => "Executive", + _ => "Modern" + }; + private async Task OnKeyDown(KeyboardEventArgs e) { if ((e.CtrlKey || e.MetaKey) && e.Key.Equals("s", StringComparison.OrdinalIgnoreCase)) @@ -617,6 +802,11 @@ { Redo(); } + else if ((e.ShiftKey && e.AltKey && e.Key.Equals("f", StringComparison.OrdinalIgnoreCase)) || + (e.CtrlKey && e.ShiftKey && e.Key.Equals("f", StringComparison.OrdinalIgnoreCase))) + { + await AutoStyleDocument(); + } else if ((e.CtrlKey || e.MetaKey) && e.Key.Equals("b", StringComparison.OrdinalIgnoreCase)) { await InsertFormatting("**", "**", "bold text"); @@ -656,6 +846,7 @@ markdownContent = "# Untitled Note\n\nStart typing here... #ideas"; documentTitle = "Untitled Note"; isFlagged = false; + documentStyle = "modern"; hasUnsavedChanges = false; saveStatus = "Saved"; @@ -665,7 +856,7 @@ UpdateStatistics(); renderedHtml = MarkdownService.RenderToHtml(markdownContent); - await DocumentService.SaveDocumentAsync(sessionId, markdownContent, isFlagged, documentTitle, currentTags); + await DocumentService.SaveDocumentAsync(sessionId, markdownContent, isFlagged, documentTitle, currentTags, documentStyle); await SessionService.SaveSessionContentAsync(sessionId, markdownContent); await UpdateFlaggedCount(); @@ -693,7 +884,7 @@ showExportDropdown = false; var filename = $"{documentTitle.Replace(" ", "_").ToLowerInvariant()}.html"; var currentTheme = await JSRuntime.InvokeAsync("notemarkTheme.getCurrentTheme"); - await JSRuntime.InvokeVoidAsync("notemarkEditor.downloadHtml", filename, documentTitle, renderedHtml, currentTheme); + await JSRuntime.InvokeVoidAsync("notemarkEditor.downloadHtml", filename, documentTitle, renderedHtml, currentTheme, documentStyle); } private async Task PrintDocument() @@ -864,6 +1055,7 @@ public void Dispose() { + toastDismissTimer?.Dispose(); autoSaveTimer?.Dispose(); dotNetRef?.Dispose(); } diff --git a/Notemark/Components/Pages/Editor.razor.css b/Notemark/Components/Pages/Editor.razor.css index 0f738a8..faf4da0 100644 --- a/Notemark/Components/Pages/Editor.razor.css +++ b/Notemark/Components/Pages/Editor.razor.css @@ -264,6 +264,52 @@ cursor: not-allowed; } +.toolbar-text { + margin-left: 0.25rem; +} + +.btn-auto-style { + font-weight: 600; + color: var(--accent); + background-color: var(--accent-light); + border: 1px solid var(--accent); + gap: 0.3rem; + padding: 0.3rem 0.65rem; +} + +.btn-auto-style:hover:not(:disabled) { + background-color: var(--accent); + color: #ffffff; +} + +.btn-auto-style.styling { + background-color: var(--accent); + color: #ffffff; + border-color: var(--accent); + cursor: wait; +} + +.spinner-inline { + display: inline-block; + animation: spinIcon 1s linear infinite; +} + +@keyframes spinIcon { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.btn-auto-style.success { + background-color: rgba(16, 185, 129, 0.15); + color: var(--success); + border-color: var(--success); +} + +.success-icon { + font-weight: 800; + color: var(--success); +} + .toolbar-divider { width: 1px; height: 18px; @@ -598,8 +644,7 @@ /* Tables */ .preview-content table { width: 100%; - border-collapse: separate; - border-spacing: 0; + border-collapse: collapse; margin: 1.5em 0; border: 1px solid var(--border); border-radius: 8px; @@ -610,24 +655,15 @@ .preview-content th, .preview-content td { padding: 0.75rem 1rem; - border-bottom: 1px solid var(--border); - border-right: 1px solid var(--border); + border: 1px solid var(--border); text-align: left; } -.preview-content th:last-child, -.preview-content td:last-child { - border-right: none; -} - -.preview-content tr:last-child td { - border-bottom: none; -} - .preview-content th { background-color: var(--surface-hover); font-weight: 600; color: var(--primary-text); + border-bottom: 2px solid var(--border); } .preview-content tr:hover td { @@ -811,6 +847,149 @@ background-color: var(--accent-hover); } +/* Document Style Dropdown in Top Bar */ +.style-container { + position: relative; + display: inline-block; +} + +.btn-style-nav { + display: inline-flex; + align-items: center; + gap: 0.35rem; +} + +.btn-caret { + font-size: 0.75rem; + opacity: 0.7; + margin-left: 0.15rem; +} + +.style-dropdown { + position: absolute; + top: 100%; + right: 0; + margin-top: 0.5rem; + background-color: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + box-shadow: var(--shadow-lg); + padding: 0.5rem; + min-width: 260px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 0.25rem; + animation: dropdownFadeIn 0.15s cubic-bezier(0.16, 1, 0.3, 1); +} + +.dropdown-header-title { + font-size: 0.72rem; + font-weight: 700; + color: var(--secondary-text); + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 0.35rem 0.65rem 0.2rem 0.65rem; +} + +.style-menu-item { + display: flex; + align-items: center; + gap: 0.6rem; + width: 100%; + padding: 0.5rem 0.65rem; + border: none; + background: none; + text-align: left; + border-radius: 6px; + cursor: pointer; + color: var(--primary-text); + transition: all 0.15s ease; +} + +.style-menu-item:hover { + background-color: var(--surface-hover); +} + +.style-menu-item.selected { + background-color: var(--accent-light); + color: var(--accent); +} + +.style-item-icon { + font-size: 1.15rem; + flex-shrink: 0; +} + +.style-item-text { + display: flex; + flex-direction: column; + flex: 1; +} + +.style-item-name { + font-weight: 600; + font-size: 0.84rem; +} + +.style-item-desc { + font-size: 0.72rem; + color: var(--secondary-text); +} + +.style-check { + font-weight: 700; + color: var(--accent); + margin-left: auto; +} + +/* Toast Notification */ +.notemark-toast { + position: fixed; + top: 60px; + left: 50%; + transform: translateX(-50%); + display: inline-flex; + align-items: center; + gap: 0.6rem; + padding: 0.65rem 1.25rem; + border-radius: 999px; + font-size: 0.875rem; + font-weight: 600; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + z-index: 2000; + animation: toastSlideDown 0.25s cubic-bezier(0.16, 1, 0.3, 1); + background-color: var(--surface); + color: var(--primary-text); + border: 1.5px solid var(--border); +} + +@keyframes toastSlideDown { + from { opacity: 0; transform: translate(-50%, -12px); } + to { opacity: 1; transform: translate(-50%, 0); } +} + +.notemark-toast.success { + border-color: var(--success); +} + +.notemark-toast.info { + border-color: var(--accent); +} + +.notemark-toast.error { + border-color: var(--error); + color: var(--error); +} + +.toast-icon { + font-size: 1.1rem; +} + +.toast-message { + line-height: 1.2; +} + /* Mobile Responsive */ @media screen and (max-width: 768px) { .top-bar { @@ -862,6 +1041,461 @@ } } +/* ========================================================= */ +/* DOCUMENT PRESET STYLES & GITHUB ALERTS */ +/* ========================================================= */ + +/* Callouts / GitHub Alerts */ +.preview-content .markdown-alert, +.markdown-alert { + padding: 0.75rem 1rem; + margin: 1.25rem 0; + border-left: 4px solid; + border-radius: 0 6px 6px 0; + background-color: var(--surface-hover); +} + +.preview-content .markdown-alert-title, +.markdown-alert-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + margin-bottom: 0.35rem; +} + +.preview-content .markdown-alert-note, .markdown-alert-note { border-left-color: #3b82f6; background-color: rgba(59, 130, 246, 0.08); } +.preview-content .markdown-alert-tip, .markdown-alert-tip { border-left-color: #10b981; background-color: rgba(16, 185, 129, 0.08); } +.preview-content .markdown-alert-important, .markdown-alert-important { border-left-color: #8b5cf6; background-color: rgba(139, 92, 246, 0.08); } +.preview-content .markdown-alert-warning, .markdown-alert-warning { border-left-color: #f59e0b; background-color: rgba(245, 158, 11, 0.08); } +.preview-content .markdown-alert-caution, .markdown-alert-caution { border-left-color: #ef4444; background-color: rgba(239, 68, 68, 0.08); } + +/* ========================================================= */ +/* DISTINCT DOCUMENT PRESET STYLES */ +/* ========================================================= */ + +/* === PRESET 1: MODERN (Clean SaaS / Notion Look) === */ +.preview-content[data-doc-style="modern"], +.preview-content.doc-style-modern { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + line-height: 1.7; + color: var(--primary-text); +} + +.preview-content[data-doc-style="modern"] h1, +.preview-content.doc-style-modern h1 { + font-size: 2.3rem; + font-weight: 800; + letter-spacing: -0.03em; + background: linear-gradient(135deg, var(--primary-text) 60%, var(--accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + border-bottom: 2px solid var(--accent); + padding-bottom: 0.4em; + margin-top: 1.4em; +} + +.preview-content[data-doc-style="modern"] h2, +.preview-content.doc-style-modern h2 { + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.02em; + border-bottom: 1px solid var(--border); + padding-bottom: 0.3em; + margin-top: 1.3em; +} + +.preview-content[data-doc-style="modern"] table, +.preview-content.doc-style-modern table { + width: 100%; + border-collapse: collapse; + border: 1.5px solid var(--border); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); + margin: 1.5em 0; +} + +.preview-content[data-doc-style="modern"] th, +.preview-content.doc-style-modern th { + background-color: var(--surface-hover); + color: var(--primary-text); + font-weight: 600; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-bottom: 2px solid var(--accent); +} + +.preview-content[data-doc-style="modern"] td, +.preview-content.doc-style-modern td { + padding: 0.7rem 1rem; + border: 1px solid var(--border); +} + +.preview-content[data-doc-style="modern"] tr:hover td, +.preview-content.doc-style-modern tr:hover td { + background-color: var(--surface-hover); +} + +.preview-content[data-doc-style="modern"] blockquote, +.preview-content.doc-style-modern blockquote { + border-left: 4px solid var(--accent); + background: linear-gradient(90deg, rgba(79, 70, 229, 0.08) 0%, transparent 100%); + border-radius: 0 10px 10px 0; + padding: 0.75rem 1.25rem; +} + + +/* === PRESET 2: ACADEMIC (LaTeX / Formal Research Paper) === */ +.preview-content[data-doc-style="academic"], +.preview-content.doc-style-academic { + font-family: 'Merriweather', 'Georgia', 'Cambria', 'Times New Roman', serif; + line-height: 1.9; + font-size: 1.05rem; + background-color: #fdfdf9; + color: #1a1a1a; + border-radius: 8px; + box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.02); + padding: 2.5rem 3rem; + text-align: justify; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="academic"], +:root[data-theme="dark"] .preview-content.doc-style-academic { + background-color: #121110; + color: #e5e0d8; + box-shadow: inset 0 0 40px rgba(255, 255, 255, 0.01); +} + +.preview-content[data-doc-style="academic"] h1, +.preview-content.doc-style-academic h1 { + font-family: 'Merriweather', 'Georgia', serif; + text-align: center; + font-size: 2.1rem; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; + border-top: 3px double currentColor; + border-bottom: 1px solid currentColor; + padding: 0.8rem 0; + margin: 1.8em 0 1.2em 0; +} + +.preview-content[data-doc-style="academic"] h2, +.preview-content.doc-style-academic h2 { + font-family: 'Merriweather', 'Georgia', serif; + font-size: 1.45rem; + font-weight: 700; + font-variant: small-caps; + letter-spacing: 0.06em; + border-bottom: 1px solid currentColor; + padding-bottom: 0.25em; + margin-top: 1.6em; +} + +.preview-content[data-doc-style="academic"] h3, +.preview-content.doc-style-academic h3 { + font-family: 'Merriweather', 'Georgia', serif; + font-size: 1.2rem; + font-style: italic; + font-weight: 600; + margin-top: 1.3em; +} + +/* Classical Academic Tables */ +.preview-content[data-doc-style="academic"] table, +.preview-content.doc-style-academic table { + width: 100%; + border-collapse: collapse; + border: 1px solid rgba(128, 128, 128, 0.4); + border-top: 2.5px solid currentColor; + border-bottom: 2.5px solid currentColor; + margin: 2em auto; + font-size: 0.95rem; +} + +.preview-content[data-doc-style="academic"] th, +.preview-content.doc-style-academic th { + border: 1px solid rgba(128, 128, 128, 0.35); + border-bottom: 2px solid currentColor; + font-variant: small-caps; + font-size: 0.88rem; + letter-spacing: 0.08em; + background: rgba(0, 0, 0, 0.03); + padding: 0.65rem 1rem; + text-align: center; +} + +.preview-content[data-doc-style="academic"] td, +.preview-content.doc-style-academic td { + border: 1px solid rgba(128, 128, 128, 0.25); + padding: 0.6rem 1rem; +} + +.preview-content[data-doc-style="academic"] blockquote, +.preview-content.doc-style-academic blockquote { + font-style: italic; + border-left: none; + background: transparent; + padding: 0.8rem 2.5rem; + margin: 1.8em 0; + position: relative; + text-align: left; +} + +.preview-content[data-doc-style="academic"] blockquote::before { + content: "“"; + font-size: 3.5rem; + line-height: 0; + position: absolute; + left: 0.5rem; + top: 1.5rem; + opacity: 0.35; + font-family: Georgia, serif; +} + + +/* === PRESET 3: TECHNICAL (Cyber / Terminal / Dev Spec) === */ +.preview-content[data-doc-style="technical"], +.preview-content.doc-style-technical { + font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', Consolas, monospace; + font-size: 0.92rem; + line-height: 1.65; + background-color: #0d1117; + color: #c9d1d9; + border: 1px solid #30363d; + border-radius: 8px; + padding: 2rem 2.5rem; +} + +.preview-content[data-doc-style="technical"] h1, +.preview-content.doc-style-technical h1 { + font-family: 'JetBrains Mono', monospace; + color: #58a6ff; + font-size: 1.8rem; + font-weight: 700; + border-bottom: 2px solid #238636; + padding-bottom: 0.4em; + letter-spacing: -0.02em; +} + +.preview-content[data-doc-style="technical"] h1::before { + content: "#!/usr/bin/spec >> "; + color: #7ee787; + font-size: 0.85rem; + display: block; + font-weight: 400; + margin-bottom: 0.3rem; + opacity: 0.8; +} + +.preview-content[data-doc-style="technical"] h2, +.preview-content.doc-style-technical h2 { + font-family: 'JetBrains Mono', monospace; + color: #79c0ff; + font-size: 1.35rem; + border-bottom: 1px dashed #30363d; + padding-bottom: 0.3em; + margin-top: 1.5em; +} + +.preview-content[data-doc-style="technical"] h2::before { + content: "## "; + color: #7ee787; + font-weight: bold; +} + +.preview-content[data-doc-style="technical"] h3, +.preview-content.doc-style-technical h3 { + font-family: 'JetBrains Mono', monospace; + color: #d2a8ff; + font-size: 1.15rem; +} + +.preview-content[data-doc-style="technical"] h3::before { + content: "fn "; + color: #ff7b72; +} + +/* Terminal Matrix Grid Table */ +.preview-content[data-doc-style="technical"] table, +.preview-content.doc-style-technical table { + border: 1px solid #30363d; + background-color: #161b22; + font-family: 'JetBrains Mono', monospace; + font-size: 0.85rem; +} + +.preview-content[data-doc-style="technical"] th, +.preview-content.doc-style-technical th { + background-color: #21262d; + color: #7ee787; + border: 1px solid #30363d; + padding: 0.5rem 0.85rem; + font-weight: 600; +} + +.preview-content[data-doc-style="technical"] td, +.preview-content.doc-style-technical td { + border: 1px solid #30363d; + padding: 0.5rem 0.85rem; +} + +.preview-content[data-doc-style="technical"] pre, +.preview-content.doc-style-technical pre { + background-color: #030712 !important; + border: 1px solid #06b6d4 !important; + box-shadow: 0 0 15px rgba(6, 182, 212, 0.12); + border-radius: 6px; +} + +.preview-content[data-doc-style="technical"] blockquote, +.preview-content.doc-style-technical blockquote { + border-left: 4px solid #238636; + background-color: rgba(35, 134, 54, 0.1); + color: #7ee787; + border-radius: 0 4px 4px 0; + padding: 0.6rem 1rem; +} + + +/* === PRESET 4: EXECUTIVE (Corporate Brief / Wall Street / McKinsey) === */ +.preview-content[data-doc-style="executive"], +.preview-content.doc-style-executive { + font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + line-height: 1.75; + color: #0f172a; + background-color: #ffffff; + border-top: 5px solid #0f2744; + border-radius: 8px; + box-shadow: 0 10px 25px -5px rgba(15, 39, 68, 0.08); + padding: 2.5rem 3rem; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"], +:root[data-theme="dark"] .preview-content.doc-style-executive { + color: #f1f5f9; + background-color: #0f172a; + border-top: 5px solid #38bdf8; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4); +} + +.preview-content[data-doc-style="executive"] h1, +.preview-content.doc-style-executive h1 { + font-size: 2.3rem; + font-weight: 800; + color: #0f2744; + letter-spacing: -0.02em; + border-bottom: 3px solid #d97706; + padding-bottom: 0.4em; + margin-top: 1.4em; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] h1, +:root[data-theme="dark"] .preview-content.doc-style-executive h1 { + color: #38bdf8; + border-bottom-color: #fbbf24; +} + +.preview-content[data-doc-style="executive"] h1::before { + content: "EXECUTIVE BRIEFING // CONFIDENTIAL"; + display: block; + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.15em; + color: #d97706; + margin-bottom: 0.4rem; +} + +.preview-content[data-doc-style="executive"] h2, +.preview-content.doc-style-executive h2 { + font-size: 1.35rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #0f2744; + border-left: 4px solid #d97706; + padding-left: 0.75rem; + margin-top: 1.6em; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] h2, +:root[data-theme="dark"] .preview-content.doc-style-executive h2 { + color: #38bdf8; + border-left-color: #fbbf24; +} + +.preview-content[data-doc-style="executive"] table, +.preview-content.doc-style-executive table { + width: 100%; + border-collapse: collapse; + border: 1.5px solid #cbd5e1; + box-shadow: 0 4px 6px -1px rgba(15, 39, 68, 0.06); + border-radius: 6px; + overflow: hidden; + margin: 1.5em 0; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] table, +:root[data-theme="dark"] .preview-content.doc-style-executive table { + border-color: #334155; +} + +.preview-content[data-doc-style="executive"] th, +.preview-content.doc-style-executive th { + background-color: #0f2744; + color: #ffffff; + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 0.75rem 1rem; + border: 1px solid #1e3a5f; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] th, +:root[data-theme="dark"] .preview-content.doc-style-executive th { + background-color: #1e293b; + color: #38bdf8; + border: 1px solid #334155; +} + +.preview-content[data-doc-style="executive"] td, +.preview-content.doc-style-executive td { + padding: 0.65rem 1rem; + border: 1px solid #cbd5e1; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] td, +:root[data-theme="dark"] .preview-content.doc-style-executive td { + border: 1px solid #334155; +} + +.preview-content[data-doc-style="executive"] tr:nth-child(even) td { + background-color: rgba(15, 39, 68, 0.03); +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] tr:nth-child(even) td { + background-color: rgba(255, 255, 255, 0.03); +} + +.preview-content[data-doc-style="executive"] blockquote, +.preview-content.doc-style-executive blockquote { + border-left: 4px solid #0f2744; + background-color: rgba(15, 39, 68, 0.05); + border-radius: 0 8px 8px 0; + font-weight: 500; + padding: 1rem 1.4rem; + border-right: 1px solid rgba(15, 39, 68, 0.1); +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] blockquote, +:root[data-theme="dark"] .preview-content.doc-style-executive blockquote { + border-left-color: #38bdf8; + background-color: rgba(56, 189, 248, 0.08); + border-right-color: rgba(56, 189, 248, 0.15); +} + /* ========================================================= */ /* PRINT / PDF EXPORT MEDIA QUERY STYLES */ /* ========================================================= */ @@ -879,6 +1513,7 @@ .editor-pane, .notes-drawer, .drawer-backdrop, + .preview-header-bar, .code-copy-btn { display: none !important; } @@ -897,6 +1532,7 @@ border: none !important; background: none !important; display: block !important; + width: 100% !important; } .preview-content { @@ -905,6 +1541,11 @@ overflow: visible !important; } + .preview-content h1, + .preview-content h2 { + page-break-after: avoid; + } + .preview-content pre { border: 1px solid #ddd !important; background-color: #f8f9fa !important; @@ -914,4 +1555,8 @@ .preview-content table { page-break-inside: avoid; } + + .preview-content .markdown-alert { + page-break-inside: avoid; + } } diff --git a/Notemark/Components/Pages/SharedPreview.razor b/Notemark/Components/Pages/SharedPreview.razor index 311b9aa..3dd5fc8 100644 --- a/Notemark/Components/Pages/SharedPreview.razor +++ b/Notemark/Components/Pages/SharedPreview.razor @@ -4,6 +4,7 @@ @using Notemark.Services @inject IShareService ShareService @inject ISessionService SessionService +@inject IDocumentService DocumentService @inject IMarkdownService MarkdownService @inject NavigationManager Navigation @inject IJSRuntime JSRuntime @@ -70,7 +71,7 @@ else } else { -
+
@((MarkupString)renderedHtml)
} @@ -86,6 +87,7 @@ else private ShareToken? shareToken = null; private string markdownContent = ""; private string renderedHtml = ""; + private string documentStyle = "modern"; protected override async Task OnInitializedAsync() { @@ -120,8 +122,10 @@ else return; } - // Load the document content + // Load the document content & metadata markdownContent = await SessionService.LoadSessionContentAsync(shareToken.SessionId); + var meta = await DocumentService.GetDocumentAsync(shareToken.SessionId); + documentStyle = meta?.DocStyle ?? "modern"; // Render to HTML renderedHtml = MarkdownService.RenderToHtml(markdownContent); diff --git a/Notemark/Components/Pages/SharedPreview.razor.css b/Notemark/Components/Pages/SharedPreview.razor.css index b5af032..1d307ab 100644 --- a/Notemark/Components/Pages/SharedPreview.razor.css +++ b/Notemark/Components/Pages/SharedPreview.razor.css @@ -327,34 +327,26 @@ .preview-content table { width: 100%; - border-collapse: separate; - border-spacing: 0; + border-collapse: collapse; margin: 1.5em 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; + font-size: 0.9375rem; } .preview-content th, .preview-content td { padding: 0.75rem 1rem; - border-bottom: 1px solid var(--border); - border-right: 1px solid var(--border); + border: 1px solid var(--border); text-align: left; } -.preview-content th:last-child, -.preview-content td:last-child { - border-right: none; -} - -.preview-content tr:last-child td { - border-bottom: none; -} - .preview-content th { background-color: var(--surface-hover); font-weight: 600; + color: var(--primary-text); + border-bottom: 2px solid var(--border); } .preview-content tr:hover td { @@ -385,6 +377,459 @@ margin: 1em 0; } +/* ========================================================= */ +/* DOCUMENT PRESET STYLES & GITHUB ALERTS */ +/* ========================================================= */ + +/* Callouts / GitHub Alerts */ +.preview-content .markdown-alert { + padding: 0.75rem 1rem; + margin: 1.25rem 0; + border-left: 4px solid; + border-radius: 0 6px 6px 0; + background-color: var(--surface-hover); +} + +.preview-content .markdown-alert-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + margin-bottom: 0.35rem; +} + +.preview-content .markdown-alert-note { border-left-color: #3b82f6; background-color: rgba(59, 130, 246, 0.08); } +.preview-content .markdown-alert-tip { border-left-color: #10b981; background-color: rgba(16, 185, 129, 0.08); } +.preview-content .markdown-alert-important { border-left-color: #8b5cf6; background-color: rgba(139, 92, 246, 0.08); } +.preview-content .markdown-alert-warning { border-left-color: #f59e0b; background-color: rgba(245, 158, 11, 0.08); } +.preview-content .markdown-alert-caution { border-left-color: #ef4444; background-color: rgba(239, 68, 68, 0.08); } + +/* ========================================================= */ +/* DISTINCT DOCUMENT PRESET STYLES */ +/* ========================================================= */ + +/* === PRESET 1: MODERN (Clean SaaS / Notion Look) === */ +.preview-content[data-doc-style="modern"], +.preview-content.doc-style-modern { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + line-height: 1.7; + color: var(--primary-text); +} + +.preview-content[data-doc-style="modern"] h1, +.preview-content.doc-style-modern h1 { + font-size: 2.3rem; + font-weight: 800; + letter-spacing: -0.03em; + background: linear-gradient(135deg, var(--primary-text) 60%, var(--accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + border-bottom: 2px solid var(--accent); + padding-bottom: 0.4em; + margin-top: 1.4em; +} + +.preview-content[data-doc-style="modern"] h2, +.preview-content.doc-style-modern h2 { + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.02em; + border-bottom: 1px solid var(--border); + padding-bottom: 0.3em; + margin-top: 1.3em; +} + +.preview-content[data-doc-style="modern"] table, +.preview-content.doc-style-modern table { + width: 100%; + border-collapse: collapse; + border: 1.5px solid var(--border); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); + margin: 1.5em 0; +} + +.preview-content[data-doc-style="modern"] th, +.preview-content.doc-style-modern th { + background-color: var(--surface-hover); + color: var(--primary-text); + font-weight: 600; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-bottom: 2px solid var(--accent); +} + +.preview-content[data-doc-style="modern"] td, +.preview-content.doc-style-modern td { + padding: 0.7rem 1rem; + border: 1px solid var(--border); +} + +.preview-content[data-doc-style="modern"] tr:hover td, +.preview-content.doc-style-modern tr:hover td { + background-color: var(--surface-hover); +} + +.preview-content[data-doc-style="modern"] blockquote, +.preview-content.doc-style-modern blockquote { + border-left: 4px solid var(--accent); + background: linear-gradient(90deg, rgba(79, 70, 229, 0.08) 0%, transparent 100%); + border-radius: 0 10px 10px 0; + padding: 0.75rem 1.25rem; +} + + +/* === PRESET 2: ACADEMIC (LaTeX / Formal Research Paper) === */ +.preview-content[data-doc-style="academic"], +.preview-content.doc-style-academic { + font-family: 'Merriweather', 'Georgia', 'Cambria', 'Times New Roman', serif; + line-height: 1.9; + font-size: 1.05rem; + background-color: #fdfdf9; + color: #1a1a1a; + border-radius: 8px; + box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.02); + padding: 2.5rem 3rem; + text-align: justify; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="academic"], +:root[data-theme="dark"] .preview-content.doc-style-academic { + background-color: #121110; + color: #e5e0d8; + box-shadow: inset 0 0 40px rgba(255, 255, 255, 0.01); +} + +.preview-content[data-doc-style="academic"] h1, +.preview-content.doc-style-academic h1 { + font-family: 'Merriweather', 'Georgia', serif; + text-align: center; + font-size: 2.1rem; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; + border-top: 3px double currentColor; + border-bottom: 1px solid currentColor; + padding: 0.8rem 0; + margin: 1.8em 0 1.2em 0; +} + +.preview-content[data-doc-style="academic"] h2, +.preview-content.doc-style-academic h2 { + font-family: 'Merriweather', 'Georgia', serif; + font-size: 1.45rem; + font-weight: 700; + font-variant: small-caps; + letter-spacing: 0.06em; + border-bottom: 1px solid currentColor; + padding-bottom: 0.25em; + margin-top: 1.6em; +} + +.preview-content[data-doc-style="academic"] h3, +.preview-content.doc-style-academic h3 { + font-family: 'Merriweather', 'Georgia', serif; + font-size: 1.2rem; + font-style: italic; + font-weight: 600; + margin-top: 1.3em; +} + +/* Classical Academic Tables */ +.preview-content[data-doc-style="academic"] table, +.preview-content.doc-style-academic table { + width: 100%; + border-collapse: collapse; + border: 1px solid rgba(128, 128, 128, 0.4); + border-top: 2.5px solid currentColor; + border-bottom: 2.5px solid currentColor; + margin: 2em auto; + font-size: 0.95rem; +} + +.preview-content[data-doc-style="academic"] th, +.preview-content.doc-style-academic th { + border: 1px solid rgba(128, 128, 128, 0.35); + border-bottom: 2px solid currentColor; + font-variant: small-caps; + font-size: 0.88rem; + letter-spacing: 0.08em; + background: rgba(0, 0, 0, 0.03); + padding: 0.65rem 1rem; + text-align: center; +} + +.preview-content[data-doc-style="academic"] td, +.preview-content.doc-style-academic td { + border: 1px solid rgba(128, 128, 128, 0.25); + padding: 0.6rem 1rem; +} + +.preview-content[data-doc-style="academic"] blockquote, +.preview-content.doc-style-academic blockquote { + font-style: italic; + border-left: none; + background: transparent; + padding: 0.8rem 2.5rem; + margin: 1.8em 0; + position: relative; + text-align: left; +} + +.preview-content[data-doc-style="academic"] blockquote::before { + content: "“"; + font-size: 3.5rem; + line-height: 0; + position: absolute; + left: 0.5rem; + top: 1.5rem; + opacity: 0.35; + font-family: Georgia, serif; +} + + +/* === PRESET 3: TECHNICAL (Cyber / Terminal / Dev Spec) === */ +.preview-content[data-doc-style="technical"], +.preview-content.doc-style-technical { + font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', Consolas, monospace; + font-size: 0.92rem; + line-height: 1.65; + background-color: #0d1117; + color: #c9d1d9; + border: 1px solid #30363d; + border-radius: 8px; + padding: 2rem 2.5rem; +} + +.preview-content[data-doc-style="technical"] h1, +.preview-content.doc-style-technical h1 { + font-family: 'JetBrains Mono', monospace; + color: #58a6ff; + font-size: 1.8rem; + font-weight: 700; + border-bottom: 2px solid #238636; + padding-bottom: 0.4em; + letter-spacing: -0.02em; +} + +.preview-content[data-doc-style="technical"] h1::before { + content: "#!/usr/bin/spec >> "; + color: #7ee787; + font-size: 0.85rem; + display: block; + font-weight: 400; + margin-bottom: 0.3rem; + opacity: 0.8; +} + +.preview-content[data-doc-style="technical"] h2, +.preview-content.doc-style-technical h2 { + font-family: 'JetBrains Mono', monospace; + color: #79c0ff; + font-size: 1.35rem; + border-bottom: 1px dashed #30363d; + padding-bottom: 0.3em; + margin-top: 1.5em; +} + +.preview-content[data-doc-style="technical"] h2::before { + content: "## "; + color: #7ee787; + font-weight: bold; +} + +.preview-content[data-doc-style="technical"] h3, +.preview-content.doc-style-technical h3 { + font-family: 'JetBrains Mono', monospace; + color: #d2a8ff; + font-size: 1.15rem; +} + +.preview-content[data-doc-style="technical"] h3::before { + content: "fn "; + color: #ff7b72; +} + +/* Terminal Matrix Grid Table */ +.preview-content[data-doc-style="technical"] table, +.preview-content.doc-style-technical table { + border: 1px solid #30363d; + background-color: #161b22; + font-family: 'JetBrains Mono', monospace; + font-size: 0.85rem; +} + +.preview-content[data-doc-style="technical"] th, +.preview-content.doc-style-technical th { + background-color: #21262d; + color: #7ee787; + border: 1px solid #30363d; + padding: 0.5rem 0.85rem; + font-weight: 600; +} + +.preview-content[data-doc-style="technical"] td, +.preview-content.doc-style-technical td { + border: 1px solid #30363d; + padding: 0.5rem 0.85rem; +} + +.preview-content[data-doc-style="technical"] pre, +.preview-content.doc-style-technical pre { + background-color: #030712 !important; + border: 1px solid #06b6d4 !important; + box-shadow: 0 0 15px rgba(6, 182, 212, 0.12); + border-radius: 6px; +} + +.preview-content[data-doc-style="technical"] blockquote, +.preview-content.doc-style-technical blockquote { + border-left: 4px solid #238636; + background-color: rgba(35, 134, 54, 0.1); + color: #7ee787; + border-radius: 0 4px 4px 0; + padding: 0.6rem 1rem; +} + + +/* === PRESET 4: EXECUTIVE (Corporate Brief / Wall Street / McKinsey) === */ +.preview-content[data-doc-style="executive"], +.preview-content.doc-style-executive { + font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + line-height: 1.75; + color: #0f172a; + background-color: #ffffff; + border-top: 5px solid #0f2744; + border-radius: 8px; + box-shadow: 0 10px 25px -5px rgba(15, 39, 68, 0.08); + padding: 2.5rem 3rem; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"], +:root[data-theme="dark"] .preview-content.doc-style-executive { + color: #f1f5f9; + background-color: #0f172a; + border-top: 5px solid #38bdf8; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4); +} + +.preview-content[data-doc-style="executive"] h1, +.preview-content.doc-style-executive h1 { + font-size: 2.3rem; + font-weight: 800; + color: #0f2744; + letter-spacing: -0.02em; + border-bottom: 3px solid #d97706; + padding-bottom: 0.4em; + margin-top: 1.4em; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] h1, +:root[data-theme="dark"] .preview-content.doc-style-executive h1 { + color: #38bdf8; + border-bottom-color: #fbbf24; +} + +.preview-content[data-doc-style="executive"] h1::before { + content: "EXECUTIVE BRIEFING // CONFIDENTIAL"; + display: block; + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.15em; + color: #d97706; + margin-bottom: 0.4rem; +} + +.preview-content[data-doc-style="executive"] h2, +.preview-content.doc-style-executive h2 { + font-size: 1.35rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #0f2744; + border-left: 4px solid #d97706; + padding-left: 0.75rem; + margin-top: 1.6em; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] h2, +:root[data-theme="dark"] .preview-content.doc-style-executive h2 { + color: #38bdf8; + border-left-color: #fbbf24; +} + +.preview-content[data-doc-style="executive"] table, +.preview-content.doc-style-executive table { + width: 100%; + border-collapse: collapse; + border: 1.5px solid #cbd5e1; + box-shadow: 0 4px 6px -1px rgba(15, 39, 68, 0.06); + border-radius: 6px; + overflow: hidden; + margin: 1.5em 0; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] table, +:root[data-theme="dark"] .preview-content.doc-style-executive table { + border-color: #334155; +} + +.preview-content[data-doc-style="executive"] th, +.preview-content.doc-style-executive th { + background-color: #0f2744; + color: #ffffff; + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 0.75rem 1rem; + border: 1px solid #1e3a5f; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] th, +:root[data-theme="dark"] .preview-content.doc-style-executive th { + background-color: #1e293b; + color: #38bdf8; + border: 1px solid #334155; +} + +.preview-content[data-doc-style="executive"] td, +.preview-content.doc-style-executive td { + padding: 0.65rem 1rem; + border: 1px solid #cbd5e1; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] td, +:root[data-theme="dark"] .preview-content.doc-style-executive td { + border: 1px solid #334155; +} + +.preview-content[data-doc-style="executive"] tr:nth-child(even) td { + background-color: rgba(15, 39, 68, 0.03); +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] tr:nth-child(even) td { + background-color: rgba(255, 255, 255, 0.03); +} + +.preview-content[data-doc-style="executive"] blockquote, +.preview-content.doc-style-executive blockquote { + border-left: 4px solid #0f2744; + background-color: rgba(15, 39, 68, 0.05); + border-radius: 0 8px 8px 0; + font-weight: 500; + padding: 1rem 1.4rem; + border-right: 1px solid rgba(15, 39, 68, 0.1); +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] blockquote, +:root[data-theme="dark"] .preview-content.doc-style-executive blockquote { + border-left-color: #38bdf8; + background-color: rgba(56, 189, 248, 0.08); + border-right-color: rgba(56, 189, 248, 0.15); +} + @media (max-width: 768px) { .shared-header { padding: 0.75rem 1rem; diff --git a/Notemark/Models/DocumentMetadata.cs b/Notemark/Models/DocumentMetadata.cs index b948e4f..debda25 100644 --- a/Notemark/Models/DocumentMetadata.cs +++ b/Notemark/Models/DocumentMetadata.cs @@ -9,4 +9,5 @@ public record DocumentMetadata public DateTime CreatedAt { get; init; } = DateTime.UtcNow; public DateTime LastModifiedAt { get; init; } = DateTime.UtcNow; public string Snippet { get; init; } = ""; + public string DocStyle { get; init; } = "modern"; } diff --git a/Notemark/Program.cs b/Notemark/Program.cs index 88b65bb..21ca0c3 100644 --- a/Notemark/Program.cs +++ b/Notemark/Program.cs @@ -23,6 +23,7 @@ builder.Services.AddHttpContextAccessor(); // Register Notemark services builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Notemark/Services/AutoStyleService.cs b/Notemark/Services/AutoStyleService.cs new file mode 100644 index 0000000..ef5bed8 --- /dev/null +++ b/Notemark/Services/AutoStyleService.cs @@ -0,0 +1,473 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace Notemark.Services; + +public class AutoStyleService : IAutoStyleService +{ + private static readonly Regex HeadingRegex = new(@"^(#{1,6})([^\s#].*)$", RegexOptions.Compiled); + private static readonly Regex HeadingTrailingHashRegex = new(@"^(#{1,6}\s+.*?)\s+#+\s*$", RegexOptions.Compiled); + private static readonly Regex BulletListRegex = new(@"^(\s*)[*+]( +.*)$", RegexOptions.Compiled); + private static readonly Regex TaskListEmptyRegex = new(@"^(\s*-\s*)\[\s*\]\s*(.*)$", RegexOptions.Compiled); + private static readonly Regex TaskListCheckedRegex = new(@"^(\s*-\s*)\[[xX]\]\s*(.*)$", RegexOptions.Compiled); + + // Callout triggers for English and Slovak + private static readonly Regex WarningCalloutRegex = new(@"^(?:>\s*)?(?:Pozor|Varovanie|Warning|Caution):\s*(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ImportantCalloutRegex = new(@"^(?:>\s*)?(?:Dôležité|Dolezite|Important):\s*(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex TipCalloutRegex = new(@"^(?:>\s*)?(?:Tip):\s*(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex NoteCalloutRegex = new(@"^(?:>\s*)?(?:Poznámka|Poznamka|Note|Info|Informácia|Informacia):\s*(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex CautionCalloutRegex = new(@"^(?:>\s*)?(?:Kritické|Kriticke|Chyba|Danger|Error):\s*(.*)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + public string FormatMarkdown(string markdown) + { + if (string.IsNullOrWhiteSpace(markdown)) + return string.Empty; + + var rawLines = markdown.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n'); + var processedLines = new List(); + + bool inCodeBlock = false; + string codeBlockFence = ""; + bool inFrontmatter = false; + + int lineIndex = 0; + while (lineIndex < rawLines.Length) + { + var line = rawLines[lineIndex]; + + // 1. Handle Frontmatter (YAML at document start) + if (lineIndex == 0 && line.Trim() == "---") + { + inFrontmatter = true; + processedLines.Add(line); + lineIndex++; + continue; + } + if (inFrontmatter) + { + processedLines.Add(line); + if (line.Trim() == "---") + { + inFrontmatter = false; + } + lineIndex++; + continue; + } + + // 2. Handle Code Blocks + var trimmedLine = line.Trim(); + if (trimmedLine.StartsWith("```") || trimmedLine.StartsWith("~~~")) + { + var fence = trimmedLine.Length >= 3 ? trimmedLine[..3] : "```"; + if (!inCodeBlock) + { + inCodeBlock = true; + codeBlockFence = fence; + processedLines.Add(line.TrimEnd()); + } + else if (trimmedLine.StartsWith(codeBlockFence)) + { + inCodeBlock = false; + codeBlockFence = ""; + processedLines.Add(line.TrimEnd()); + } + else + { + processedLines.Add(line); + } + lineIndex++; + continue; + } + + if (inCodeBlock) + { + // Preserve code block contents exactly as-is + processedLines.Add(line); + lineIndex++; + continue; + } + + // 3. Detect and Format Markdown Tables + if (IsTableStart(rawLines, lineIndex)) + { + if (processedLines.Count > 0 && !string.IsNullOrWhiteSpace(processedLines[^1])) + { + processedLines.Add(string.Empty); + } + + var (formattedTableLines, nextIndex) = FormatTableBlock(rawLines, lineIndex); + processedLines.AddRange(formattedTableLines); + + if (nextIndex < rawLines.Length && !string.IsNullOrWhiteSpace(rawLines[nextIndex])) + { + processedLines.Add(string.Empty); + } + + lineIndex = nextIndex; + continue; + } + + // 4. Format Regular Markdown Line + var formattedLine = FormatSingleLine(line); + + // 5. Smart Callouts Conversion + var calloutLines = TryConvertSmartCallout(formattedLine); + if (calloutLines != null) + { + processedLines.AddRange(calloutLines); + } + else + { + processedLines.Add(formattedLine); + } + + lineIndex++; + } + + // 6. Clean up consecutive blank lines & ensure single final newline + return NormalizeBlankLines(processedLines); + } + + private static string FormatSingleLine(string line) + { + var trimmedEnd = line.TrimEnd(); + + // Fix Heading missing space: e.g. #Heading -> # Heading + var headingMatch = HeadingRegex.Match(trimmedEnd); + if (headingMatch.Success) + { + trimmedEnd = $"{headingMatch.Groups[1].Value} {headingMatch.Groups[2].Value.Trim()}"; + } + + // Remove trailing hash decorations in headings: ## Heading ## -> ## Heading + var trailingHashMatch = HeadingTrailingHashRegex.Match(trimmedEnd); + if (trailingHashMatch.Success) + { + trimmedEnd = trailingHashMatch.Groups[1].Value.TrimEnd(); + } + + // Fix bullet list style: * item or + item -> - item + var bulletMatch = BulletListRegex.Match(trimmedEnd); + if (bulletMatch.Success) + { + trimmedEnd = $"{bulletMatch.Groups[1].Value}-{bulletMatch.Groups[2].Value}"; + } + + // Fix Task lists spacing + var taskEmptyMatch = TaskListEmptyRegex.Match(trimmedEnd); + if (taskEmptyMatch.Success) + { + trimmedEnd = $"{taskEmptyMatch.Groups[1].Value}[ ] {taskEmptyMatch.Groups[2].Value.TrimStart()}"; + } + else + { + var taskCheckedMatch = TaskListCheckedRegex.Match(trimmedEnd); + if (taskCheckedMatch.Success) + { + trimmedEnd = $"{taskCheckedMatch.Groups[1].Value}[x] {taskCheckedMatch.Groups[2].Value.TrimStart()}"; + } + } + + return trimmedEnd; + } + + private static List? TryConvertSmartCallout(string line) + { + var trimmed = line.Trim(); + if (string.IsNullOrWhiteSpace(trimmed)) return null; + + // Skip if already in GitHub alert syntax: > [!NOTE], > [!WARNING], etc. + if (trimmed.StartsWith("> [!") || trimmed.StartsWith("[!")) return null; + + if (WarningCalloutRegex.IsMatch(trimmed)) + { + var text = WarningCalloutRegex.Match(trimmed).Groups[1].Value.Trim(); + return new List { "> [!WARNING]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" }; + } + if (ImportantCalloutRegex.IsMatch(trimmed)) + { + var text = ImportantCalloutRegex.Match(trimmed).Groups[1].Value.Trim(); + return new List { "> [!IMPORTANT]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" }; + } + if (TipCalloutRegex.IsMatch(trimmed)) + { + var text = TipCalloutRegex.Match(trimmed).Groups[1].Value.Trim(); + return new List { "> [!TIP]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" }; + } + if (NoteCalloutRegex.IsMatch(trimmed)) + { + var text = NoteCalloutRegex.Match(trimmed).Groups[1].Value.Trim(); + return new List { "> [!NOTE]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" }; + } + if (CautionCalloutRegex.IsMatch(trimmed)) + { + var text = CautionCalloutRegex.Match(trimmed).Groups[1].Value.Trim(); + return new List { "> [!CAUTION]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" }; + } + + return null; + } + + private static bool IsTableStart(string[] lines, int currentIndex) + { + if (currentIndex + 1 >= lines.Length) return false; + + var current = lines[currentIndex].Trim(); + var next = lines[currentIndex + 1].Trim(); + + if (!current.Contains('|') || !next.Contains('|')) return false; + + // Next line must be table separator: containing only |, -, :, and spaces + var separatorClean = next.Replace("|", "").Replace("-", "").Replace(":", "").Trim(); + return separatorClean.Length == 0 && next.Contains('-'); + } + + private static (List FormattedLines, int NextIndex) FormatTableBlock(string[] lines, int startIndex) + { + var tableRawLines = new List(); + int currentIndex = startIndex; + + while (currentIndex < lines.Length) + { + var line = lines[currentIndex]; + var trimmed = line.Trim(); + if (string.IsNullOrWhiteSpace(trimmed) || !trimmed.Contains('|')) + { + break; + } + tableRawLines.Add(trimmed); + currentIndex++; + } + + if (tableRawLines.Count < 2) + { + return (tableRawLines, currentIndex); + } + + // Parse cells for all rows + var parsedRows = new List>(); + foreach (var r in tableRawLines) + { + var cells = SplitTableCells(r); + parsedRows.Add(cells); + } + + // Determine max column count + int colCount = parsedRows.Max(r => r.Count); + if (colCount == 0) return (tableRawLines, currentIndex); + + // Normalize all rows to colCount + foreach (var row in parsedRows) + { + while (row.Count < colCount) + { + row.Add(""); + } + } + + // Trim trailing columns if they are empty in every non-separator row + while (colCount > 1) + { + bool allEmptyInLastCol = true; + for (int r = 0; r < parsedRows.Count; r++) + { + if (r == 1) continue; // skip separator + if (!string.IsNullOrWhiteSpace(parsedRows[r][colCount - 1])) + { + allEmptyInLastCol = false; + break; + } + } + if (allEmptyInLastCol) + { + colCount--; + foreach (var row in parsedRows) + { + if (row.Count > colCount) row.RemoveAt(colCount); + } + } + else + { + break; + } + } + + // Ensure header cells in row 0 are not blank + for (int c = 0; c < colCount; c++) + { + if (string.IsNullOrWhiteSpace(parsedRows[0][c])) + { + parsedRows[0][c] = $"Column {c + 1}"; + } + } + + // Parse alignments from separator (row 1) + var alignments = new List(); + var separatorRow = parsedRows[1]; + for (int c = 0; c < colCount; c++) + { + var cell = c < separatorRow.Count ? separatorRow[c].Trim() : ""; + bool left = cell.StartsWith(':'); + bool right = cell.EndsWith(':'); + + if (left && right) + alignments.Add(ColumnAlign.Center); + else if (right) + alignments.Add(ColumnAlign.Right); + else + alignments.Add(ColumnAlign.Left); + } + + // Filter out any duplicate separator rows inside data rows (r >= 2) + var dataRows = new List>(); + for (int r = 2; r < parsedRows.Count; r++) + { + var row = parsedRows[r]; + bool isDashesOnly = row.All(c => { + var clean = c.Replace("-", "").Replace(":", "").Trim(); + return clean.Length == 0 && c.Contains('-'); + }); + if (!isDashesOnly) + { + dataRows.Add(row); + } + } + + // Determine column widths + var colWidths = new int[colCount]; + for (int c = 0; c < colCount; c++) + { + int maxWidth = 3; // minimum 3 chars (e.g. ---) + maxWidth = Math.Max(maxWidth, parsedRows[0][c].Trim().Length); + for (int r = 0; r < dataRows.Count; r++) + { + maxWidth = Math.Max(maxWidth, dataRows[r][c].Trim().Length); + } + colWidths[c] = maxWidth; + } + + // Reconstruct formatted table + var result = new List(); + + // 1. Header row + result.Add(FormatTableRow(parsedRows[0], colWidths, alignments)); + + // 2. Separator row + var sepSb = new StringBuilder(); + sepSb.Append('|'); + for (int c = 0; c < colCount; c++) + { + var width = colWidths[c]; + var align = alignments[c]; + sepSb.Append(' '); + if (align == ColumnAlign.Center) + { + sepSb.Append(':'); + sepSb.Append(new string('-', Math.Max(1, width - 2))); + sepSb.Append(':'); + } + else if (align == ColumnAlign.Right) + { + sepSb.Append(new string('-', Math.Max(2, width - 1))); + sepSb.Append(':'); + } + else + { + sepSb.Append(':'); + sepSb.Append(new string('-', Math.Max(2, width - 1))); + } + sepSb.Append(" |"); + } + result.Add(sepSb.ToString()); + + // 3. Data rows + for (int r = 0; r < dataRows.Count; r++) + { + result.Add(FormatTableRow(dataRows[r], colWidths, alignments)); + } + + return (result, currentIndex); + } + + private static List SplitTableCells(string row) + { + var trimmed = row.Trim(); + if (trimmed.StartsWith('|')) trimmed = trimmed[1..]; + if (trimmed.EndsWith('|')) trimmed = trimmed[..^1]; + + var raw = trimmed.Split('|'); + return raw.Select(s => s.Trim()).ToList(); + } + + private static string FormatTableRow(List cells, int[] colWidths, List alignments) + { + var sb = new StringBuilder(); + sb.Append('|'); + for (int c = 0; c < colWidths.Length; c++) + { + var text = c < cells.Count ? cells[c].Trim() : ""; + var width = colWidths[c]; + var align = c < alignments.Count ? alignments[c] : ColumnAlign.Left; + + sb.Append(' '); + if (align == ColumnAlign.Right) + { + sb.Append(text.PadLeft(width)); + } + else if (align == ColumnAlign.Center) + { + int totalPad = width - text.Length; + int padLeft = totalPad / 2; + int padRight = totalPad - padLeft; + sb.Append(new string(' ', Math.Max(0, padLeft))); + sb.Append(text); + sb.Append(new string(' ', Math.Max(0, padRight))); + } + else + { + sb.Append(text.PadRight(width)); + } + sb.Append(" |"); + } + return sb.ToString(); + } + + private static string NormalizeBlankLines(List lines) + { + var output = new List(); + bool prevWasBlank = false; + + foreach (var line in lines) + { + var isBlank = string.IsNullOrWhiteSpace(line); + if (isBlank) + { + if (!prevWasBlank && output.Count > 0) + { + output.Add(string.Empty); + prevWasBlank = true; + } + } + else + { + output.Add(line); + prevWasBlank = false; + } + } + + // Trim trailing blank lines + while (output.Count > 0 && string.IsNullOrWhiteSpace(output[^1])) + { + output.RemoveAt(output.Count - 1); + } + + return string.Join("\n", output) + "\n"; + } + + private enum ColumnAlign + { + Left, + Center, + Right + } +} diff --git a/Notemark/Services/DocumentService.cs b/Notemark/Services/DocumentService.cs index 8e4781f..b396539 100644 --- a/Notemark/Services/DocumentService.cs +++ b/Notemark/Services/DocumentService.cs @@ -107,7 +107,7 @@ public partial class DocumentService : IDocumentService return docs.FirstOrDefault(d => d.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); } - public async Task SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List? tags = null) + public async Task SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List? tags = null, string? docStyle = null) { if (string.IsNullOrEmpty(id)) throw new ArgumentException("Document ID cannot be empty", nameof(id)); @@ -132,6 +132,7 @@ public partial class DocumentService : IDocumentService Snippet = snippet, IsFlagged = isFlagged ?? existing.IsFlagged, Tags = extractedTags, + DocStyle = docStyle ?? existing.DocStyle ?? "modern", LastModifiedAt = DateTime.UtcNow }; docs[existingIndex] = doc; @@ -145,6 +146,7 @@ public partial class DocumentService : IDocumentService Snippet = snippet, IsFlagged = isFlagged ?? false, Tags = extractedTags, + DocStyle = docStyle ?? "modern", CreatedAt = DateTime.UtcNow, LastModifiedAt = DateTime.UtcNow }; diff --git a/Notemark/Services/IAutoStyleService.cs b/Notemark/Services/IAutoStyleService.cs new file mode 100644 index 0000000..01f99fc --- /dev/null +++ b/Notemark/Services/IAutoStyleService.cs @@ -0,0 +1,9 @@ +namespace Notemark.Services; + +public interface IAutoStyleService +{ + /// + /// Formats and cleans markdown content (aligns tables, fixes headings, cleans whitespace, converts smart callouts). + /// + string FormatMarkdown(string markdown); +} diff --git a/Notemark/Services/IDocumentService.cs b/Notemark/Services/IDocumentService.cs index c30d31d..1722310 100644 --- a/Notemark/Services/IDocumentService.cs +++ b/Notemark/Services/IDocumentService.cs @@ -11,7 +11,7 @@ public interface IDocumentService Task> GetDocumentsByTagAsync(string tag); Task GetMostRecentDocumentAsync(); Task GetDocumentAsync(string id); - Task SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List? tags = null); + Task SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List? tags = null, string? docStyle = null); Task ToggleFlagAsync(string id); Task DeleteDocumentAsync(string id); string ExtractTitle(string content); diff --git a/Notemark/Services/MarkdownService.cs b/Notemark/Services/MarkdownService.cs index dff370b..8d347c4 100644 --- a/Notemark/Services/MarkdownService.cs +++ b/Notemark/Services/MarkdownService.cs @@ -13,6 +13,7 @@ public class MarkdownService : IMarkdownService // Configure Markdig pipeline with GitHub-style features _pipeline = new MarkdownPipelineBuilder() .UseAdvancedExtensions() // Tables, footnotes, task lists, etc. + .UseAlertBlocks() // GitHub-style alerts (> [!NOTE], > [!WARNING], etc.) .UseSoftlineBreakAsHardlineBreak() // GitHub-style line breaks .Build(); @@ -29,10 +30,127 @@ public class MarkdownService : IMarkdownService if (string.IsNullOrEmpty(markdown)) return string.Empty; + // Preprocess markdown to ensure tables are correctly surrounded by blank lines for CommonMark / Markdig parser + var preprocessed = EnsureTableSpacing(markdown); + // Convert markdown to HTML - var html = Markdown.ToHtml(markdown, _pipeline); + var html = Markdown.ToHtml(preprocessed, _pipeline); // Sanitize HTML to prevent XSS attacks return _sanitizer.Sanitize(html); } + + public static string EnsureTableSpacing(string markdown) + { + if (string.IsNullOrEmpty(markdown)) return markdown; + + var lines = markdown.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n'); + var result = new List(lines.Length + 8); + bool inCodeBlock = false; + string codeFence = ""; + bool inFrontmatter = false; + bool inTable = false; + + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i]; + var trimmed = line.Trim(); + + // 1. Frontmatter + if (i == 0 && trimmed == "---") + { + inFrontmatter = true; + result.Add(line); + continue; + } + if (inFrontmatter) + { + result.Add(line); + if (trimmed == "---") inFrontmatter = false; + continue; + } + + // 2. Code blocks + if (trimmed.StartsWith("```") || trimmed.StartsWith("~~~")) + { + var fence = trimmed.Length >= 3 ? trimmed[..3] : "```"; + if (!inCodeBlock) + { + inCodeBlock = true; + codeFence = fence; + } + else if (trimmed.StartsWith(codeFence)) + { + inCodeBlock = false; + codeFence = ""; + } + result.Add(line); + inTable = false; + continue; + } + + if (inCodeBlock) + { + result.Add(line); + continue; + } + + // 3. Table detection + if (!inTable && IsTableStartLine(lines, i)) + { + // Ensure blank line before table if previous line is not blank + if (result.Count > 0 && !string.IsNullOrWhiteSpace(result[^1])) + { + result.Add(string.Empty); + } + inTable = true; + result.Add(line); + continue; + } + + if (inTable) + { + if (IsTableRowLine(line)) + { + result.Add(line); + continue; + } + else + { + // Table ended + inTable = false; + if (!string.IsNullOrWhiteSpace(line)) + { + // Ensure blank line after table + result.Add(string.Empty); + } + result.Add(line); + continue; + } + } + + result.Add(line); + } + + return string.Join("\n", result); + } + + private static bool IsTableStartLine(string[] lines, int index) + { + if (index + 1 >= lines.Length) return false; + var current = lines[index].Trim(); + var next = lines[index + 1].Trim(); + + if (!current.Contains('|') || !next.Contains('|')) return false; + + // Next line must be a table delimiter line containing only |, -, :, and whitespace + var cleanDelim = next.Replace("|", "").Replace("-", "").Replace(":", "").Trim(); + return cleanDelim.Length == 0 && next.Contains('-'); + } + + private static bool IsTableRowLine(string line) + { + var trimmed = line.Trim(); + return !string.IsNullOrWhiteSpace(trimmed) && trimmed.Contains('|'); + } } \ No newline at end of file diff --git a/Notemark/data/documents.json b/Notemark/data/documents.json deleted file mode 100644 index dabf435..0000000 --- a/Notemark/data/documents.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "id": "2ba5f046-c8d8-4e55-acf4-5e654dee5afc", - "title": "Kopanie", - "isFlagged": true, - "tags": [], - "createdAt": "2026-09-22T09:08:10.0528137Z", - "lastModifiedAt": "2026-09-22T09:45:38.5280677Z", - "snippet": "Kopanie Moj otec mi v\u017Edy hovoril, \u017Ee do pivnice sa nechod\u00ED. Nie preto, \u017Ee by tam bolo nebezpe\u010Dn\u00E9 \u2014 aspo\u0148 tak to tvrdil \u2014..." - } -] \ No newline at end of file diff --git a/Notemark/wwwroot/app.css b/Notemark/wwwroot/app.css index 43772d0..f37a46a 100644 --- a/Notemark/wwwroot/app.css +++ b/Notemark/wwwroot/app.css @@ -123,4 +123,135 @@ a:hover { .blazor-error-boundary::after { content: "An error has occurred."; +} + +/* ========================================================= */ +/* GLOBAL MARKDOWN PREVIEW STYLING */ +/* ========================================================= */ + +/* Base Tables */ +.preview-content table { + width: 100% !important; + border-collapse: collapse !important; + margin: 1.5em 0 !important; + border: 1.5px solid var(--border) !important; + border-radius: 8px !important; + overflow: hidden !important; + font-size: 0.9375rem !important; + display: table !important; +} + +.preview-content th, +.preview-content td { + padding: 0.75rem 1rem !important; + border: 1px solid var(--border) !important; + text-align: left; +} + +.preview-content th { + background-color: var(--surface-hover) !important; + font-weight: 600 !important; + color: var(--primary-text) !important; + border-bottom: 2px solid var(--border) !important; +} + +.preview-content tr:hover td { + background-color: var(--surface-hover); +} + +/* Modern Preset Tables */ +.preview-content[data-doc-style="modern"] table, +.preview-content.doc-style-modern table { + border: 1.5px solid var(--border) !important; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important; +} + +.preview-content[data-doc-style="modern"] th, +.preview-content.doc-style-modern th { + background-color: var(--surface-hover) !important; + color: var(--primary-text) !important; + border: 1px solid var(--border) !important; + border-bottom: 2px solid var(--accent) !important; +} + +.preview-content[data-doc-style="modern"] td, +.preview-content.doc-style-modern td { + border: 1px solid var(--border) !important; +} + +/* Academic Preset Tables */ +.preview-content[data-doc-style="academic"] table, +.preview-content.doc-style-academic table { + border: 1px solid rgba(128, 128, 128, 0.4) !important; + border-top: 2.5px solid currentColor !important; + border-bottom: 2.5px solid currentColor !important; +} + +.preview-content[data-doc-style="academic"] th, +.preview-content.doc-style-academic th { + border: 1px solid rgba(128, 128, 128, 0.35) !important; + border-bottom: 2px solid currentColor !important; + background: rgba(0, 0, 0, 0.03) !important; +} + +.preview-content[data-doc-style="academic"] td, +.preview-content.doc-style-academic td { + border: 1px solid rgba(128, 128, 128, 0.25) !important; +} + +/* Technical Preset Tables */ +.preview-content[data-doc-style="technical"] table, +.preview-content.doc-style-technical table { + border: 1.5px solid #30363d !important; + background-color: #161b22 !important; + font-family: 'JetBrains Mono', monospace !important; +} + +.preview-content[data-doc-style="technical"] th, +.preview-content.doc-style-technical th { + background-color: #21262d !important; + color: #7ee787 !important; + border: 1px solid #30363d !important; +} + +.preview-content[data-doc-style="technical"] td, +.preview-content.doc-style-technical td { + border: 1px solid #30363d !important; + color: #c9d1d9 !important; +} + +/* Executive Preset Tables */ +.preview-content[data-doc-style="executive"] table, +.preview-content.doc-style-executive table { + border: 1.5px solid #cbd5e1 !important; + box-shadow: 0 4px 6px -1px rgba(15, 39, 68, 0.06) !important; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] table, +:root[data-theme="dark"] .preview-content.doc-style-executive table { + border-color: #334155 !important; +} + +.preview-content[data-doc-style="executive"] th, +.preview-content.doc-style-executive th { + background-color: #0f2744 !important; + color: #ffffff !important; + border: 1px solid #1e3a5f !important; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] th, +:root[data-theme="dark"] .preview-content.doc-style-executive th { + background-color: #1e293b !important; + color: #38bdf8 !important; + border: 1px solid #334155 !important; +} + +.preview-content[data-doc-style="executive"] td, +.preview-content.doc-style-executive td { + border: 1px solid #cbd5e1 !important; +} + +:root[data-theme="dark"] .preview-content[data-doc-style="executive"] td, +:root[data-theme="dark"] .preview-content.doc-style-executive td { + border: 1px solid #334155 !important; } \ No newline at end of file diff --git a/Notemark/wwwroot/js/editor.js b/Notemark/wwwroot/js/editor.js index b81c845..bf75fd2 100644 --- a/Notemark/wwwroot/js/editor.js +++ b/Notemark/wwwroot/js/editor.js @@ -256,29 +256,382 @@ window.notemarkEditor = { }, { passive: false }); }, - // Download standalone styled HTML file - downloadHtml: function(filename, title, htmlBody, theme) { + // Download standalone styled HTML file with chosen document style & theme + downloadHtml: function(filename, title, htmlBody, theme, docStyle) { + const style = docStyle || 'modern'; const fullHtml = ` - + ${title || 'Notemark Document'} + + +