diff --git a/Notemark/Components/Layout/MainLayout.razor.css b/Notemark/Components/Layout/MainLayout.razor.css
index 38d1f25..888471b 100644
--- a/Notemark/Components/Layout/MainLayout.razor.css
+++ b/Notemark/Components/Layout/MainLayout.razor.css
@@ -1,98 +1,97 @@
-.page {
- position: relative;
- display: flex;
- flex-direction: column;
+/* Notemark CSS Variables for Light/Dark Theme */
+:root {
+ /* Light Mode (Default) */
+ --bg: #F9FAFB;
+ --surface: #FFFFFF;
+ --border: #E5E7EB;
+ --primary-text: #111827;
+ --secondary-text: #6B7280;
+ --accent: #4F46E5;
+ --accent-hover: #4338CA;
+ --success: #10B981;
+ --error: #F43F5E;
}
-main {
- flex: 1;
+[data-theme="dark"] {
+ /* Dark Mode */
+ --bg: #0B0F19;
+ --surface: #111827;
+ --border: #1F2937;
+ --primary-text: #F9FAFB;
+ --secondary-text: #9CA3AF;
+ --accent: #22D3EE;
+ --accent-hover: #06B6D4;
+ --success: #4ADE80;
+ --error: #FB7185;
}
-.sidebar {
- background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
+/* Auto-detect system preference */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #0B0F19;
+ --surface: #111827;
+ --border: #1F2937;
+ --primary-text: #F9FAFB;
+ --secondary-text: #9CA3AF;
+ --accent: #22D3EE;
+ --accent-hover: #06B6D4;
+ --success: #4ADE80;
+ --error: #FB7185;
+ }
}
-.top-row {
- background-color: #f7f7f7;
- border-bottom: 1px solid #d6d5d5;
- justify-content: flex-end;
- height: 3.5rem;
- display: flex;
- align-items: center;
+/* Override system dark mode when explicitly set to light */
+[data-theme="light"] {
+ --bg: #F9FAFB;
+ --surface: #FFFFFF;
+ --border: #E5E7EB;
+ --primary-text: #111827;
+ --secondary-text: #6B7280;
+ --accent: #4F46E5;
+ --accent-hover: #4338CA;
+ --success: #10B981;
+ --error: #F43F5E;
}
- .top-row ::deep a, .top-row ::deep .btn-link {
- white-space: nowrap;
- margin-left: 1.5rem;
- text-decoration: none;
- }
-
- .top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
- text-decoration: underline;
- }
-
- .top-row ::deep a:first-child {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
-@media (max-width: 640.98px) {
- .top-row {
- justify-content: space-between;
- }
-
- .top-row ::deep a, .top-row ::deep .btn-link {
- margin-left: 0;
- }
+/* Notemark App Layout */
+.notemark-app {
+ height: 100vh;
+ width: 100vw;
+ background-color: var(--bg);
+ color: var(--primary-text);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
+ overflow: hidden;
}
-@media (min-width: 641px) {
- .page {
- flex-direction: row;
- }
+/* Reset default margins and padding */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
- .sidebar {
- width: 250px;
- height: 100vh;
- position: sticky;
- top: 0;
- }
-
- .top-row {
- position: sticky;
- top: 0;
- z-index: 1;
- }
-
- .top-row.auth ::deep a:first-child {
- flex: 1;
- text-align: right;
- width: 0;
- }
-
- .top-row, article {
- padding-left: 2rem !important;
- padding-right: 1.5rem !important;
- }
+body {
+ background-color: var(--bg);
+ color: var(--primary-text);
}
#blazor-error-ui {
- color-scheme: light only;
- background: lightyellow;
- bottom: 0;
- box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
- box-sizing: border-box;
- display: none;
- left: 0;
- padding: 0.6rem 1.25rem 0.7rem 1.25rem;
- position: fixed;
- width: 100%;
- z-index: 1000;
+ color-scheme: light only;
+ background: lightyellow;
+ bottom: 0;
+ box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ display: none;
+ left: 0;
+ padding: 0.6rem 1.25rem 0.7rem 1.25rem;
+ position: fixed;
+ width: 100%;
+ z-index: 1000;
}
- #blazor-error-ui .dismiss {
- cursor: pointer;
- position: absolute;
- right: 0.75rem;
- top: 0.5rem;
- }
+#blazor-error-ui .dismiss {
+ cursor: pointer;
+ position: absolute;
+ right: 0.75rem;
+ top: 0.5rem;
+}
diff --git a/Notemark/Components/Layout/NavMenu.razor b/Notemark/Components/Layout/NavMenu.razor
index 3edd244..0f160cb 100644
--- a/Notemark/Components/Layout/NavMenu.razor
+++ b/Notemark/Components/Layout/NavMenu.razor
@@ -1,30 +1,2 @@
-
-
-
-
-
+@* Notemark uses a minimalist layout with no navigation menu *@
diff --git a/Notemark/Components/Pages/Counter.razor b/Notemark/Components/Pages/Counter.razor
deleted file mode 100644
index 1a4f8e7..0000000
--- a/Notemark/Components/Pages/Counter.razor
+++ /dev/null
@@ -1,19 +0,0 @@
-@page "/counter"
-@rendermode InteractiveServer
-
-
Counter
-
-
Counter
-
-
Current count: @currentCount
-
-
-
-@code {
- private int currentCount = 0;
-
- private void IncrementCount()
- {
- currentCount++;
- }
-}
diff --git a/Notemark/Components/Pages/Editor.razor b/Notemark/Components/Pages/Editor.razor
new file mode 100644
index 0000000..92b967a
--- /dev/null
+++ b/Notemark/Components/Pages/Editor.razor
@@ -0,0 +1,245 @@
+@page "/"
+@rendermode InteractiveServer
+@using Notemark.Services
+@inject ISessionService SessionService
+@inject IMarkdownService MarkdownService
+@inject IShareService ShareService
+@inject IJSRuntime JSRuntime
+@inject IHttpContextAccessor HttpContextAccessor
+
+
Notemark
+
+
+
+
+
+
+
+
+
+ @saveStatus
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (showPreview)
+ {
+
+
+ @((MarkupString)renderedHtml)
+
+
+ }
+
+
+
+ @if (showShareModal)
+ {
+
+
+
+
+
+ @if (isGeneratingLinks)
+ {
+
Generating share links...
+ }
+ else if (!string.IsNullOrEmpty(readonlyLink))
+ {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+
+
+ }
+
+
+@code {
+ private string sessionId = "";
+ private string markdownContent = "";
+ private string renderedHtml = "";
+ private bool showPreview = false;
+ private string saveStatus = "Saved";
+
+ // Share modal state
+ private bool showShareModal = false;
+ private bool isGeneratingLinks = false;
+ private string readonlyLink = "";
+ private string editableLink = "";
+
+ // 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)
+ {
+ sessionId = SessionService.GetOrCreateSessionId(httpContext);
+
+ // Load existing content
+ markdownContent = await SessionService.LoadSessionContentAsync(sessionId);
+
+ // Render initial preview if needed
+ if (showPreview)
+ {
+ renderedHtml = MarkdownService.RenderToHtml(markdownContent);
+ }
+ }
+ }
+
+ private async Task OnContentChanged(ChangeEventArgs e)
+ {
+ markdownContent = e.Value?.ToString() ?? "";
+ hasUnsavedChanges = true;
+ saveStatus = "Saving...";
+
+ // Update preview if visible
+ if (showPreview)
+ {
+ renderedHtml = MarkdownService.RenderToHtml(markdownContent);
+ }
+
+ // Debounce auto-save
+ autoSaveTimer?.Dispose();
+ autoSaveTimer = new Timer(async _ => await AutoSave(), null, 1000, Timeout.Infinite);
+
+ await InvokeAsync(StateHasChanged);
+ }
+
+ private async Task OnKeyDown(KeyboardEventArgs e)
+ {
+ // Ctrl+S or Cmd+S for manual save
+ if ((e.CtrlKey || e.MetaKey) && e.Key == "s")
+ {
+ await SaveContent();
+ }
+ }
+
+ private async Task AutoSave()
+ {
+ if (hasUnsavedChanges)
+ {
+ await SaveContent();
+ }
+ }
+
+ private async Task SaveContent()
+ {
+ if (string.IsNullOrEmpty(sessionId))
+ return;
+
+ try
+ {
+ await SessionService.SaveSessionContentAsync(sessionId, markdownContent);
+ hasUnsavedChanges = false;
+ saveStatus = "Saved";
+ }
+ catch
+ {
+ saveStatus = "Error";
+ }
+
+ await InvokeAsync(StateHasChanged);
+ }
+
+ private void TogglePreview()
+ {
+ showPreview = !showPreview;
+
+ if (showPreview)
+ {
+ renderedHtml = MarkdownService.RenderToHtml(markdownContent);
+ }
+ }
+
+ private async Task ShowShareModal()
+ {
+ showShareModal = true;
+ isGeneratingLinks = true;
+ readonlyLink = "";
+ editableLink = "";
+
+ await InvokeAsync(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}";
+ }
+ }
+ catch
+ {
+ readonlyLink = "Error generating link";
+ editableLink = "Error generating link";
+ }
+ finally
+ {
+ isGeneratingLinks = false;
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+
+ private void HideShareModal()
+ {
+ showShareModal = false;
+ }
+
+ private async Task CopyToClipboard(string text)
+ {
+ await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
+ }
+
+ public void Dispose()
+ {
+ autoSaveTimer?.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/Notemark/Components/Pages/Editor.razor.css b/Notemark/Components/Pages/Editor.razor.css
new file mode 100644
index 0000000..937055e
--- /dev/null
+++ b/Notemark/Components/Pages/Editor.razor.css
@@ -0,0 +1,345 @@
+/* Notemark Editor Styles */
+
+.editor-container {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background-color: var(--bg);
+}
+
+/* Top Bar */
+.top-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem 1rem;
+ background-color: var(--surface);
+ border-bottom: 1px solid var(--border);
+ height: 60px;
+ flex-shrink: 0;
+}
+
+.top-bar-left,
+.top-bar-center,
+.top-bar-right {
+ display: flex;
+ align-items: center;
+}
+
+.top-bar-center {
+ flex: 1;
+ justify-content: center;
+}
+
+/* Buttons */
+.btn-toggle,
+.btn-share,
+.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-toggle:hover,
+.btn-share:hover,
+.btn-copy:hover {
+ background-color: var(--accent);
+ color: white;
+ border-color: var(--accent);
+}
+
+.btn-toggle.active {
+ background-color: var(--accent);
+ color: white;
+ border-color: var(--accent);
+}
+
+.btn-close {
+ padding: 0.25rem 0.5rem;
+ font-size: 1.25rem;
+ line-height: 1;
+ background: none;
+ border: none;
+}
+
+.btn-close:hover {
+ background-color: var(--error);
+ color: white;
+}
+
+/* Save Status */
+.save-status {
+ font-size: 0.875rem;
+ font-weight: 500;
+ padding: 0.25rem 0.75rem;
+ border-radius: 4px;
+}
+
+.save-status.saved {
+ color: var(--success);
+}
+
+.save-status.saving {
+ color: var(--secondary-text);
+}
+
+.save-status.error {
+ color: var(--error);
+}
+
+/* Editor Layout */
+.editor-layout {
+ flex: 1;
+ display: flex;
+ min-height: 0;
+}
+
+.editor-layout.full-view {
+ flex-direction: column;
+}
+
+.editor-layout.split-view {
+ flex-direction: row;
+}
+
+/* Editor Pane */
+.editor-pane {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.split-view .editor-pane {
+ border-right: 1px solid var(--border);
+}
+
+.markdown-editor {
+ flex: 1;
+ width: 100%;
+ 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;
+ background-color: var(--bg);
+ color: var(--primary-text);
+}
+
+.markdown-editor::placeholder {
+ color: var(--secondary-text);
+}
+
+.markdown-editor:focus {
+ outline: none;
+}
+
+/* Preview Pane */
+.preview-pane {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background-color: var(--surface);
+ min-width: 0;
+}
+
+.preview-content {
+ flex: 1;
+ padding: 1.5rem;
+ overflow-y: auto;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
+ line-height: 1.6;
+}
+
+/* Markdown Preview Styles */
+.preview-content h1,
+.preview-content h2,
+.preview-content h3,
+.preview-content h4,
+.preview-content h5,
+.preview-content h6 {
+ color: var(--primary-text);
+ margin-top: 2em;
+ margin-bottom: 1em;
+ font-weight: 600;
+}
+
+.preview-content h1 { font-size: 2em; }
+.preview-content h2 { font-size: 1.5em; }
+.preview-content h3 { font-size: 1.25em; }
+
+.preview-content p {
+ margin-bottom: 1em;
+ color: var(--primary-text);
+}
+
+.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;
+ color: var(--accent);
+}
+
+.preview-content pre {
+ background-color: var(--bg);
+ padding: 1em;
+ border-radius: 6px;
+ overflow-x: auto;
+ margin-bottom: 1em;
+ border: 1px solid var(--border);
+}
+
+.preview-content pre code {
+ background: none;
+ padding: 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 table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 1em;
+}
+
+.preview-content th,
+.preview-content td {
+ border: 1px solid var(--border);
+ padding: 0.5em 1em;
+ text-align: left;
+}
+
+.preview-content th {
+ background-color: var(--bg);
+ font-weight: 600;
+}
+
+/* Modal Styles */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal-content {
+ background-color: var(--surface);
+ border-radius: 8px;
+ width: 90%;
+ max-width: 500px;
+ max-height: 80vh;
+ overflow: hidden;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.modal-header h3 {
+ margin: 0;
+ font-size: 1.25rem;
+ color: var(--primary-text);
+}
+
+.modal-body {
+ padding: 1.5rem;
+}
+
+.share-link-group {
+ margin-bottom: 1.5rem;
+}
+
+.share-link-group:last-child {
+ margin-bottom: 0;
+}
+
+.share-link-group label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ color: var(--primary-text);
+}
+
+.link-input-group {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.link-input {
+ flex: 1;
+ padding: 0.75rem;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background-color: var(--bg);
+ color: var(--primary-text);
+ font-family: monospace;
+ font-size: 0.875rem;
+}
+
+.loading {
+ text-align: center;
+ color: var(--secondary-text);
+ padding: 2rem;
+}
+
+/* Mobile Responsive */
+@media (max-width: 768px) {
+ .editor-layout.split-view {
+ flex-direction: column;
+ }
+
+ .split-view .editor-pane {
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .top-bar {
+ padding: 0.5rem;
+ }
+
+ .btn-toggle,
+ .btn-share {
+ font-size: 0.75rem;
+ padding: 0.375rem 0.75rem;
+ }
+
+ .markdown-editor,
+ .preview-content {
+ padding: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/Notemark/Components/Pages/Home.razor b/Notemark/Components/Pages/Home.razor
deleted file mode 100644
index 9001e0b..0000000
--- a/Notemark/Components/Pages/Home.razor
+++ /dev/null
@@ -1,7 +0,0 @@
-@page "/"
-
-
Home
-
-
Hello, world!
-
-Welcome to your new app.
diff --git a/Notemark/Components/Pages/Weather.razor b/Notemark/Components/Pages/Weather.razor
deleted file mode 100644
index f437e5e..0000000
--- a/Notemark/Components/Pages/Weather.razor
+++ /dev/null
@@ -1,64 +0,0 @@
-@page "/weather"
-@attribute [StreamRendering]
-
-
Weather
-
-
Weather
-
-
This component demonstrates showing data.
-
-@if (forecasts == null)
-{
-
Loading...
-}
-else
-{
-
-
-
- | Date |
- Temp. (C) |
- Temp. (F) |
- Summary |
-
-
-
- @foreach (var forecast in forecasts)
- {
-
- | @forecast.Date.ToShortDateString() |
- @forecast.TemperatureC |
- @forecast.TemperatureF |
- @forecast.Summary |
-
- }
-
-
-}
-
-@code {
- private WeatherForecast[]? forecasts;
-
- protected override async Task OnInitializedAsync()
- {
- // Simulate asynchronous loading to demonstrate streaming rendering
- await Task.Delay(500);
-
- var startDate = DateOnly.FromDateTime(DateTime.Now);
- var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
- forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
- {
- Date = startDate.AddDays(index),
- TemperatureC = Random.Shared.Next(-20, 55),
- Summary = summaries[Random.Shared.Next(summaries.Length)]
- }).ToArray();
- }
-
- private class WeatherForecast
- {
- public DateOnly Date { get; set; }
- public int TemperatureC { get; set; }
- public string? Summary { get; set; }
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
- }
-}
diff --git a/Notemark/Notemark.csproj b/Notemark/Notemark.csproj
index b5f6af0..1fd7612 100644
--- a/Notemark/Notemark.csproj
+++ b/Notemark/Notemark.csproj
@@ -7,4 +7,9 @@
true
+
+
+
+
+
diff --git a/Notemark/Program.cs b/Notemark/Program.cs
index 3854a39..0adda16 100644
--- a/Notemark/Program.cs
+++ b/Notemark/Program.cs
@@ -1,4 +1,5 @@
using Notemark.Components;
+using Notemark.Services;
var builder = WebApplication.CreateBuilder(args);
@@ -6,6 +7,14 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
+// Add HTTP context accessor for session management
+builder.Services.AddHttpContextAccessor();
+
+// Register Notemark services
+builder.Services.AddSingleton
();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
var app = builder.Build();
// Configure the HTTP request pipeline.
@@ -18,6 +27,13 @@ if (!app.Environment.IsDevelopment())
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();
+// Enable cookies for session management
+app.UseCookiePolicy(new CookiePolicyOptions
+{
+ MinimumSameSitePolicy = SameSiteMode.Lax,
+ Secure = CookieSecurePolicy.SameAsRequest
+});
+
app.UseAntiforgery();
app.MapStaticAssets();
diff --git a/Notemark/Services/IMarkdownService.cs b/Notemark/Services/IMarkdownService.cs
new file mode 100644
index 0000000..a26ebdf
--- /dev/null
+++ b/Notemark/Services/IMarkdownService.cs
@@ -0,0 +1,6 @@
+namespace Notemark.Services;
+
+public interface IMarkdownService
+{
+ string RenderToHtml(string markdown);
+}
\ No newline at end of file
diff --git a/Notemark/Services/ISessionService.cs b/Notemark/Services/ISessionService.cs
new file mode 100644
index 0000000..4a6b6a8
--- /dev/null
+++ b/Notemark/Services/ISessionService.cs
@@ -0,0 +1,9 @@
+namespace Notemark.Services;
+
+public interface ISessionService
+{
+ string GetOrCreateSessionId(HttpContext httpContext);
+ Task LoadSessionContentAsync(string sessionId);
+ Task SaveSessionContentAsync(string sessionId, string content);
+ Task SessionExistsAsync(string sessionId);
+}
\ No newline at end of file
diff --git a/Notemark/Services/IShareService.cs b/Notemark/Services/IShareService.cs
new file mode 100644
index 0000000..72f69ae
--- /dev/null
+++ b/Notemark/Services/IShareService.cs
@@ -0,0 +1,17 @@
+namespace Notemark.Services;
+
+public record ShareToken(
+ string SessionId,
+ string Mode, // "readonly" or "editable"
+ DateTime CreatedAt,
+ DateTime? ExpiresAt = null,
+ DateTime? RevokedAt = null
+);
+
+public interface IShareService
+{
+ Task CreateShareTokenAsync(string sessionId, string mode);
+ Task GetShareTokenAsync(string token);
+ Task RevokeShareTokenAsync(string token);
+ Task IsValidTokenAsync(string token);
+}
\ No newline at end of file
diff --git a/Notemark/Services/MarkdownService.cs b/Notemark/Services/MarkdownService.cs
new file mode 100644
index 0000000..dff370b
--- /dev/null
+++ b/Notemark/Services/MarkdownService.cs
@@ -0,0 +1,38 @@
+using Markdig;
+using Ganss.Xss;
+
+namespace Notemark.Services;
+
+public class MarkdownService : IMarkdownService
+{
+ private readonly MarkdownPipeline _pipeline;
+ private readonly HtmlSanitizer _sanitizer;
+
+ public MarkdownService()
+ {
+ // Configure Markdig pipeline with GitHub-style features
+ _pipeline = new MarkdownPipelineBuilder()
+ .UseAdvancedExtensions() // Tables, footnotes, task lists, etc.
+ .UseSoftlineBreakAsHardlineBreak() // GitHub-style line breaks
+ .Build();
+
+ // Configure HTML sanitizer for security
+ _sanitizer = new HtmlSanitizer();
+ _sanitizer.AllowedTags.Add("details");
+ _sanitizer.AllowedTags.Add("summary");
+ _sanitizer.AllowedAttributes.Add("class");
+ _sanitizer.AllowedAttributes.Add("id");
+ }
+
+ public string RenderToHtml(string markdown)
+ {
+ if (string.IsNullOrEmpty(markdown))
+ return string.Empty;
+
+ // Convert markdown to HTML
+ var html = Markdown.ToHtml(markdown, _pipeline);
+
+ // Sanitize HTML to prevent XSS attacks
+ return _sanitizer.Sanitize(html);
+ }
+}
\ No newline at end of file
diff --git a/Notemark/Services/SessionService.cs b/Notemark/Services/SessionService.cs
new file mode 100644
index 0000000..f159fa9
--- /dev/null
+++ b/Notemark/Services/SessionService.cs
@@ -0,0 +1,69 @@
+namespace Notemark.Services;
+
+public class SessionService : ISessionService
+{
+ private readonly string _dataPath;
+ private const string SessionCookieName = "mk_session";
+
+ public SessionService(IWebHostEnvironment environment)
+ {
+ _dataPath = Path.Combine(environment.ContentRootPath, "data", "sessions");
+ Directory.CreateDirectory(_dataPath);
+ }
+
+ public string GetOrCreateSessionId(HttpContext httpContext)
+ {
+ // Try to get existing session from cookie
+ if (httpContext.Request.Cookies.TryGetValue(SessionCookieName, out var existingSessionId)
+ && Guid.TryParse(existingSessionId, out _))
+ {
+ return existingSessionId;
+ }
+
+ // Create new session
+ var newSessionId = Guid.NewGuid().ToString();
+
+ // Set cookie with 30-day expiration
+ var cookieOptions = new CookieOptions
+ {
+ Expires = DateTimeOffset.UtcNow.AddDays(30),
+ HttpOnly = true,
+ Secure = true,
+ SameSite = SameSiteMode.Lax
+ };
+
+ httpContext.Response.Cookies.Append(SessionCookieName, newSessionId, cookieOptions);
+
+ return newSessionId;
+ }
+
+ public async Task LoadSessionContentAsync(string sessionId)
+ {
+ var filePath = GetSessionFilePath(sessionId);
+
+ if (!File.Exists(filePath))
+ return string.Empty;
+
+ return await File.ReadAllTextAsync(filePath);
+ }
+
+ public async Task SaveSessionContentAsync(string sessionId, string content)
+ {
+ var filePath = GetSessionFilePath(sessionId);
+ await File.WriteAllTextAsync(filePath, content ?? string.Empty);
+ }
+
+ public async Task SessionExistsAsync(string sessionId)
+ {
+ var filePath = GetSessionFilePath(sessionId);
+ return File.Exists(filePath);
+ }
+
+ private string GetSessionFilePath(string sessionId)
+ {
+ if (!Guid.TryParse(sessionId, out _))
+ throw new ArgumentException("Invalid session ID format", nameof(sessionId));
+
+ return Path.Combine(_dataPath, $"{sessionId}.md");
+ }
+}
\ No newline at end of file
diff --git a/Notemark/Services/ShareService.cs b/Notemark/Services/ShareService.cs
new file mode 100644
index 0000000..29447a9
--- /dev/null
+++ b/Notemark/Services/ShareService.cs
@@ -0,0 +1,107 @@
+using System.Text.Json;
+
+namespace Notemark.Services;
+
+public class ShareService : IShareService
+{
+ private readonly string _dataPath;
+ private readonly JsonSerializerOptions _jsonOptions;
+
+ public ShareService(IWebHostEnvironment environment)
+ {
+ _dataPath = Path.Combine(environment.ContentRootPath, "data", "shares");
+ Directory.CreateDirectory(_dataPath);
+
+ _jsonOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = true
+ };
+ }
+
+ public async Task CreateShareTokenAsync(string sessionId, string mode)
+ {
+ if (!Guid.TryParse(sessionId, out _))
+ throw new ArgumentException("Invalid session ID format", nameof(sessionId));
+
+ if (mode != "readonly" && mode != "editable")
+ throw new ArgumentException("Mode must be 'readonly' or 'editable'", nameof(mode));
+
+ var token = GenerateToken();
+ var shareToken = new ShareToken(
+ SessionId: sessionId,
+ Mode: mode,
+ CreatedAt: DateTime.UtcNow
+ );
+
+ var filePath = GetTokenFilePath(token);
+ var json = JsonSerializer.Serialize(shareToken, _jsonOptions);
+ await File.WriteAllTextAsync(filePath, json);
+
+ return token;
+ }
+
+ public async Task GetShareTokenAsync(string token)
+ {
+ var filePath = GetTokenFilePath(token);
+
+ if (!File.Exists(filePath))
+ return null;
+
+ try
+ {
+ var json = await File.ReadAllTextAsync(filePath);
+ return JsonSerializer.Deserialize(json, _jsonOptions);
+ }
+ catch (JsonException)
+ {
+ return null;
+ }
+ }
+
+ public async Task RevokeShareTokenAsync(string token)
+ {
+ var shareToken = await GetShareTokenAsync(token);
+ if (shareToken == null)
+ return;
+
+ var revokedToken = shareToken with { RevokedAt = DateTime.UtcNow };
+ var filePath = GetTokenFilePath(token);
+ var json = JsonSerializer.Serialize(revokedToken, _jsonOptions);
+ await File.WriteAllTextAsync(filePath, json);
+ }
+
+ public async Task IsValidTokenAsync(string token)
+ {
+ var shareToken = await GetShareTokenAsync(token);
+
+ if (shareToken == null)
+ return false;
+
+ // Check if revoked
+ if (shareToken.RevokedAt.HasValue)
+ return false;
+
+ // Check if expired
+ if (shareToken.ExpiresAt.HasValue && shareToken.ExpiresAt.Value < DateTime.UtcNow)
+ return false;
+
+ return true;
+ }
+
+ private string GenerateToken()
+ {
+ // Generate a URL-safe random token
+ return Convert.ToBase64String(Guid.NewGuid().ToByteArray())
+ .Replace('+', '-')
+ .Replace('/', '_')
+ .Replace("=", "");
+ }
+
+ private string GetTokenFilePath(string token)
+ {
+ // Sanitize token for filename
+ var sanitizedToken = string.Concat(token.Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_'));
+ return Path.Combine(_dataPath, $"{sanitizedToken}.json");
+ }
+}
\ No newline at end of file
diff --git a/Notemark/data/sessions/b5acffbe-706e-405e-b006-3415508a59a7.md b/Notemark/data/sessions/b5acffbe-706e-405e-b006-3415508a59a7.md
new file mode 100644
index 0000000..992cd91
--- /dev/null
+++ b/Notemark/data/sessions/b5acffbe-706e-405e-b006-3415508a59a7.md
@@ -0,0 +1,22 @@
+# Notemark — .NET 10 Blazor Server Specification (Markdown Editor + Preview + Share)
+
+## 0) Goal
+Notemark is a minimalist Markdown editor web app built with **.NET 10 Blazor Server**.
+
+- First visit generates a **GUID sessionId**
+- Markdown is saved as a **file on disk** (no database)
+- Fullscreen editor, optional preview split view
+- Share links:
+ - **Readonly** → preview only
+ - **Editable** → preview only + Edit button
+
+---
+
+## 1) Tech Stack
+- .NET 10
+- Blazor Server
+- Markdown rendering: Markdig
+- HTML sanitization: Ganss.XSS (HtmlSanitizer)
+- Persistence: File system only
+
+
diff --git a/Notemark/data/shares/6XbclTsBI0SD_OZxxZtX5g.json b/Notemark/data/shares/6XbclTsBI0SD_OZxxZtX5g.json
new file mode 100644
index 0000000..84d98c8
--- /dev/null
+++ b/Notemark/data/shares/6XbclTsBI0SD_OZxxZtX5g.json
@@ -0,0 +1,7 @@
+{
+ "sessionId": "b5acffbe-706e-405e-b006-3415508a59a7",
+ "mode": "editable",
+ "createdAt": "2026-01-30T15:19:41.7110595Z",
+ "expiresAt": null,
+ "revokedAt": null
+}
\ No newline at end of file
diff --git a/Notemark/data/shares/P3Xs1x-1E02PsvO90XVXuA.json b/Notemark/data/shares/P3Xs1x-1E02PsvO90XVXuA.json
new file mode 100644
index 0000000..b62e897
--- /dev/null
+++ b/Notemark/data/shares/P3Xs1x-1E02PsvO90XVXuA.json
@@ -0,0 +1,7 @@
+{
+ "sessionId": "b5acffbe-706e-405e-b006-3415508a59a7",
+ "mode": "readonly",
+ "createdAt": "2026-01-30T15:19:41.7093218Z",
+ "expiresAt": null,
+ "revokedAt": null
+}
\ No newline at end of file
diff --git a/Notemark/data/shares/ll5J-tEyOkeKGkI6ZmDeow.json b/Notemark/data/shares/ll5J-tEyOkeKGkI6ZmDeow.json
new file mode 100644
index 0000000..43e0b4f
--- /dev/null
+++ b/Notemark/data/shares/ll5J-tEyOkeKGkI6ZmDeow.json
@@ -0,0 +1,7 @@
+{
+ "sessionId": "b5acffbe-706e-405e-b006-3415508a59a7",
+ "mode": "readonly",
+ "createdAt": "2026-01-30T15:19:24.7974267Z",
+ "expiresAt": null,
+ "revokedAt": null
+}
\ No newline at end of file
diff --git a/Notemark/data/shares/uN4HzT9eB0axkHy66QRIfQ.json b/Notemark/data/shares/uN4HzT9eB0axkHy66QRIfQ.json
new file mode 100644
index 0000000..1bfde9f
--- /dev/null
+++ b/Notemark/data/shares/uN4HzT9eB0axkHy66QRIfQ.json
@@ -0,0 +1,7 @@
+{
+ "sessionId": "b5acffbe-706e-405e-b006-3415508a59a7",
+ "mode": "editable",
+ "createdAt": "2026-01-30T15:19:24.8076694Z",
+ "expiresAt": null,
+ "revokedAt": null
+}
\ No newline at end of file