diff --git a/Notemark/Components/App.razor b/Notemark/Components/App.razor
index 8003bb5..6299e97 100644
--- a/Notemark/Components/App.razor
+++ b/Notemark/Components/App.razor
@@ -1,5 +1,20 @@
-
-
+@inject IHttpContextAccessor HttpContextAccessor
+
+@{
+ var initialTheme = "light";
+ try
+ {
+ var cookie = HttpContextAccessor.HttpContext?.Request.Cookies["notemark_theme"];
+ if (!string.IsNullOrEmpty(cookie) && (cookie == "dark" || cookie == "light"))
+ {
+ initialTheme = cookie;
+ }
+ }
+ catch { }
+}
+
+
+
@@ -9,8 +24,24 @@
+
+
+
+
+
@@ -32,22 +63,9 @@
+
+
-
diff --git a/Notemark/Components/Pages/Editor.razor b/Notemark/Components/Pages/Editor.razor
index db1a694..09952d6 100644
--- a/Notemark/Components/Pages/Editor.razor
+++ b/Notemark/Components/Pages/Editor.razor
@@ -1,38 +1,142 @@
-@page "/"
+@page "/editor"
+@page "/editor/{DocumentId}"
+@page "/doc/{DocumentId}"
@rendermode InteractiveServer
+@implements IDisposable
@using Notemark.Services
+@using Notemark.Models
@inject ISessionService SessionService
+@inject IDocumentService DocumentService
@inject IMarkdownService MarkdownService
@inject IShareService ShareService
@inject IJSRuntime JSRuntime
@inject IHttpContextAccessor HttpContextAccessor
+@inject NavigationManager Navigation
-Notemark
+@(string.IsNullOrWhiteSpace(documentTitle) ? "Notemark" : $"{documentTitle} - Notemark")
-
-
+
@saveStatus
+
+ @wordCount words • @charCount chars • @readingTime min read
+
-
+
-
- 💾
+
+
+ @(showPreview ? "Hide Preview" : "Preview")
+
+
+
+
+
+
+
+
+
+ Export
+
+
+ @if (showExportDropdown)
+ {
+
showExportDropdown = false">
+
+
+
+
+
+ }
+
+
+
-
Share
-
+
+
+ Share
+
+
@if (showShareDropdown)
{
@@ -47,123 +151,361 @@
- CopyToClipboard(readonlyLink)" class="btn-copy-small">📋
+ CopyToClipboard(readonlyLink)" class="btn-copy-small" title="Copy view link">
+ @(copiedLink == readonlyLink ? "✓" : "📋")
+
-
+
}
}
+
+
+
+
+ ↩️
+
+
+ ↪️
+
+
+ InsertFormatting("**", "**", "bold text")' title="Bold (Ctrl+B)">
+ B
+
+ InsertFormatting("*", "*", "italic text")' title="Italic (Ctrl+I)">
+ I
+
+ InsertFormatting("### ", "", "Heading")' title="Heading 3">
+ H3
+
+
+ InsertFormatting("`", "`", "code")' title="Inline Code">
+ </>
+
+ InsertFormatting("```csharp\n", "\n```", "// Write code here")' title="Code Block">
+ { }
+
+ InsertFormatting("```mermaid\ngraph TD;\n A[Start] --> B(Process);\n B --> C{Done?};\n C -->|Yes| D[End];\n", "\n```", "")' title="Mermaid Diagram">
+ 📊 Diagram
+
+ InsertFormatting("[", "](https://example.com)", "link title")' title="Insert Link (Ctrl+K)">
+ 🔗 Link
+
+
+ InsertFormatting("- [ ] ", "", "Task item")' title="Task checkbox">
+ ☑️ Task
+
+ InsertFormatting("- ", "", "List item")' title="Bullet List">
+ • List
+
+ InsertFormatting("> ", "", "Quote text")' title="Blockquote">
+ ❝ Quote
+
+ InsertFormatting("> [!NOTE]\n> ", "", "Helpful information here")' title="Note Callout">
+ 💡 Note
+
+ InsertFormatting("| Column 1 | Column 2 |\n|---|---|\n| Cell 1 | Cell 2 |", "", "")' title="Table">
+ 📋 Table
+
+
+
-
-
+
+
@if (showPreview)
{
}
-
+
@if (showPreview)
{
-
+
+
@((MarkupString)renderedHtml)
}
-
- @* Removed modal - now using dropdown *@
+
+
@code {
+ [Parameter] public string? DocumentId { get; set; }
+
private string sessionId = "";
+ private string documentTitle = "Untitled Note";
+ private bool isFlagged = false;
+ private int flaggedCount = 0;
+ private List currentTags = new();
private string markdownContent = "";
private string renderedHtml = "";
- private bool showPreview = false;
+ private bool showPreview = true;
private string saveStatus = "Saved";
-
+ private bool showNotesDrawer = false;
+ private bool showExportDropdown = false;
+ private ElementReference editorTextareaRef;
+ private ElementReference editorLayoutRef;
+ private DotNetObjectReference? dotNetRef;
+ private double splitRatio = 0.5;
+ private bool isDragging = false;
+ private int previewZoom = 100;
+
+ // Undo / Redo History Stack
+ private readonly List undoStack = new();
+ private readonly List redoStack = new();
+ private bool canUndo => undoStack.Count > 0;
+ private bool canRedo => redoStack.Count > 0;
+
+ // Word stats
+ private int wordCount = 0;
+ private int charCount = 0;
+ private int readingTime = 1;
+
// Share dropdown state
private bool showShareDropdown = false;
private bool isGeneratingLinks = false;
private string readonlyLink = "";
private string editableLink = "";
-
+ private string copiedLink = "";
+
// Auto-save timer
private Timer? autoSaveTimer;
private bool hasUnsavedChanges = false;
protected override async Task OnInitializedAsync()
{
- // Get or create session
var httpContext = HttpContextAccessor.HttpContext;
- if (httpContext != null)
+ if (!string.IsNullOrEmpty(DocumentId))
+ {
+ sessionId = DocumentId;
+ }
+ else if (httpContext != null)
{
sessionId = SessionService.GetOrCreateSessionId(httpContext);
-
- // Load existing content
- markdownContent = await SessionService.LoadSessionContentAsync(sessionId);
-
- // Render initial preview if needed
- if (showPreview)
- {
- renderedHtml = MarkdownService.RenderToHtml(markdownContent);
- }
}
+ else
+ {
+ sessionId = Guid.NewGuid().ToString();
+ }
+
+ await LoadDocumentData();
}
- private async Task OnContentChanged(ChangeEventArgs e)
+ private async Task LoadDocumentData()
{
- markdownContent = e.Value?.ToString() ?? "";
- hasUnsavedChanges = true;
-
- // Update preview if visible
+ markdownContent = await SessionService.LoadSessionContentAsync(sessionId);
+ var meta = await DocumentService.GetDocumentAsync(sessionId);
+ if (meta != null)
+ {
+ documentTitle = meta.Title;
+ isFlagged = meta.IsFlagged;
+ currentTags = meta.Tags;
+ }
+ else
+ {
+ documentTitle = DocumentService.ExtractTitle(markdownContent);
+ isFlagged = false;
+ currentTags = DocumentService.ExtractTags(markdownContent);
+ }
+
+ undoStack.Clear();
+ redoStack.Clear();
+
+ UpdateStatistics();
if (showPreview)
{
renderedHtml = MarkdownService.RenderToHtml(markdownContent);
}
-
- // Debounced autosave - cancel previous timer and start new one
- autoSaveTimer?.Dispose();
- autoSaveTimer = new Timer(async _ =>
+
+ await UpdateFlaggedCount();
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
{
- await InvokeAsync(async () => await AutoSave());
- }, null, 2000, Timeout.Infinite); // Increased to 2 seconds for production
-
- // Only update status if it was previously "Saved"
- if (saveStatus == "Saved")
+ try
+ {
+ // Save current document ID to localStorage as the last worked on note on this device
+ await JSRuntime.InvokeVoidAsync("localStorage.setItem", "notemark-last-doc-id", sessionId);
+
+ // Initialize Tab handling, image paste/drop, and scroll sync
+ await JSRuntime.InvokeVoidAsync("notemarkEditor.initTextarea", editorTextareaRef);
+
+ // Load saved split ratio
+ var savedRatio = await JSRuntime.InvokeAsync("localStorage.getItem", "notemark-split-ratio");
+ if (!string.IsNullOrEmpty(savedRatio) && double.TryParse(savedRatio, out var ratio))
+ {
+ splitRatio = Math.Max(0.2, Math.Min(0.8, ratio));
+ StateHasChanged();
+ }
+
+ // Load saved preview zoom
+ var savedZoom = await JSRuntime.InvokeAsync("localStorage.getItem", "notemark-preview-zoom");
+ if (!string.IsNullOrEmpty(savedZoom) && int.TryParse(savedZoom, out var z))
+ {
+ previewZoom = Math.Clamp(z, 70, 200);
+ StateHasChanged();
+ }
+
+ // Attach mouse wheel zoom listener (Ctrl + Wheel over preview or direct scroll over zoom controls)
+ dotNetRef ??= DotNetObjectReference.Create(this);
+ await JSRuntime.InvokeVoidAsync("notemarkEditor.attachPreviewZoomWheel", ".preview-pane", dotNetRef);
+ }
+ catch { }
+ }
+
+ // Highlight code and render Mermaid in preview
+ if (showPreview && !string.IsNullOrEmpty(renderedHtml))
{
- saveStatus = "Modified";
+ try
+ {
+ await JSRuntime.InvokeVoidAsync("notemarkEditor.highlightPreview", ".preview-content");
+ }
+ catch { }
+ }
+ }
+
+ private void UpdateStatistics()
+ {
+ charCount = markdownContent.Length;
+ var words = markdownContent.Split(new[] { ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+ wordCount = words.Length;
+ readingTime = Math.Max(1, (int)Math.Ceiling(wordCount / 200.0));
+ currentTags = DocumentService.ExtractTags(markdownContent);
+ }
+
+ private async Task OnContentChanged(ChangeEventArgs e)
+ {
+ var newContent = e.Value?.ToString() ?? "";
+ if (newContent != markdownContent)
+ {
+ // Push to undo stack (max 50 states)
+ if (undoStack.Count > 50) undoStack.RemoveAt(0);
+ undoStack.Add(markdownContent);
+ redoStack.Clear();
+
+ markdownContent = newContent;
+ hasUnsavedChanges = true;
+ UpdateStatistics();
+
+ if (documentTitle == "Untitled Note" || string.IsNullOrWhiteSpace(documentTitle))
+ {
+ documentTitle = DocumentService.ExtractTitle(markdownContent);
+ }
+
+ if (showPreview)
+ {
+ renderedHtml = MarkdownService.RenderToHtml(markdownContent);
+ }
+
+ // Debounced autosave
+ autoSaveTimer?.Dispose();
+ autoSaveTimer = new Timer(async _ =>
+ {
+ await InvokeAsync(async () => await AutoSave());
+ }, null, 1500, Timeout.Infinite);
+
+ if (saveStatus == "Saved")
+ {
+ saveStatus = "Modified";
+ StateHasChanged();
+ }
+ }
+ }
+
+ private void Undo()
+ {
+ if (canUndo)
+ {
+ var prev = undoStack[^1];
+ undoStack.RemoveAt(undoStack.Count - 1);
+ redoStack.Add(markdownContent);
+ markdownContent = prev;
+ hasUnsavedChanges = true;
+ UpdateStatistics();
+ if (showPreview) renderedHtml = MarkdownService.RenderToHtml(markdownContent);
StateHasChanged();
}
}
+ private void Redo()
+ {
+ if (canRedo)
+ {
+ var next = redoStack[^1];
+ redoStack.RemoveAt(redoStack.Count - 1);
+ undoStack.Add(markdownContent);
+ markdownContent = next;
+ hasUnsavedChanges = true;
+ UpdateStatistics();
+ if (showPreview) renderedHtml = MarkdownService.RenderToHtml(markdownContent);
+ StateHasChanged();
+ }
+ }
+
+ private async Task OnTitleChanged(ChangeEventArgs e)
+ {
+ documentTitle = e.Value?.ToString() ?? "Untitled Note";
+ hasUnsavedChanges = true;
+ await SaveContent();
+ }
+
private async Task AutoSave()
{
if (hasUnsavedChanges && !string.IsNullOrEmpty(sessionId))
@@ -174,61 +516,68 @@
private async Task SaveContent()
{
- if (string.IsNullOrEmpty(sessionId) || !hasUnsavedChanges)
- return;
+ if (string.IsNullOrEmpty(sessionId)) return;
- var previousStatus = saveStatus;
-
- // Set saving status only when actually starting to save
saveStatus = "Saving...";
- StateHasChanged(); // Use StateHasChanged instead of InvokeAsync
-
+ StateHasChanged();
+
try
{
- // Longer timeout for large content (30 seconds)
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
-
await SessionService.SaveSessionContentAsync(sessionId, markdownContent);
-
+ await DocumentService.SaveDocumentAsync(sessionId, markdownContent, isFlagged, documentTitle, currentTags);
hasUnsavedChanges = false;
saveStatus = "Saved";
+ await UpdateFlaggedCount();
}
- catch (OperationCanceledException)
- {
- saveStatus = "Timeout";
- // Simplified retry - no immediate re-rendering
- _ = Task.Delay(5000).ContinueWith(async _ =>
- {
- if (hasUnsavedChanges)
- {
- await InvokeAsync(async () => await SaveContent());
- }
- });
- }
- catch (Exception)
+ catch
{
saveStatus = "Error";
- // Simplified retry - no immediate re-rendering
- _ = Task.Delay(3000).ContinueWith(async _ =>
- {
- if (hasUnsavedChanges)
- {
- await InvokeAsync(async () => await SaveContent());
- }
- });
}
-
- // Only trigger re-render if status actually changed
- if (saveStatus != previousStatus)
+
+ StateHasChanged();
+ }
+
+ private async Task ToggleFlag()
+ {
+ isFlagged = await DocumentService.ToggleFlagAsync(sessionId);
+ await UpdateFlaggedCount();
+ }
+
+ private async Task UpdateFlaggedCount()
+ {
+ try
{
- StateHasChanged();
+ var flagged = await DocumentService.GetFlaggedDocumentsAsync();
+ flaggedCount = flagged.Count;
}
+ catch { }
+ }
+
+ private void ToggleNotesDrawer()
+ {
+ showNotesDrawer = !showNotesDrawer;
+ }
+
+ private void ToggleExportDropdown()
+ {
+ showExportDropdown = !showExportDropdown;
+ }
+
+ private async Task OnDocumentSelected(string docId)
+ {
+ if (hasUnsavedChanges)
+ {
+ await SaveContent();
+ }
+
+ sessionId = docId;
+ await LoadDocumentData();
+ StateHasChanged();
}
private void TogglePreview()
{
showPreview = !showPreview;
-
if (showPreview)
{
renderedHtml = MarkdownService.RenderToHtml(markdownContent);
@@ -237,16 +586,38 @@
private async Task OnKeyDown(KeyboardEventArgs e)
{
- // Ctrl+S or Cmd+S for manual save
- if ((e.CtrlKey || e.MetaKey) && e.Key == "s")
+ if ((e.CtrlKey || e.MetaKey) && e.Key.Equals("s", StringComparison.OrdinalIgnoreCase))
{
await ManualSave();
}
- // Ctrl+N or Cmd+N for new document
- else if ((e.CtrlKey || e.MetaKey) && e.Key == "n")
+ else if ((e.CtrlKey || e.MetaKey) && e.Key.Equals("n", StringComparison.OrdinalIgnoreCase))
{
await NewDocument();
}
+ else if ((e.CtrlKey || e.MetaKey) && e.Key.Equals("z", StringComparison.OrdinalIgnoreCase) && !e.ShiftKey)
+ {
+ Undo();
+ }
+ else if ((e.CtrlKey || e.MetaKey) && (e.Key.Equals("y", StringComparison.OrdinalIgnoreCase) || (e.Key.Equals("z", StringComparison.OrdinalIgnoreCase) && e.ShiftKey)))
+ {
+ Redo();
+ }
+ else if ((e.CtrlKey || e.MetaKey) && e.Key.Equals("b", StringComparison.OrdinalIgnoreCase))
+ {
+ await InsertFormatting("**", "**", "bold text");
+ }
+ else if ((e.CtrlKey || e.MetaKey) && e.Key.Equals("i", StringComparison.OrdinalIgnoreCase))
+ {
+ await InsertFormatting("*", "*", "italic text");
+ }
+ else if ((e.CtrlKey || e.MetaKey) && e.Key.Equals("k", StringComparison.OrdinalIgnoreCase))
+ {
+ await InsertFormatting("[", "](https://example.com)", "link text");
+ }
+ else if ((e.CtrlKey || e.MetaKey) && e.Key.Equals("p", StringComparison.OrdinalIgnoreCase))
+ {
+ await PrintDocument();
+ }
}
private async Task ManualSave()
@@ -256,34 +627,59 @@
private async Task NewDocument()
{
- // Ask for confirmation if there are unsaved changes
if (hasUnsavedChanges)
{
- bool confirmed = await JSRuntime.InvokeAsync("confirm",
- "You have unsaved changes. Are you sure you want to create a new document?");
- if (!confirmed)
- return;
+ await SaveContent();
}
- // Simple approach: Clear current session content
- markdownContent = "";
- renderedHtml = "";
+ sessionId = Guid.NewGuid().ToString();
+ markdownContent = "# Untitled Note\n\nStart typing here... #ideas";
+ documentTitle = "Untitled Note";
+ isFlagged = false;
hasUnsavedChanges = false;
- saveStatus = "New Document";
-
- // Save empty content to current session
- if (!string.IsNullOrEmpty(sessionId))
- {
- await SessionService.SaveSessionContentAsync(sessionId, "");
- }
-
- // Update UI
- await InvokeAsync(StateHasChanged);
-
- // Reset save status after a moment
- await Task.Delay(1500);
saveStatus = "Saved";
- await InvokeAsync(StateHasChanged);
+
+ undoStack.Clear();
+ redoStack.Clear();
+
+ UpdateStatistics();
+ renderedHtml = MarkdownService.RenderToHtml(markdownContent);
+
+ await DocumentService.SaveDocumentAsync(sessionId, markdownContent, isFlagged, documentTitle, currentTags);
+ await SessionService.SaveSessionContentAsync(sessionId, markdownContent);
+ await UpdateFlaggedCount();
+
+ StateHasChanged();
+ }
+
+ private async Task InsertFormatting(string prefix, string suffix, string defaultText)
+ {
+ try
+ {
+ await JSRuntime.InvokeVoidAsync("notemarkEditor.wrapSelection", editorTextareaRef, prefix, suffix, defaultText);
+ }
+ catch { }
+ }
+
+ private async Task ExportMarkdown()
+ {
+ showExportDropdown = false;
+ var filename = $"{documentTitle.Replace(" ", "_").ToLowerInvariant()}.md";
+ await JSRuntime.InvokeVoidAsync("notemarkEditor.downloadMarkdown", filename, markdownContent);
+ }
+
+ private async Task ExportHtml()
+ {
+ showExportDropdown = false;
+ var filename = $"{documentTitle.Replace(" ", "_").ToLowerInvariant()}.html";
+ var currentTheme = await JSRuntime.InvokeAsync("notemarkTheme.getCurrentTheme");
+ await JSRuntime.InvokeVoidAsync("notemarkEditor.downloadHtml", filename, documentTitle, renderedHtml, currentTheme);
+ }
+
+ private async Task PrintDocument()
+ {
+ showExportDropdown = false;
+ await JSRuntime.InvokeVoidAsync("notemarkEditor.printDocument");
}
private async Task ToggleShareDropdown()
@@ -294,23 +690,21 @@
isGeneratingLinks = true;
readonlyLink = "";
editableLink = "";
-
- await InvokeAsync(StateHasChanged);
-
+
+ StateHasChanged();
+
try
{
- // Generate share tokens
var readonlyToken = await ShareService.CreateShareTokenAsync(sessionId, "readonly");
var editableToken = await ShareService.CreateShareTokenAsync(sessionId, "editable");
-
- // Build URLs
+
var httpContext = HttpContextAccessor.HttpContext;
- if (httpContext != null)
- {
- var baseUrl = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}";
- readonlyLink = $"{baseUrl}/s/{readonlyToken}";
- editableLink = $"{baseUrl}/e/{editableToken}";
- }
+ var baseUrl = httpContext != null
+ ? $"{httpContext.Request.Scheme}://{httpContext.Request.Host}"
+ : Navigation.BaseUri.TrimEnd('/');
+
+ readonlyLink = $"{baseUrl}/s/{readonlyToken}";
+ editableLink = $"{baseUrl}/e/{editableToken}";
}
catch
{
@@ -320,7 +714,7 @@
finally
{
isGeneratingLinks = false;
- await InvokeAsync(StateHasChanged);
+ StateHasChanged();
}
}
else
@@ -332,6 +726,7 @@
private void CloseShareDropdown()
{
showShareDropdown = false;
+ copiedLink = "";
}
private async Task CopyToClipboard(string text)
@@ -339,46 +734,20 @@
try
{
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
- // Could add a toast notification here in the future
+ copiedLink = text;
+ StateHasChanged();
+ _ = Task.Delay(2000).ContinueWith(_ =>
+ {
+ copiedLink = "";
+ InvokeAsync(StateHasChanged);
+ });
}
catch
{
- // Fallback for browsers that don't support clipboard API
await JSRuntime.InvokeVoidAsync("prompt", "Copy this link:", text);
}
}
- public void Dispose()
- {
- autoSaveTimer?.Dispose();
- }
-
- // Simple splitter functionality
- private ElementReference editorLayoutRef;
- private double splitRatio = 0.5; // 50/50 by default
- private bool isDragging = false;
-
- protected override async Task OnAfterRenderAsync(bool firstRender)
- {
- if (firstRender)
- {
- try
- {
- // Load saved split ratio from localStorage
- var savedRatio = await JSRuntime.InvokeAsync("localStorage.getItem", "notemark-split-ratio");
- if (!string.IsNullOrEmpty(savedRatio) && double.TryParse(savedRatio, out var ratio))
- {
- splitRatio = Math.Max(0.2, Math.Min(0.8, ratio));
- StateHasChanged();
- }
- }
- catch
- {
- // Ignore localStorage errors
- }
- }
- }
-
private string GetEditorPaneStyle()
{
if (!showPreview) return "";
@@ -394,20 +763,16 @@
private async Task OnSplitterMouseDown(MouseEventArgs e)
{
isDragging = true;
- await JSRuntime.InvokeVoidAsync("startSplitterDrag", DotNetObjectReference.Create(this));
+ dotNetRef?.Dispose();
+ dotNetRef = DotNetObjectReference.Create(this);
+ await JSRuntime.InvokeVoidAsync("startSplitterDrag", dotNetRef);
}
[JSInvokable]
public async Task UpdateSplitRatio(double clientX, double containerWidth)
{
if (!isDragging || containerWidth <= 0) return;
-
- var newRatio = Math.Max(0.2, Math.Min(0.8, clientX / containerWidth)); // Clamp between 20% and 80%
-
- // Update immediately for smooth dragging
- splitRatio = newRatio;
-
- // Use StateHasChanged for immediate visual feedback
+ splitRatio = Math.Max(0.2, Math.Min(0.8, clientX / containerWidth));
StateHasChanged();
}
@@ -415,20 +780,71 @@
public async Task StopDragging()
{
if (!isDragging) return;
-
isDragging = false;
-
- // Save split ratio to localStorage when dragging stops
try
{
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "notemark-split-ratio", splitRatio.ToString("F2"));
}
- catch
- {
- // Ignore localStorage errors
- }
-
- // Single StateHasChanged at the end
+ catch { }
StateHasChanged();
}
+
+ [JSInvokable]
+ public void AdjustZoom(int delta)
+ {
+ previewZoom = Math.Clamp(previewZoom + delta, 70, 200);
+ SavePreviewZoom();
+ StateHasChanged();
+ }
+
+ private void ZoomIn()
+ {
+ previewZoom = Math.Min(200, previewZoom + 10);
+ SavePreviewZoom();
+ StateHasChanged();
+ }
+
+ private void ZoomOut()
+ {
+ previewZoom = Math.Max(70, previewZoom - 10);
+ SavePreviewZoom();
+ StateHasChanged();
+ }
+
+ private void ResetZoom()
+ {
+ previewZoom = 100;
+ SavePreviewZoom();
+ StateHasChanged();
+ }
+
+ private async void SavePreviewZoom()
+ {
+ try
+ {
+ await JSRuntime.InvokeVoidAsync("localStorage.setItem", "notemark-preview-zoom", previewZoom.ToString());
+ }
+ catch { }
+ }
+
+ private string GetPreviewContentStyle()
+ {
+ var factor = (previewZoom / 100.0).ToString("0.##", System.Globalization.CultureInfo.InvariantCulture);
+ return $"zoom: {factor}; font-size: calc(1rem * {factor}); --preview-zoom: {factor};";
+ }
+
+ private async Task GoToHome()
+ {
+ if (hasUnsavedChanges)
+ {
+ await SaveContent();
+ }
+ Navigation.NavigateTo("/");
+ }
+
+ public void Dispose()
+ {
+ autoSaveTimer?.Dispose();
+ dotNetRef?.Dispose();
+ }
}
\ No newline at end of file
diff --git a/Notemark/Components/Pages/Editor.razor.css b/Notemark/Components/Pages/Editor.razor.css
index 3d95185..0f738a8 100644
--- a/Notemark/Components/Pages/Editor.razor.css
+++ b/Notemark/Components/Pages/Editor.razor.css
@@ -1,9 +1,10 @@
-/* Notemark Editor Styles */
+/* Notemark Main Editor Layout */
.editor-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--bg);
+ overflow: hidden;
}
/* Top Bar */
@@ -11,11 +12,13 @@
display: flex;
align-items: center;
justify-content: space-between;
- padding: 0.75rem 1rem;
+ padding: 0.5rem 1rem;
background-color: var(--surface);
border-bottom: 1px solid var(--border);
- height: 60px;
+ min-height: 54px;
flex-shrink: 0;
+ gap: 0.75rem;
+ z-index: 100;
}
.top-bar-left,
@@ -23,124 +26,249 @@
.top-bar-right {
display: flex;
align-items: center;
- gap: 0.75rem;
+ gap: 0.5rem;
}
.top-bar-center {
flex: 1;
justify-content: center;
+ gap: 1rem;
}
-/* Buttons */
-.btn-toggle,
-.btn-share,
-.btn-save,
-.btn-new,
-.btn-copy,
-.btn-close {
- background: none;
- border: 1px solid var(--border);
- color: var(--primary-text);
- padding: 0.5rem 1rem;
- border-radius: 6px;
- cursor: pointer;
- font-size: 0.875rem;
- font-family: inherit;
- transition: all 0.2s ease;
-}
-
-.btn-save {
- padding: 0.5rem;
- min-width: 2.5rem;
+/* Document Title Input */
+.doc-title-wrapper {
display: flex;
align-items: center;
- justify-content: center;
- font-size: 1rem;
}
-.btn-save:enabled:hover {
- background-color: var(--success);
- color: white;
- border-color: var(--success);
+.doc-title-input {
+ background: none;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ padding: 0.35rem 0.6rem;
+ font-size: 0.9375rem;
+ font-weight: 600;
+ color: var(--primary-text);
+ outline: none;
+ transition: all 0.15s ease;
+ max-width: 200px;
+}
+
+.doc-title-input:hover {
+ border-color: var(--border);
+ background-color: var(--surface-hover);
+}
+
+.doc-title-input:focus {
+ border-color: var(--accent);
+ background-color: var(--bg);
+ box-shadow: 0 0 0 2px var(--accent-light);
+}
+
+/* Topbar Tag Pills */
+.topbar-tags {
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+}
+
+.topbar-tag-pill {
+ font-size: 0.7rem;
+ font-weight: 600;
+ padding: 0.15rem 0.45rem;
+ background-color: var(--accent-light);
+ color: var(--accent);
+ border-radius: 999px;
+ border: 1px solid var(--border);
+}
+
+/* Stats Pill */
+.doc-stats {
+ font-size: 0.75rem;
+ color: var(--secondary-text);
+ background-color: var(--bg);
+ padding: 0.25rem 0.6rem;
+ border-radius: 999px;
+ border: 1px solid var(--border);
+ white-space: nowrap;
+}
+
+/* Topbar Buttons */
+.btn-topbar {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ padding: 0.45rem 0.75rem;
+ background-color: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--primary-text);
+ border-radius: 7px;
+ font-size: 0.8125rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow: var(--shadow-sm);
+ white-space: nowrap;
+ height: 34px;
+}
+
+.btn-topbar:hover:not(:disabled) {
+ background-color: var(--surface-hover);
+ border-color: var(--accent);
+ color: var(--accent);
transform: translateY(-1px);
}
-.btn-save:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- background-color: transparent;
+.btn-topbar:active:not(:disabled) {
+ transform: translateY(0);
}
-.btn-save:disabled:hover {
- background-color: transparent;
- transform: none;
+.btn-topbar:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+ box-shadow: none;
+}
+
+.btn-notes {
+ position: relative;
+ font-weight: 600;
+}
+
+.notes-badge {
+ background-color: #ef4444;
+ color: #ffffff;
+ font-size: 0.6875rem;
+ font-weight: 700;
+ padding: 0.05rem 0.35rem;
+ border-radius: 999px;
+ line-height: 1.2;
}
.btn-new {
- background-color: var(--success);
- color: white;
- border-color: var(--success);
+ color: var(--success);
+ border-color: var(--border);
}
.btn-new:hover {
- background-color: var(--accent);
- border-color: var(--accent);
- transform: translateY(-1px);
+ background-color: rgba(16, 185, 129, 0.1);
+ border-color: var(--success);
+ color: var(--success);
}
-.btn-toggle:hover,
-.btn-share:hover,
-.btn-copy:hover {
- background-color: var(--accent);
- color: white;
- border-color: var(--accent);
+.btn-flag {
+ padding: 0.45rem 0.55rem;
+ font-size: 0.95rem;
+}
+
+.btn-flag.flagged {
+ border-color: #ef4444;
+ background-color: rgba(239, 68, 68, 0.08);
}
.btn-toggle.active {
- background-color: var(--accent);
- color: white;
+ background-color: var(--accent-light);
border-color: var(--accent);
+ color: var(--accent);
}
-.btn-close {
- padding: 0.25rem 0.5rem;
- font-size: 1.25rem;
- line-height: 1;
- background: none;
- border: none;
+.btn-save {
+ padding: 0.45rem 0.65rem;
}
-.btn-close:hover {
- background-color: var(--error);
- color: white;
+.btn-save:enabled:hover {
+ background-color: rgba(16, 185, 129, 0.12);
+ border-color: var(--success);
+ color: var(--success);
}
-/* Save Status */
+.btn-export {
+ padding: 0.45rem 0.65rem;
+}
+
+/* Save Status Indicator */
.save-status {
- font-size: 0.875rem;
- font-weight: 500;
- padding: 0.25rem 0.75rem;
- border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ padding: 0.2rem 0.5rem;
+ border-radius: 999px;
+ text-transform: capitalize;
}
.save-status.saved {
color: var(--success);
+ background-color: rgba(16, 185, 129, 0.1);
}
.save-status.modified {
color: var(--secondary-text);
+ background-color: var(--bg);
+ border: 1px solid var(--border);
}
.save-status.saving {
color: var(--accent);
+ background-color: var(--accent-light);
}
.save-status.error {
color: var(--error);
+ background-color: rgba(239, 68, 68, 0.1);
}
-.save-status.timeout {
- color: var(--error);
+/* Formatting Toolbar */
+.format-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+ padding: 0.35rem 1rem;
+ background-color: var(--surface);
+ border-bottom: 1px solid var(--border);
+ overflow-x: auto;
+ flex-shrink: 0;
+ scrollbar-width: none;
+}
+
+.format-toolbar::-webkit-scrollbar {
+ display: none;
+}
+
+.toolbar-btn {
+ background: none;
+ border: 1px solid transparent;
+ color: var(--secondary-text);
+ padding: 0.3rem 0.55rem;
+ border-radius: 5px;
+ font-size: 0.8125rem;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 28px;
+ min-width: 28px;
+}
+
+.toolbar-btn:hover:not(:disabled) {
+ background-color: var(--surface-hover);
+ border-color: var(--border);
+ color: var(--primary-text);
+}
+
+.toolbar-btn:active:not(:disabled) {
+ background-color: var(--border);
+}
+
+.toolbar-btn:disabled {
+ opacity: 0.35;
+ cursor: not-allowed;
+}
+
+.toolbar-divider {
+ width: 1px;
+ height: 18px;
+ background-color: var(--border);
+ margin: 0 0.25rem;
}
/* Editor Layout */
@@ -148,6 +276,7 @@
flex: 1;
display: flex;
min-height: 0;
+ overflow: hidden;
}
.editor-layout.full-view {
@@ -164,10 +293,7 @@
display: flex;
flex-direction: column;
min-width: 0;
-}
-
-.split-view .editor-pane {
- border-right: 1px solid var(--border);
+ background-color: var(--bg);
}
.markdown-editor {
@@ -176,20 +302,58 @@
border: none;
outline: none;
resize: none;
- font-family: 'JetBrains Mono', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
- font-size: 14px;
- line-height: 1.6;
- padding: 1.5rem;
+ font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', Consolas, Monaco, monospace;
+ font-size: 14.5px;
+ line-height: 1.7;
+ padding: 1.5rem 2rem;
background-color: var(--bg);
color: var(--primary-text);
+ tab-size: 2;
+ box-sizing: border-box;
}
.markdown-editor::placeholder {
color: var(--secondary-text);
+ opacity: 0.6;
}
-.markdown-editor:focus {
- outline: none;
+/* Splitter */
+.splitter {
+ width: 8px;
+ background-color: var(--border);
+ cursor: col-resize;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ flex-shrink: 0;
+ transition: background-color 0.15s ease, width 0.15s ease;
+ z-index: 10;
+}
+
+.splitter:hover {
+ background-color: var(--accent);
+ width: 9px;
+}
+
+.splitter.dragging {
+ background-color: var(--accent);
+ width: 10px;
+}
+
+.splitter-handle {
+ width: 2px;
+ height: 36px;
+ background-color: var(--secondary-text);
+ border-radius: 1px;
+ transition: all 0.15s ease;
+}
+
+.splitter:hover .splitter-handle,
+.splitter.dragging .splitter-handle {
+ background-color: #ffffff;
+ height: 50px;
+ width: 3px;
}
/* Preview Pane */
@@ -199,17 +363,91 @@
flex-direction: column;
background-color: var(--surface);
min-width: 0;
+ border-left: 1px solid var(--border);
+ position: relative;
+}
+
+.preview-header-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.35rem 1rem;
+ background-color: var(--surface);
+ border-bottom: 1px solid var(--border-subtle);
+ flex-shrink: 0;
+ font-size: 0.75rem;
+ color: var(--secondary-text);
+}
+
+.preview-label {
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.preview-zoom-controls {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.2rem;
+ background-color: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 0.15rem 0.25rem;
+}
+
+.btn-zoom {
+ background: none;
+ border: none;
+ color: var(--secondary-text);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.2rem 0.35rem;
+ border-radius: 4px;
+ transition: all 0.15s ease;
+}
+
+.btn-zoom:hover:not(:disabled) {
+ background-color: var(--surface-hover);
+ color: var(--accent);
+}
+
+.btn-zoom:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+.btn-zoom-reset {
+ background: none;
+ border: none;
+ font-size: 0.72rem;
+ font-weight: 700;
+ color: var(--primary-text);
+ cursor: pointer;
+ padding: 0.15rem 0.35rem;
+ border-radius: 3px;
+ min-width: 38px;
+ text-align: center;
+}
+
+.btn-zoom-reset:hover {
+ background-color: var(--accent-light);
+ color: var(--accent);
}
.preview-content {
flex: 1;
- padding: 1.5rem;
+ padding: 2rem 2.5rem;
overflow-y: auto;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
- line-height: 1.6;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+ line-height: 1.7;
+ color: var(--primary-text);
+ max-width: 900px;
+ box-sizing: border-box;
}
-/* Markdown Preview Styles */
+/* Markdown Preview Typography */
.preview-content h1,
.preview-content h2,
.preview-content h3,
@@ -217,77 +455,272 @@
.preview-content h5,
.preview-content h6 {
color: var(--primary-text);
- margin-top: 2em;
- margin-bottom: 1em;
- font-weight: 600;
+ font-weight: 700;
+ line-height: 1.3;
+ margin-top: 1.75em;
+ margin-bottom: 0.6em;
+ letter-spacing: -0.02em;
}
-.preview-content h1 { font-size: 2em; }
-.preview-content h2 { font-size: 1.5em; }
-.preview-content h3 { font-size: 1.25em; }
+.preview-content h1:first-child,
+.preview-content h2:first-child,
+.preview-content h3:first-child {
+ margin-top: 0;
+}
+
+.preview-content h1 {
+ font-size: 2.15em;
+ border-bottom: 1px solid var(--border);
+ padding-bottom: 0.4em;
+}
+
+.preview-content h2 {
+ font-size: 1.6em;
+ border-bottom: 1px solid var(--border-subtle);
+ padding-bottom: 0.3em;
+}
+
+.preview-content h3 { font-size: 1.3em; }
+.preview-content h4 { font-size: 1.1em; }
+.preview-content h5 { font-size: 0.95em; text-transform: uppercase; letter-spacing: 0.05em; }
+.preview-content h6 { font-size: 0.875em; color: var(--secondary-text); }
.preview-content p {
- margin-bottom: 1em;
- color: var(--primary-text);
+ margin-top: 0;
+ margin-bottom: 1.1em;
+ font-size: 1em;
}
-.preview-content code {
- background-color: var(--bg);
- padding: 0.2em 0.4em;
- border-radius: 3px;
- font-family: 'JetBrains Mono', 'SF Mono', 'Monaco', monospace;
- font-size: 0.875em;
+.preview-content a {
color: var(--accent);
+ text-decoration: underline;
+ text-underline-offset: 3px;
+ font-weight: 500;
+ transition: color 0.15s ease;
}
-.preview-content pre {
- background-color: var(--bg);
- padding: 1em;
+.preview-content a:hover {
+ color: var(--accent-hover);
+}
+
+/* Inline Code */
+.preview-content :not(pre) > code {
+ background-color: var(--code-bg);
+ color: var(--accent);
+ padding: 0.2em 0.45em;
border-radius: 6px;
- overflow-x: auto;
- margin-bottom: 1em;
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
+ font-size: 0.875em;
border: 1px solid var(--border);
+ font-weight: 500;
+}
+
+/* Pre / Code Blocks */
+.preview-content pre {
+ position: relative;
+ background-color: var(--code-bg);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 1.25rem 1.5rem;
+ margin: 1.25em 0;
+ overflow-x: auto;
+ box-shadow: var(--shadow-sm);
}
.preview-content pre code {
- background: none;
- padding: 0;
+ font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
+ font-size: 0.9em;
+ line-height: 1.6;
+}
+
+/* Mermaid Rendered Charts */
+.mermaid-rendered {
+ display: flex;
+ justify-content: center;
+ margin: 1.5em 0;
+ padding: 1rem;
+ background-color: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ overflow-x: auto;
+}
+
+/* Code Copy Button */
+.code-copy-btn {
+ position: absolute;
+ top: 0.65rem;
+ right: 0.65rem;
+ padding: 0.35rem 0.5rem;
+ background-color: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--secondary-text);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: all 0.2s ease;
+ box-shadow: var(--shadow-sm);
+ z-index: 5;
+}
+
+.preview-content pre:hover .code-copy-btn {
+ opacity: 1;
+}
+
+.code-copy-btn:hover {
+ color: var(--primary-text);
+ background-color: var(--surface-hover);
+ border-color: var(--accent);
+}
+
+.code-copy-btn.copied {
+ color: var(--success);
+ border-color: var(--success);
+ opacity: 1;
+}
+
+/* Blockquotes & Callouts / Alerts */
+.preview-content blockquote {
+ margin: 1.25em 0;
+ padding: 0.75rem 1.25rem;
+ border-left: 4px solid var(--accent);
+ background-color: var(--accent-light);
+ border-radius: 0 8px 8px 0;
color: var(--primary-text);
}
-.preview-content blockquote {
- border-left: 4px solid var(--accent);
- padding-left: 1em;
- margin-left: 0;
- margin-bottom: 1em;
- color: var(--secondary-text);
-}
-
-.preview-content ul,
-.preview-content ol {
- margin-bottom: 1em;
- padding-left: 2em;
+.preview-content blockquote p:last-child {
+ margin-bottom: 0;
}
+/* Tables */
.preview-content table {
width: 100%;
- border-collapse: collapse;
- margin-bottom: 1em;
+ border-collapse: separate;
+ border-spacing: 0;
+ margin: 1.5em 0;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ overflow: hidden;
+ font-size: 0.9375rem;
}
.preview-content th,
.preview-content td {
- border: 1px solid var(--border);
- padding: 0.5em 1em;
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--border);
+ border-right: 1px solid var(--border);
text-align: left;
}
-.preview-content th {
- background-color: var(--bg);
- font-weight: 600;
+.preview-content th:last-child,
+.preview-content td:last-child {
+ border-right: none;
}
-/* Share Dropdown Styles */
+.preview-content tr:last-child td {
+ border-bottom: none;
+}
+
+.preview-content th {
+ background-color: var(--surface-hover);
+ font-weight: 600;
+ color: var(--primary-text);
+}
+
+.preview-content tr:hover td {
+ background-color: var(--surface-hover);
+}
+
+/* Lists */
+.preview-content ul,
+.preview-content ol {
+ padding-left: 1.75em;
+ margin-bottom: 1.1em;
+}
+
+.preview-content li {
+ margin-bottom: 0.35em;
+}
+
+/* Task Lists */
+.preview-content ul.task-list,
+.preview-content li.task-list-item {
+ list-style: none;
+ padding-left: 0;
+}
+
+.preview-content input[type="checkbox"] {
+ margin-right: 0.5em;
+ accent-color: var(--accent);
+ vertical-align: middle;
+ cursor: default;
+}
+
+/* Horizontal Rule */
+.preview-content hr {
+ border: none;
+ height: 1px;
+ background-color: var(--border);
+ margin: 2em 0;
+}
+
+/* Images */
+.preview-content img {
+ max-width: 100%;
+ border-radius: 8px;
+ box-shadow: var(--shadow-md);
+ margin: 1em 0;
+}
+
+/* Export Container & Dropdown */
+.export-container {
+ position: relative;
+ display: inline-block;
+}
+
+.export-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: 230px;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ animation: dropdownFadeIn 0.15s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.export-menu-item {
+ width: 100%;
+ text-align: left;
+ background: none;
+ border: none;
+ padding: 0.6rem 0.8rem;
+ border-radius: 6px;
+ color: var(--primary-text);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ display: flex;
+ align-items: center;
+}
+
+.export-menu-item:hover {
+ background-color: var(--surface-hover);
+ color: var(--accent);
+}
+
+/* Share Dropdown */
.share-container {
position: relative;
display: inline-block;
@@ -309,36 +742,18 @@
right: 0;
margin-top: 0.5rem;
background-color: var(--surface);
- border: 2px solid var(--accent);
- border-radius: 8px;
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
- padding: 1rem;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ box-shadow: var(--shadow-lg);
+ padding: 1.25rem;
width: 320px;
z-index: 1000;
- animation: dropdownFadeIn 0.15s ease-out;
+ animation: dropdownFadeIn 0.15s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes dropdownFadeIn {
- from {
- opacity: 0;
- transform: translateY(-8px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.share-dropdown::before {
- content: '';
- position: absolute;
- top: -8px;
- right: 1rem;
- width: 0;
- height: 0;
- border-left: 8px solid transparent;
- border-right: 8px solid transparent;
- border-bottom: 8px solid var(--accent);
+ from { opacity: 0; transform: translateY(-8px); }
+ to { opacity: 1; transform: translateY(0); }
}
.share-item {
@@ -351,9 +766,9 @@
.share-item label {
display: block;
- margin-bottom: 0.5rem;
+ margin-bottom: 0.4rem;
font-weight: 600;
- font-size: 0.875rem;
+ font-size: 0.75rem;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -361,187 +776,142 @@
.link-group {
display: flex;
- gap: 0.5rem;
+ gap: 0.4rem;
align-items: center;
}
.link-field {
flex: 1;
- padding: 0.5rem 0.75rem;
+ padding: 0.45rem 0.65rem;
border: 1px solid var(--border);
- border-radius: 4px;
+ border-radius: 6px;
background-color: var(--bg);
color: var(--primary-text);
- font-family: 'JetBrains Mono', 'SF Mono', 'Monaco', monospace;
+ font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
- font-weight: 500;
}
.link-field:focus {
outline: none;
border-color: var(--accent);
- box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2);
}
.btn-copy-small {
- padding: 0.5rem;
+ padding: 0.45rem 0.65rem;
background-color: var(--accent);
- color: white;
+ color: #ffffff;
border: none;
- border-radius: 4px;
+ border-radius: 6px;
cursor: pointer;
- transition: all 0.2s ease;
- font-size: 0.875rem;
- display: flex;
- align-items: center;
- justify-content: center;
- min-width: 2rem;
+ transition: all 0.15s ease;
+ font-size: 0.8125rem;
}
.btn-copy-small:hover {
background-color: var(--accent-hover);
- transform: scale(1.05);
}
-.loading-small {
- text-align: center;
- color: var(--accent);
- padding: 1rem;
- font-size: 0.875rem;
- font-weight: 600;
-}
-
-.loading-small::after {
- content: '';
- display: inline-block;
- width: 16px;
- height: 16px;
- border: 2px solid var(--border);
- border-radius: 50%;
- border-top-color: var(--accent);
- animation: spin 1s ease-in-out infinite;
- margin-left: 0.5rem;
- vertical-align: middle;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-/* Splitter Styles */
-.splitter {
- width: 8px;
- background-color: var(--border);
- cursor: col-resize;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- flex-shrink: 0;
- transition: all 0.15s ease;
-}
-
-.splitter:hover {
- background-color: var(--accent);
- width: 10px;
-}
-
-.splitter.dragging {
- background-color: var(--accent);
- width: 12px;
- box-shadow: 0 0 8px rgba(0, 122, 204, 0.3);
-}
-
-.splitter-handle {
- width: 2px;
- height: 40px;
- background-color: var(--secondary-text);
- border-radius: 1px;
- transition: all 0.15s ease;
-}
-
-.splitter:hover .splitter-handle {
- background-color: white;
- width: 3px;
- height: 50px;
-}
-
-.splitter.dragging .splitter-handle {
- background-color: white;
- width: 4px;
- height: 60px;
-}
-
-/* Mobile Responsive - Place at end to override desktop styles */
+/* Mobile Responsive */
@media screen and (max-width: 768px) {
+ .top-bar {
+ padding: 0.4rem 0.6rem;
+ flex-wrap: wrap;
+ }
+
+ .top-bar-center {
+ display: none;
+ }
+
+ .topbar-tags {
+ display: none;
+ }
+
+ .doc-title-input {
+ max-width: 140px;
+ font-size: 0.875rem;
+ }
+
+ .btn-text {
+ display: none;
+ }
+
+ .format-toolbar {
+ padding: 0.25rem 0.5rem;
+ }
+
+ .preview-content {
+ padding: 1.25rem;
+ }
+
+ .markdown-editor {
+ padding: 1.25rem;
+ font-size: 16px;
+ }
+
.editor-layout.split-view {
flex-direction: column !important;
}
-
- .split-view .editor-pane {
- border-right: none;
- border-bottom: 1px solid var(--border);
- flex: 1;
- }
-
- .preview-pane {
- flex: 1;
- }
-
+
.splitter {
display: none;
}
-
- .top-bar {
- padding: 0.5rem;
- height: auto;
- min-height: 50px;
- flex-wrap: wrap;
- gap: 0.5rem;
- }
-
- .top-bar-left,
- .top-bar-center,
- .top-bar-right {
- gap: 0.5rem;
- }
-
- .btn-toggle,
- .btn-share,
- .btn-save,
- .btn-new {
- font-size: 14px;
- padding: 8px 12px;
- min-height: 44px;
- white-space: nowrap;
- }
-
- .btn-save {
- min-width: 44px;
- padding: 8px;
- }
-
- .markdown-editor {
- font-size: 16px; /* Prevent iOS zoom */
- padding: 1rem;
- -webkit-overflow-scrolling: touch;
- }
-
- .preview-content {
- padding: 1rem;
- -webkit-overflow-scrolling: touch;
- }
-
- /* Share dropdown mobile adjustments */
- .share-dropdown {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 90vw;
- max-width: 320px;
- z-index: 1000;
+
+ .preview-pane {
+ border-left: none;
+ border-top: 1px solid var(--border);
}
}
+/* ========================================================= */
+/* PRINT / PDF EXPORT MEDIA QUERY STYLES */
+/* ========================================================= */
+@media print {
+ body, html {
+ background-color: #ffffff !important;
+ color: #000000 !important;
+ height: auto !important;
+ overflow: visible !important;
+ }
+ .top-bar,
+ .format-toolbar,
+ .splitter,
+ .editor-pane,
+ .notes-drawer,
+ .drawer-backdrop,
+ .code-copy-btn {
+ display: none !important;
+ }
+
+ .editor-container {
+ height: auto !important;
+ display: block !important;
+ background: none !important;
+ }
+
+ .editor-layout {
+ display: block !important;
+ }
+
+ .preview-pane {
+ border: none !important;
+ background: none !important;
+ display: block !important;
+ }
+
+ .preview-content {
+ max-width: 100% !important;
+ padding: 0 !important;
+ overflow: visible !important;
+ }
+
+ .preview-content pre {
+ border: 1px solid #ddd !important;
+ background-color: #f8f9fa !important;
+ page-break-inside: avoid;
+ }
+
+ .preview-content table {
+ page-break-inside: avoid;
+ }
+}
diff --git a/Notemark/Components/Pages/Home.razor b/Notemark/Components/Pages/Home.razor
new file mode 100644
index 0000000..5ec2266
--- /dev/null
+++ b/Notemark/Components/Pages/Home.razor
@@ -0,0 +1,355 @@
+@page "/"
+@rendermode InteractiveServer
+@using Notemark.Models
+@using Notemark.Services
+@inject IDocumentService DocumentService
+@inject ISessionService SessionService
+@inject IHttpContextAccessor HttpContextAccessor
+@inject IJSRuntime JSRuntime
+@inject NavigationManager Navigation
+
+Notemark - Home
+
+
+
+
+
+
+
+ @if (isLoading)
+ {
+
+ }
+ else
+ {
+
+
+
+
📄
+
+
New Blank Note
+
Start writing markdown from scratch
+
+
+
+ @if (localRecentDoc != null)
+ {
+ OpenNote(localRecentDoc.Id)">
+
⚡
+
+
Your Last Note on this PC
+
@localRecentDoc.Title
+
@localRecentDoc.Snippet
+
Last edited @FormatDate(localRecentDoc.LastModifiedAt)
+
+
+ }
+
+
+
+ @if (activeTags.Any())
+ {
+
+ SelectTag(string.Empty)">
+ All Pinned & Tagged (@displayedNotes.Count)
+
+ @foreach (var tag in activeTags)
+ {
+ var count = displayedNotes.Count(d => d.Tags.Any(t => t.Equals(tag, StringComparison.OrdinalIgnoreCase)));
+ SelectTag(tag)">
+ #@tag (@count)
+
+ }
+
+ }
+
+
+ @if (flaggedDocs.Any() && string.IsNullOrEmpty(selectedTag) && string.IsNullOrEmpty(searchQuery))
+ {
+
+
+
+ @foreach (var doc in flaggedDocs)
+ {
+
OpenNote(doc.Id)">
+
+
@doc.Snippet
+ @if (doc.Tags.Any())
+ {
+
+ @foreach (var tag in doc.Tags)
+ {
+ SelectTag(tag)" @onclick:stopPropagation="true">#@tag
+ }
+
+ }
+
+
+ }
+
+
+ }
+
+
+
+
+
+ @if (!filteredNotes.Any())
+ {
+
+
🏷️
+
No flagged or tagged notes yet
+
Flag notes with 🚩 in the editor or add #tags in your text to see them on your home screen!
+ @if (localRecentDoc != null)
+ {
+
OpenNote(localRecentDoc.Id)">
+ Continue in Last Note: @localRecentDoc.Title
+
+ }
+ else
+ {
+
+ Create Your First Note
+
+ }
+
+ }
+ else
+ {
+
+ @foreach (var doc in filteredNotes)
+ {
+
OpenNote(doc.Id)">
+
+
@doc.Snippet
+ @if (doc.Tags.Any())
+ {
+
+ @foreach (var tag in doc.Tags)
+ {
+ SelectTag(tag)" @onclick:stopPropagation="true">#@tag
+ }
+
+ }
+
+
+ }
+
+ }
+
+ }
+
+
+
+@code {
+ private bool isLoading = true;
+ private string searchQuery = "";
+ private string selectedTag = "";
+ private DocumentMetadata? localRecentDoc;
+ private List displayedNotes = new();
+ private List activeTags = new();
+
+ private List flaggedDocs => displayedNotes.Where(d => d.IsFlagged).OrderByDescending(d => d.LastModifiedAt).ToList();
+
+ private List filteredNotes
+ {
+ get
+ {
+ // If flaggedDocs section is shown above, only show tagged (non-flagged) or all matching filter
+ var list = string.IsNullOrEmpty(selectedTag) && string.IsNullOrEmpty(searchQuery) && flaggedDocs.Any()
+ ? displayedNotes.Where(d => !d.IsFlagged)
+ : displayedNotes.AsEnumerable();
+
+ if (!string.IsNullOrEmpty(selectedTag))
+ {
+ list = list.Where(d => d.Tags.Any(t => t.Equals(selectedTag, StringComparison.OrdinalIgnoreCase)));
+ }
+
+ if (!string.IsNullOrWhiteSpace(searchQuery))
+ {
+ var q = searchQuery.Trim().ToLowerInvariant();
+ list = list.Where(d => d.Title.ToLowerInvariant().Contains(q) ||
+ d.Snippet.ToLowerInvariant().Contains(q) ||
+ d.Tags.Any(t => t.ToLowerInvariant().Contains(q)));
+ }
+
+ return list.OrderByDescending(d => d.LastModifiedAt).ToList();
+ }
+ }
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadDashboard();
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ try
+ {
+ // Read last note ID worked on this specific browser/PC
+ var lastDocId = await JSRuntime.InvokeAsync("localStorage.getItem", "notemark-last-doc-id");
+ if (!string.IsNullOrEmpty(lastDocId))
+ {
+ var doc = await DocumentService.GetDocumentAsync(lastDocId);
+ if (doc != null && doc.Id != localRecentDoc?.Id)
+ {
+ localRecentDoc = doc;
+ StateHasChanged();
+ }
+ }
+ }
+ catch { }
+ }
+ }
+
+ private async Task LoadDashboard()
+ {
+ isLoading = true;
+ try
+ {
+ var allDocs = await DocumentService.GetAllDocumentsAsync();
+
+ // Only include notes that are Flagged OR Tagged (having at least 1 tag)
+ displayedNotes = allDocs.Where(d => d.IsFlagged || d.Tags.Any())
+ .OrderByDescending(d => d.LastModifiedAt)
+ .ToList();
+
+ activeTags = displayedNotes.SelectMany(d => d.Tags)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(t => t)
+ .ToList();
+
+ // Try to find device's current session document as fallback
+ var httpContext = HttpContextAccessor.HttpContext;
+ if (httpContext != null)
+ {
+ var currentSessionId = SessionService.GetOrCreateSessionId(httpContext);
+ localRecentDoc = await DocumentService.GetDocumentAsync(currentSessionId);
+ }
+
+ if (localRecentDoc == null)
+ {
+ localRecentDoc = await DocumentService.GetMostRecentDocumentAsync();
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Error loading dashboard: {ex.Message}");
+ }
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private void SelectTag(string tag)
+ {
+ selectedTag = selectedTag == tag ? string.Empty : tag;
+ }
+
+ private void CreateNewNote()
+ {
+ var newId = Guid.NewGuid().ToString();
+ Navigation.NavigateTo($"/editor/{newId}?isNew=true");
+ }
+
+ private void OpenNote(string id)
+ {
+ Navigation.NavigateTo($"/editor/{id}");
+ }
+
+ private async Task ToggleFlag(string id)
+ {
+ await DocumentService.ToggleFlagAsync(id);
+ await LoadDashboard();
+ }
+
+ private async Task DeleteNote(string id)
+ {
+ await DocumentService.DeleteDocumentAsync(id);
+ await LoadDashboard();
+ }
+
+ private string FormatDate(DateTime date)
+ {
+ var diff = DateTime.UtcNow - date;
+ if (diff.TotalMinutes < 1) return "Just now";
+ if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m ago";
+ if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h ago";
+ if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}d ago";
+ return date.ToLocalTime().ToString("MMM dd, yyyy");
+ }
+}
diff --git a/Notemark/Components/Pages/Home.razor.css b/Notemark/Components/Pages/Home.razor.css
new file mode 100644
index 0000000..5b3dd21
--- /dev/null
+++ b/Notemark/Components/Pages/Home.razor.css
@@ -0,0 +1,456 @@
+/* Home Dashboard Styles */
+.home-container {
+ min-height: 100vh;
+ background-color: var(--bg);
+ color: var(--primary-text);
+ display: flex;
+ flex-direction: column;
+}
+
+/* Topbar */
+.home-topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem 2rem;
+ background-color: var(--surface);
+ border-bottom: 1px solid var(--border);
+ gap: 1.5rem;
+ position: sticky;
+ top: 0;
+ z-index: 50;
+}
+
+.topbar-brand {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+}
+
+.brand-logo {
+ color: var(--accent);
+ display: flex;
+ align-items: center;
+}
+
+.brand-name {
+ font-size: 1.25rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ color: var(--primary-text);
+}
+
+.topbar-search {
+ flex: 1;
+ max-width: 520px;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background-color: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ color: var(--secondary-text);
+ transition: all 0.15s ease;
+}
+
+.topbar-search:focus-within {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 2px var(--accent-light);
+}
+
+.topbar-search input {
+ flex: 1;
+ background: none;
+ border: none;
+ outline: none;
+ color: var(--primary-text);
+ font-size: 0.875rem;
+}
+
+.btn-clear-search {
+ background: none;
+ border: none;
+ color: var(--secondary-text);
+ cursor: pointer;
+ padding: 0;
+}
+
+.topbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.btn-primary-action {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.45rem;
+ padding: 0.5rem 1rem;
+ background-color: var(--accent);
+ color: #ffffff;
+ border: none;
+ border-radius: 8px;
+ font-size: 0.875rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow: var(--shadow-sm);
+}
+
+.btn-primary-action:hover {
+ background-color: var(--accent-hover);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+/* Main Layout */
+.home-main {
+ flex: 1;
+ max-width: 1100px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 2.5rem 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 2.5rem;
+ box-sizing: border-box;
+}
+
+/* Loading */
+.home-loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 5rem 0;
+ gap: 1rem;
+ color: var(--secondary-text);
+}
+
+.spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid var(--border);
+ border-top-color: var(--accent);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Hero Quick Actions */
+.quick-hero {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ gap: 1.25rem;
+}
+
+.hero-card {
+ padding: 1.5rem;
+ background-color: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ display: flex;
+ gap: 1.25rem;
+ align-items: center;
+ box-shadow: var(--shadow-sm);
+}
+
+.hero-card:hover {
+ border-color: var(--accent);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.hero-icon {
+ font-size: 2rem;
+ padding: 0.75rem;
+ background-color: var(--bg);
+ border-radius: 10px;
+ border: 1px solid var(--border);
+}
+
+.hero-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.hero-tag {
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--accent);
+ margin-bottom: 0.2rem;
+}
+
+.hero-info h3 {
+ margin: 0 0 0.35rem 0;
+ font-size: 1.125rem;
+ font-weight: 700;
+ color: var(--primary-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.hero-info p {
+ margin: 0;
+ font-size: 0.84rem;
+ color: var(--secondary-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.hero-time {
+ display: inline-block;
+ font-size: 0.75rem;
+ color: var(--secondary-text);
+ margin-top: 0.4rem;
+}
+
+/* Tag Filter Chips */
+.home-tags-filter {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.tag-chip {
+ padding: 0.35rem 0.8rem;
+ background-color: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ color: var(--secondary-text);
+ font-size: 0.8125rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.tag-chip:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.tag-chip.active {
+ background-color: var(--accent);
+ border-color: var(--accent);
+ color: #ffffff;
+}
+
+/* Sections */
+.notes-section {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.section-header h2 {
+ font-size: 1.25rem;
+ font-weight: 700;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.section-count {
+ font-size: 0.8125rem;
+ font-weight: 700;
+ padding: 0.15rem 0.55rem;
+ background-color: var(--surface-hover);
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ color: var(--secondary-text);
+}
+
+/* Notes Grid */
+.notes-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 1.25rem;
+}
+
+.note-card {
+ padding: 1.25rem;
+ background-color: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
+ box-shadow: var(--shadow-sm);
+}
+
+.note-card:hover {
+ border-color: var(--accent);
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.flagged-card {
+ border-color: rgba(239, 68, 68, 0.3);
+ background: linear-gradient(to bottom right, var(--surface), rgba(239, 68, 68, 0.03));
+}
+
+.card-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.card-title {
+ margin: 0;
+ font-size: 1.05rem;
+ font-weight: 700;
+ color: var(--primary-text);
+ line-height: 1.35;
+ word-break: break-word;
+}
+
+.card-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ flex-shrink: 0;
+}
+
+.btn-card-flag,
+.btn-card-delete {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0.25rem;
+ font-size: 0.95rem;
+ border-radius: 6px;
+ opacity: 0.7;
+ transition: all 0.15s ease;
+}
+
+.btn-card-flag:hover,
+.btn-card-delete:hover {
+ opacity: 1;
+ background-color: var(--surface-hover);
+ transform: scale(1.1);
+}
+
+.btn-card-flag.active {
+ opacity: 1;
+}
+
+.card-snippet {
+ margin: 0;
+ font-size: 0.875rem;
+ color: var(--secondary-text);
+ line-height: 1.5;
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+}
+
+.card-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ margin-top: 0.2rem;
+}
+
+.tag-pill {
+ font-size: 0.75rem;
+ font-weight: 600;
+ padding: 0.15rem 0.5rem;
+ background-color: var(--accent-light);
+ color: var(--accent);
+ border-radius: 999px;
+ transition: all 0.15s ease;
+}
+
+.tag-pill:hover {
+ background-color: var(--accent);
+ color: #ffffff;
+}
+
+.card-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 0.75rem;
+ color: var(--secondary-text);
+ margin-top: auto;
+ padding-top: 0.5rem;
+ border-top: 1px solid var(--border-subtle);
+}
+
+/* Empty Dashboard */
+.empty-dashboard {
+ text-align: center;
+ padding: 4rem 2rem;
+ background-color: var(--surface);
+ border: 1px dashed var(--border);
+ border-radius: 12px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+}
+
+.empty-icon {
+ font-size: 3rem;
+}
+
+.empty-dashboard h3 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 700;
+}
+
+.empty-dashboard p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: var(--secondary-text);
+ max-width: 400px;
+}
+
+/* Responsive */
+@media screen and (max-width: 768px) {
+ .home-topbar {
+ padding: 0.75rem 1rem;
+ flex-wrap: wrap;
+ }
+
+ .topbar-search {
+ order: 3;
+ width: 100%;
+ max-width: 100%;
+ }
+
+ .home-main {
+ padding: 1.5rem 1rem;
+ gap: 1.5rem;
+ }
+
+ .quick-hero {
+ grid-template-columns: 1fr;
+ }
+
+ .notes-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/Notemark/Components/Pages/SharedEditor.razor b/Notemark/Components/Pages/SharedEditor.razor
index 8d0cecf..e67808c 100644
--- a/Notemark/Components/Pages/SharedEditor.razor
+++ b/Notemark/Components/Pages/SharedEditor.razor
@@ -1,5 +1,6 @@
@page "/e/{token}"
@rendermode InteractiveServer
+@implements IDisposable
@using Notemark.Services
@inject IShareService ShareService
@inject ISessionService SessionService
@@ -227,6 +228,18 @@ else
Navigation.NavigateTo("/");
}
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (showPreview && !string.IsNullOrEmpty(renderedHtml))
+ {
+ try
+ {
+ await JSRuntime.InvokeVoidAsync("notemarkEditor.highlightPreview", ".preview-content");
+ }
+ catch { }
+ }
+ }
+
public void Dispose()
{
autoSaveTimer?.Dispose();
diff --git a/Notemark/Components/Pages/SharedPreview.razor b/Notemark/Components/Pages/SharedPreview.razor
index 7ab8b05..311b9aa 100644
--- a/Notemark/Components/Pages/SharedPreview.razor
+++ b/Notemark/Components/Pages/SharedPreview.razor
@@ -1,10 +1,12 @@
@page "/s/{token}"
@rendermode InteractiveServer
+@implements IDisposable
@using Notemark.Services
@inject IShareService ShareService
@inject ISessionService SessionService
@inject IMarkdownService MarkdownService
@inject NavigationManager Navigation
+@inject IJSRuntime JSRuntime
Notemark - Shared Document
@@ -37,6 +39,17 @@ else