Add shared document editor and preview components
Introduced SharedEditor.razor and SharedPreview.razor for editing and viewing shared documents via tokenized links. Includes auto-save, markdown preview, permission checks, and dedicated responsive styles for both editable and view-only modes. Enables secure sharing of documents with user-friendly interfaces.
This commit is contained in:
@@ -0,0 +1,233 @@
|
|||||||
|
@page "/e/{token}"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@using Notemark.Services
|
||||||
|
@inject IShareService ShareService
|
||||||
|
@inject ISessionService SessionService
|
||||||
|
@inject IMarkdownService MarkdownService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
|
<PageTitle>Notemark - Edit Shared Document</PageTitle>
|
||||||
|
|
||||||
|
@if (isLoading)
|
||||||
|
{
|
||||||
|
<div class="share-loading">
|
||||||
|
<div class="loading-content">
|
||||||
|
<h2>Loading shared document...</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (errorMessage != null)
|
||||||
|
{
|
||||||
|
<div class="share-error">
|
||||||
|
<div class="error-content">
|
||||||
|
<h2>⚠️ Unable to Load Document</h2>
|
||||||
|
<p>@errorMessage</p>
|
||||||
|
<button class="btn-home" @onclick="GoHome">Create New Document</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="editor-container">
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<div class="top-bar">
|
||||||
|
<div class="top-bar-left">
|
||||||
|
<h2 class="shared-title">Notemark</h2>
|
||||||
|
<span class="share-badge editable">Shared - Editable</span>
|
||||||
|
<button class="btn-toggle @(showPreview ? "active" : "")"
|
||||||
|
@onclick="TogglePreview">
|
||||||
|
@(showPreview ? "Hide Preview" : "Show Preview")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-bar-center">
|
||||||
|
<span class="save-status @saveStatus.ToLower()">@saveStatus</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-bar-right">
|
||||||
|
<button class="btn-view" @onclick="GoToPreview">👁️ View</button>
|
||||||
|
<button class="btn-home" @onclick="GoHome">New Doc</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor Layout -->
|
||||||
|
<div class="editor-layout @(showPreview ? "split-view" : "full-view")">
|
||||||
|
<!-- Editor Pane -->
|
||||||
|
<div class="editor-pane">
|
||||||
|
<textarea class="markdown-editor"
|
||||||
|
@bind="markdownContent"
|
||||||
|
@oninput="OnContentChanged"
|
||||||
|
@onkeydown="OnKeyDown"
|
||||||
|
placeholder="Start editing this shared document..."
|
||||||
|
spellcheck="true">
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview Pane -->
|
||||||
|
@if (showPreview)
|
||||||
|
{
|
||||||
|
<div class="preview-pane">
|
||||||
|
<div class="preview-content">
|
||||||
|
@((MarkupString)renderedHtml)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string Token { get; set; } = "";
|
||||||
|
|
||||||
|
private bool isLoading = true;
|
||||||
|
private string? errorMessage = null;
|
||||||
|
private ShareToken? shareToken = null;
|
||||||
|
private string markdownContent = "";
|
||||||
|
private string renderedHtml = "";
|
||||||
|
private bool showPreview = false;
|
||||||
|
private string saveStatus = "Saved";
|
||||||
|
|
||||||
|
// Auto-save timer
|
||||||
|
private Timer? autoSaveTimer;
|
||||||
|
private bool hasUnsavedChanges = false;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadSharedDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadSharedDocument()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
// Validate token
|
||||||
|
if (string.IsNullOrEmpty(Token))
|
||||||
|
{
|
||||||
|
errorMessage = "Invalid share link.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get share token info
|
||||||
|
shareToken = await ShareService.GetShareTokenAsync(Token);
|
||||||
|
if (shareToken == null)
|
||||||
|
{
|
||||||
|
errorMessage = "Share link not found or has expired.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is editable and valid
|
||||||
|
if (shareToken.Mode != "editable")
|
||||||
|
{
|
||||||
|
errorMessage = "This share link is view-only. You cannot edit this document.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await ShareService.IsValidTokenAsync(Token))
|
||||||
|
{
|
||||||
|
errorMessage = "This share link has been revoked or has expired.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the document content
|
||||||
|
markdownContent = await SessionService.LoadSessionContentAsync(shareToken.SessionId);
|
||||||
|
|
||||||
|
// Render initial preview if needed
|
||||||
|
if (showPreview)
|
||||||
|
{
|
||||||
|
renderedHtml = MarkdownService.RenderToHtml(markdownContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorMessage = "An error occurred while loading the document.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isLoading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && shareToken != null)
|
||||||
|
{
|
||||||
|
await SaveContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveContent()
|
||||||
|
{
|
||||||
|
if (shareToken == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SessionService.SaveSessionContentAsync(shareToken.SessionId, markdownContent);
|
||||||
|
hasUnsavedChanges = false;
|
||||||
|
saveStatus = "Saved";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
saveStatus = "Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TogglePreview()
|
||||||
|
{
|
||||||
|
showPreview = !showPreview;
|
||||||
|
|
||||||
|
if (showPreview)
|
||||||
|
{
|
||||||
|
renderedHtml = MarkdownService.RenderToHtml(markdownContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoToPreview()
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"/s/{Token}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoHome()
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
autoSaveTimer?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/* Shared Editor Styles - Extends main editor styles */
|
||||||
|
|
||||||
|
.shared-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-badge.editable {
|
||||||
|
background-color: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view {
|
||||||
|
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;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view:hover {
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile adjustments for shared editor */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.top-bar-left {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-badge {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-toggle,
|
||||||
|
.btn-view,
|
||||||
|
.btn-home {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
@page "/s/{token}"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@using Notemark.Services
|
||||||
|
@inject IShareService ShareService
|
||||||
|
@inject ISessionService SessionService
|
||||||
|
@inject IMarkdownService MarkdownService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
<PageTitle>Notemark - Shared Document</PageTitle>
|
||||||
|
|
||||||
|
@if (isLoading)
|
||||||
|
{
|
||||||
|
<div class="share-loading">
|
||||||
|
<div class="loading-content">
|
||||||
|
<h2>Loading shared document...</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (errorMessage != null)
|
||||||
|
{
|
||||||
|
<div class="share-error">
|
||||||
|
<div class="error-content">
|
||||||
|
<h2>⚠️ Unable to Load Document</h2>
|
||||||
|
<p>@errorMessage</p>
|
||||||
|
<button class="btn-home" @onclick="GoHome">Create New Document</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="shared-container">
|
||||||
|
<!-- Header Bar -->
|
||||||
|
<div class="shared-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h1>Notemark</h1>
|
||||||
|
<span class="share-badge readonly">View Only</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
@if (shareToken?.Mode == "editable")
|
||||||
|
{
|
||||||
|
<button class="btn-edit" @onclick="GoToEditor">✏️ Edit</button>
|
||||||
|
}
|
||||||
|
<button class="btn-home" @onclick="GoHome">Create New</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="shared-content">
|
||||||
|
@if (string.IsNullOrEmpty(markdownContent))
|
||||||
|
{
|
||||||
|
<div class="empty-content">
|
||||||
|
<h3>Empty Document</h3>
|
||||||
|
<p>This document doesn't have any content yet.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="preview-content">
|
||||||
|
@((MarkupString)renderedHtml)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string Token { get; set; } = "";
|
||||||
|
|
||||||
|
private bool isLoading = true;
|
||||||
|
private string? errorMessage = null;
|
||||||
|
private ShareToken? shareToken = null;
|
||||||
|
private string markdownContent = "";
|
||||||
|
private string renderedHtml = "";
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadSharedDocument();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadSharedDocument()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
// Validate token
|
||||||
|
if (string.IsNullOrEmpty(Token))
|
||||||
|
{
|
||||||
|
errorMessage = "Invalid share link.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get share token info
|
||||||
|
shareToken = await ShareService.GetShareTokenAsync(Token);
|
||||||
|
if (shareToken == null)
|
||||||
|
{
|
||||||
|
errorMessage = "Share link not found or has expired.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is still valid
|
||||||
|
if (!await ShareService.IsValidTokenAsync(Token))
|
||||||
|
{
|
||||||
|
errorMessage = "This share link has been revoked or has expired.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the document content
|
||||||
|
markdownContent = await SessionService.LoadSessionContentAsync(shareToken.SessionId);
|
||||||
|
|
||||||
|
// Render to HTML
|
||||||
|
renderedHtml = MarkdownService.RenderToHtml(markdownContent);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorMessage = "An error occurred while loading the document.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isLoading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoToEditor()
|
||||||
|
{
|
||||||
|
if (shareToken?.Mode == "editable")
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo($"/e/{Token}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoHome()
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
/* Shared Document Styles */
|
||||||
|
|
||||||
|
.share-loading,
|
||||||
|
.share-error {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content,
|
||||||
|
.error-content {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--secondary-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-content h2 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-content p {
|
||||||
|
margin: 0 0 2rem 0;
|
||||||
|
color: var(--secondary-text);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-container {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.shared-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
background-color: var(--surface);
|
||||||
|
border-bottom: 2px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-badge.readonly {
|
||||||
|
background-color: var(--secondary-text);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-badge.editable {
|
||||||
|
background-color: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn-edit,
|
||||||
|
.btn-home {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--surface);
|
||||||
|
color: var(--primary-text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit:hover {
|
||||||
|
background-color: var(--accent-hover);
|
||||||
|
border-color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-home:hover {
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Area */
|
||||||
|
.shared-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-content {
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--secondary-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-content h3 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown Preview Styles (same as editor) */
|
||||||
|
.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: 2.5em; margin-top: 0; }
|
||||||
|
.preview-content h2 { font-size: 2em; }
|
||||||
|
.preview-content h3 { font-size: 1.5em; }
|
||||||
|
|
||||||
|
.preview-content p {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
color: var(--primary-text);
|
||||||
|
font-size: 1.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.9em;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content pre {
|
||||||
|
background-color: var(--bg);
|
||||||
|
padding: 1.5em;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
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: 1.5em;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
color: var(--secondary-text);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content ul,
|
||||||
|
.preview-content ol {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content li {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content th,
|
||||||
|
.preview-content td {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content th {
|
||||||
|
background-color: var(--bg);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 2px solid var(--border);
|
||||||
|
margin: 3em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.shared-header {
|
||||||
|
padding: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-edit,
|
||||||
|
.btn-home {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user