This commit is contained in:
Michal Vrba
2026-09-29 13:10:20 +02:00
parent b0f044446d
commit 18ffaebb3c
15 changed files with 2427 additions and 62 deletions
+2
View File
@@ -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
+198 -6
View File
@@ -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 @@
</svg>
</button>
<!-- Document Style Dropdown in Top Bar -->
<div class="style-container">
<button class="btn-topbar btn-style-nav @(showStyleDropdown ? "active" : "")" @onclick="ToggleStyleDropdown" title="Document Typography & Style Preset (@GetDocStyleDisplayName(documentStyle))">
<span>🎨</span>
<span class="btn-text">@GetDocStyleDisplayName(documentStyle)</span>
<span class="btn-caret">▾</span>
</button>
@if (showStyleDropdown)
{
<div class="dropdown-backdrop" @onclick="CloseStyleDropdown"></div>
<div class="style-dropdown">
<div class="dropdown-header-title">Document Style</div>
<button type="button" class="style-menu-item @(documentStyle == "modern" ? "selected" : "")" @onclick='() => SetDocumentStyle("modern")'>
<span class="style-item-icon">🌟</span>
<div class="style-item-text">
<span class="style-item-name">Modern</span>
<span class="style-item-desc">Clean sans-serif, modern cards & alerts</span>
</div>
@if (documentStyle == "modern") { <span class="style-check">✓</span> }
</button>
<button type="button" class="style-menu-item @(documentStyle == "academic" ? "selected" : "")" @onclick='() => SetDocumentStyle("academic")'>
<span class="style-item-icon">🎓</span>
<div class="style-item-text">
<span class="style-item-name">Academic</span>
<span class="style-item-desc">Formal serif, booktabs academic tables</span>
</div>
@if (documentStyle == "academic") { <span class="style-check">✓</span> }
</button>
<button type="button" class="style-menu-item @(documentStyle == "technical" ? "selected" : "")" @onclick='() => SetDocumentStyle("technical")'>
<span class="style-item-icon">💻</span>
<div class="style-item-text">
<span class="style-item-name">Technical</span>
<span class="style-item-desc">Monospace accents, compact dev spec</span>
</div>
@if (documentStyle == "technical") { <span class="style-check">✓</span> }
</button>
<button type="button" class="style-menu-item @(documentStyle == "executive" ? "selected" : "")" @onclick='() => SetDocumentStyle("executive")'>
<span class="style-item-icon">📊</span>
<div class="style-item-text">
<span class="style-item-name">Executive</span>
<span class="style-item-desc">Corporate navy headers, executive report</span>
</div>
@if (documentStyle == "executive") { <span class="style-check">✓</span> }
</button>
</div>
}
</div>
<!-- Export / Actions Dropdown -->
<div class="export-container">
<button class="btn-topbar btn-export" @onclick="ToggleExportDropdown" title="Export & Print Options">
@@ -180,6 +230,15 @@
</div>
</div>
<!-- Toast Notification for Auto Style and System status -->
@if (!string.IsNullOrEmpty(autoStyleToastMessage))
{
<div class="notemark-toast @autoStyleToastType" role="alert">
<span class="toast-icon">@(autoStyleToastType == "success" ? "✨" : (autoStyleToastType == "error" ? "⚠️" : "ℹ️"))</span>
<span class="toast-message">@autoStyleToastMessage</span>
</div>
}
<!-- Format Toolbar with Undo / Redo -->
<div class="format-toolbar">
<button type="button" class="toolbar-btn" @onclick="Undo" disabled="@(!canUndo)" title="Undo (Ctrl+Z)">
@@ -224,9 +283,31 @@
<button type="button" class="toolbar-btn" @onclick='() => InsertFormatting("> [!NOTE]\n> ", "", "Helpful information here")' title="Note Callout">
💡 Note
</button>
<button type="button" class="toolbar-btn" @onclick='() => InsertFormatting("| Column 1 | Column 2 |\n|---|---|\n| Cell 1 | Cell 2 |", "", "")' title="Table">
<button type="button" class="toolbar-btn" @onclick='() => InsertFormatting("\n\n| Column 1 | Column 2 |\n| :--- | :--- |\n| Cell 1 | Cell 2 |\n\n", "", "")' title="Table">
📋 Table
</button>
<span class="toolbar-divider"></span>
<button type="button"
class="toolbar-btn btn-auto-style @(isAutoStyling ? "styling" : "") @(isAutoStyledSuccess ? "success" : "")"
@onclick="AutoStyleDocument"
disabled="@isAutoStyling"
title="Auto Style & Format Markdown (Shift+Alt+F)">
@if (isAutoStyling)
{
<span class="spinner-inline">⏳</span>
<span class="toolbar-text">Formatting...</span>
}
else if (isAutoStyledSuccess)
{
<span class="success-icon">✓</span>
<span class="toolbar-text">Formatted!</span>
}
else
{
<span>🪄</span>
<span class="toolbar-text">Auto Style</span>
}
</button>
</div>
<!-- Editor Layout -->
@@ -257,7 +338,7 @@
{
<div class="preview-pane" style="@GetPreviewPaneStyle()">
<div class="preview-header-bar">
<span class="preview-label">Live Preview</span>
<span class="preview-label">Live Preview (@GetDocStyleDisplayName(documentStyle))</span>
<div class="preview-zoom-controls" title="Zoom: Scroll wheel over controls, or Ctrl + Scroll over preview">
<button type="button" class="btn-zoom" @onclick="ZoomOut" title="Zoom out (min 70%)" disabled="@(previewZoom <= 70)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
@@ -275,7 +356,7 @@
</button>
</div>
</div>
<div class="preview-content" style="@GetPreviewContentStyle()">
<div class="preview-content doc-style-@documentStyle" data-doc-style="@documentStyle" style="@GetPreviewContentStyle()">
@((MarkupString)renderedHtml)
</div>
</div>
@@ -303,6 +384,13 @@
private bool isFlagged = false;
private int flaggedCount = 0;
private List<string> 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<string>("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();
}
+658 -13
View File
@@ -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;
}
}
@@ -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
{
<div class="preview-content" style="@GetPreviewContentStyle()">
<div class="preview-content doc-style-@documentStyle" data-doc-style="@documentStyle" style="@GetPreviewContentStyle()">
@((MarkupString)renderedHtml)
</div>
}
@@ -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);
+458 -13
View File
@@ -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;
+1
View File
@@ -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";
}
+1
View File
@@ -23,6 +23,7 @@ builder.Services.AddHttpContextAccessor();
// Register Notemark services
builder.Services.AddSingleton<IMarkdownService, MarkdownService>();
builder.Services.AddSingleton<IAutoStyleService, AutoStyleService>();
builder.Services.AddSingleton<IDocumentService, DocumentService>();
builder.Services.AddScoped<ISessionService, SessionService>();
builder.Services.AddScoped<IShareService, ShareService>();
+473
View File
@@ -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<string>();
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<string>? 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<string> { "> [!WARNING]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" };
}
if (ImportantCalloutRegex.IsMatch(trimmed))
{
var text = ImportantCalloutRegex.Match(trimmed).Groups[1].Value.Trim();
return new List<string> { "> [!IMPORTANT]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" };
}
if (TipCalloutRegex.IsMatch(trimmed))
{
var text = TipCalloutRegex.Match(trimmed).Groups[1].Value.Trim();
return new List<string> { "> [!TIP]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" };
}
if (NoteCalloutRegex.IsMatch(trimmed))
{
var text = NoteCalloutRegex.Match(trimmed).Groups[1].Value.Trim();
return new List<string> { "> [!NOTE]", string.IsNullOrEmpty(text) ? "> " : $"> {text}" };
}
if (CautionCalloutRegex.IsMatch(trimmed))
{
var text = CautionCalloutRegex.Match(trimmed).Groups[1].Value.Trim();
return new List<string> { "> [!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<string> FormattedLines, int NextIndex) FormatTableBlock(string[] lines, int startIndex)
{
var tableRawLines = new List<string>();
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<List<string>>();
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<ColumnAlign>();
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<List<string>>();
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<string>();
// 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<string> 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<string> cells, int[] colWidths, List<ColumnAlign> 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<string> lines)
{
var output = new List<string>();
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
}
}
+3 -1
View File
@@ -107,7 +107,7 @@ public partial class DocumentService : IDocumentService
return docs.FirstOrDefault(d => d.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
}
public async Task<DocumentMetadata> SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List<string>? tags = null)
public async Task<DocumentMetadata> SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List<string>? 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
};
+9
View File
@@ -0,0 +1,9 @@
namespace Notemark.Services;
public interface IAutoStyleService
{
/// <summary>
/// Formats and cleans markdown content (aligns tables, fixes headings, cleans whitespace, converts smart callouts).
/// </summary>
string FormatMarkdown(string markdown);
}
+1 -1
View File
@@ -11,7 +11,7 @@ public interface IDocumentService
Task<List<DocumentMetadata>> GetDocumentsByTagAsync(string tag);
Task<DocumentMetadata?> GetMostRecentDocumentAsync();
Task<DocumentMetadata?> GetDocumentAsync(string id);
Task<DocumentMetadata> SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List<string>? tags = null);
Task<DocumentMetadata> SaveDocumentAsync(string id, string content, bool? isFlagged = null, string? customTitle = null, List<string>? tags = null, string? docStyle = null);
Task<bool> ToggleFlagAsync(string id);
Task DeleteDocumentAsync(string id);
string ExtractTitle(string content);
+119 -1
View File
@@ -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<string>(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('|');
}
}
-11
View File
@@ -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..."
}
]
+131
View File
@@ -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;
}
+367 -14
View File
@@ -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 = `<!DOCTYPE html>
<html lang="en" data-theme="${theme || 'light'}">
<html lang="en" data-theme="${theme || 'light'}" data-doc-style="${style}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title || 'Notemark Document'}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=Merriweather:ital,wght@0,400;0,700;1,400;1,700&family=Plus+Jakarta+Sans:ital,wght@0,400;0,600;0,700;0,800;1,400&display=swap" rel="stylesheet">
<style>
:root { --bg: #ffffff; --primary-text: #111827; --accent: #4f46e5; --border: #e5e7eb; --code-bg: #f1f5f9; --surface-hover: #f3f4f6; }
:root[data-theme="dark"] { --bg: #0b0f19; --primary-text: #f9fafb; --accent: #6366f1; --border: #1f2937; --code-bg: #1e293b; --surface-hover: #1f2937; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.7; background-color: var(--bg); color: var(--primary-text); max-width: 860px; margin: 2rem auto; padding: 0 1.5rem; }
:root {
--bg: #ffffff;
--primary-text: #111827;
--secondary-text: #4b5563;
--accent: #4f46e5;
--border: #e5e7eb;
--border-subtle: #f3f4f6;
--code-bg: #f8fafc;
--surface-hover: #f3f4f6;
--callout-bg: rgba(79, 70, 229, 0.08);
}
:root[data-theme="dark"] {
--bg: #0b0f19;
--primary-text: #f9fafb;
--secondary-text: #9ca3af;
--accent: #6366f1;
--border: #1f2937;
--border-subtle: #111827;
--code-bg: #111827;
--surface-hover: #1f2937;
--callout-bg: rgba(99, 102, 241, 0.12);
}
/* Base Body Typography */
body {
background-color: var(--bg);
color: var(--primary-text);
max-width: 880px;
margin: 2.5rem auto;
padding: 0 2rem;
line-height: 1.7;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* Headings */
h1, h2, h3, h4, h5, h6 { color: var(--primary-text); font-weight: 700; }
h1 { font-size: 2.2rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
h2 { font-size: 1.6rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
code { background-color: var(--code-bg); color: var(--accent); padding: 0.2em 0.4em; border-radius: 4px; font-family: monospace; }
pre { background-color: var(--code-bg); padding: 1.25rem; border-radius: 8px; overflow-x: auto; border: 1px solid var(--border); }
pre code { background: none; color: inherit; padding: 0; }
blockquote { border-left: 4px solid var(--accent); padding: 0.5rem 1rem; margin: 1em 0; background: rgba(79, 70, 229, 0.08); }
h1 { font-size: 2.2rem; border-bottom: 1px solid var(--border); padding-bottom: 0.4em; margin-top: 1.5em; }
h2 { font-size: 1.6rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; margin-top: 1.4em; }
h3 { font-size: 1.25rem; margin-top: 1.2em; }
/* Code & Pre */
code { background-color: var(--code-bg); color: var(--accent); padding: 0.2em 0.4em; border-radius: 4px; font-family: 'JetBrains Mono', Consolas, monospace; font-size: 0.9em; }
pre { background-color: var(--code-bg); padding: 1.25rem; border-radius: 8px; overflow-x: auto; border: 1px solid var(--border); line-height: 1.5; }
pre code { background: none; color: inherit; padding: 0; font-size: 0.88rem; }
/* Quotes & Callouts */
blockquote { border-left: 4px solid var(--accent); padding: 0.6rem 1.2rem; margin: 1.2em 0; background: var(--callout-bg); border-radius: 0 6px 6px 0; }
/* GitHub Alerts */
.markdown-alert { padding: 0.75rem 1rem; margin: 1.25rem 0; border-left: 4px solid; border-radius: 0 6px 6px 0; background: var(--callout-bg); }
.markdown-alert-title { display: flex; align-items: center; gap: 0.5rem; font-weight: 600; margin-bottom: 0.35rem; }
.markdown-alert-note { border-left-color: #3b82f6; background-color: rgba(59, 130, 246, 0.08); }
.markdown-alert-tip { border-left-color: #10b981; background-color: rgba(16, 185, 129, 0.08); }
.markdown-alert-important { border-left-color: #8b5cf6; background-color: rgba(139, 92, 246, 0.08); }
.markdown-alert-warning { border-left-color: #f59e0b; background-color: rgba(245, 158, 11, 0.08); }
.markdown-alert-caution { border-left-color: #ef4444; background-color: rgba(239, 68, 68, 0.08); }
/* Tables */
table { width: 100%; border-collapse: collapse; margin: 1.5em 0; }
th, td { border: 1px solid var(--border); padding: 0.6rem 0.9rem; text-align: left; }
th { background-color: var(--surface-hover); }
th, td { border: 1px solid var(--border); padding: 0.65rem 0.9rem; text-align: left; }
th { background-color: var(--surface-hover); font-weight: 600; }
img { max-width: 100%; border-radius: 8px; }
/* === PRESET 1: MODERN (Clean SaaS / Notion) === */
[data-doc-style="modern"] body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.7;
}
[data-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;
}
[data-doc-style="modern"] h2 {
font-size: 1.6rem;
font-weight: 700;
border-bottom: 1px solid var(--border);
}
[data-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;
}
[data-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);
}
[data-doc-style="modern"] td {
padding: 0.7rem 1rem;
border: 1px solid var(--border);
}
[data-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;
}
/* === PRESET 2: ACADEMIC (LaTeX / Research Paper) === */
[data-doc-style="academic"] body {
font-family: 'Merriweather', 'Georgia', 'Cambria', 'Times New Roman', serif;
line-height: 1.9;
font-size: 1.05rem;
background-color: #fdfdf9;
color: #1a1a1a;
padding: 3rem 4rem;
text-align: justify;
}
:root[data-theme="dark"][data-doc-style="academic"] body {
background-color: #121110;
color: #e5e0d8;
}
[data-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;
}
[data-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;
}
[data-doc-style="academic"] h3 {
font-family: 'Merriweather', 'Georgia', serif;
font-size: 1.2rem;
font-style: italic;
}
[data-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;
}
[data-doc-style="academic"] th {
border: 1px solid rgba(128, 128, 128, 0.35);
border-bottom: 2px solid currentColor;
font-variant: small-caps;
letter-spacing: 0.08em;
background: rgba(0, 0, 0, 0.03);
text-align: center;
padding: 0.65rem 1rem;
}
[data-doc-style="academic"] td {
border: 1px solid rgba(128, 128, 128, 0.25);
padding: 0.6rem 1rem;
}
[data-doc-style="academic"] blockquote {
font-style: italic;
border-left: none;
background: transparent;
padding: 0.8rem 2.5rem;
position: relative;
text-align: left;
}
[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) === */
[data-doc-style="technical"] body {
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;
padding: 2.5rem;
}
[data-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;
}
[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;
}
[data-doc-style="technical"] h2 {
font-family: 'JetBrains Mono', monospace;
color: #79c0ff;
font-size: 1.35rem;
border-bottom: 1px dashed #30363d;
padding-bottom: 0.3em;
}
[data-doc-style="technical"] h2::before {
content: "## ";
color: #7ee787;
font-weight: bold;
}
[data-doc-style="technical"] h3 {
font-family: 'JetBrains Mono', monospace;
color: #d2a8ff;
font-size: 1.15rem;
}
[data-doc-style="technical"] h3::before {
content: "fn ";
color: #ff7b72;
}
[data-doc-style="technical"] table {
width: 100%;
border-collapse: collapse;
border: 1.5px solid #30363d;
background-color: #161b22;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
margin: 1.5em 0;
}
[data-doc-style="technical"] th {
background-color: #21262d;
color: #7ee787;
border: 1px solid #30363d;
padding: 0.6rem 0.85rem;
font-weight: 600;
}
[data-doc-style="technical"] td {
border: 1px solid #30363d;
padding: 0.55rem 0.85rem;
color: #c9d1d9;
}
[data-doc-style="technical"] pre {
background-color: #030712 !important;
border: 1px solid #06b6d4 !important;
box-shadow: 0 0 15px rgba(6, 182, 212, 0.12);
}
[data-doc-style="technical"] blockquote {
border-left: 4px solid #238636;
background-color: rgba(35, 134, 54, 0.1);
color: #7ee787;
}
/* === PRESET 4: EXECUTIVE (Corporate Brief / McKinsey) === */
[data-doc-style="executive"] body {
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.75;
color: #0f172a;
background-color: #ffffff;
border-top: 6px solid #0f2744;
padding: 2.5rem 3.5rem;
}
:root[data-theme="dark"][data-doc-style="executive"] body {
color: #f1f5f9;
background-color: #0f172a;
border-top: 6px solid #38bdf8;
}
[data-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;
}
:root[data-theme="dark"][data-doc-style="executive"] h1 {
color: #38bdf8;
border-bottom-color: #fbbf24;
}
[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;
}
[data-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;
}
:root[data-theme="dark"][data-doc-style="executive"] h2 {
color: #38bdf8;
border-left-color: #fbbf24;
}
[data-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"][data-doc-style="executive"] table {
border-color: #334155;
}
[data-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"][data-doc-style="executive"] th {
background-color: #1e293b;
color: #38bdf8;
border: 1px solid #334155;
}
[data-doc-style="executive"] td {
padding: 0.65rem 1rem;
border: 1px solid #cbd5e1;
}
:root[data-theme="dark"][data-doc-style="executive"] td {
border: 1px solid #334155;
}
[data-doc-style="executive"] tr:nth-child(even) td {
background-color: rgba(15, 39, 68, 0.03);
}
:root[data-theme="dark"][data-doc-style="executive"] tr:nth-child(even) td {
background-color: rgba(255, 255, 255, 0.03);
}
[data-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;
}
</style>
</head>
<body>