feat(editor): align selection popup and all editor control elements styling with Reader (#81)
## Summary of Changes This pull request aligns all major interactive editor control elements in the Milkdown Crepe editor with the premium `SelectionAiPanel` / `IntelligenceToolbar` glassmorphism design. ### Changes: 1. **Selection Bubble Menu Unification:** Relocated the selection menu overrides from `Creator.razor.css` to `app.css` to resolve scoping bugs. Themed to match the Reader's selection popup 1:1. 2. **Editor Controls Theming:** Themed table cell drag handles, table actions popups, line insertion handles & add buttons, Notion-style paragraph drag handles, and slash commands menus with glassmorphic backgrounds, perimeter borders, hover transitions, and active accent states. 3. **Visibility Lifecycle Fixes:** Excluded `.cell-handle` and `.milkdown-block-handle` from explicit `display: none !important` rules when hidden, preserving their dimensions for correct JS positioning calculations and preventing handles from jumping/sliding. 4. **Table Margin Clipping Fix:** Set `overflow: visible !important` on `.tableWrapper` to allow table controls to draw cleanly into the editor canvas's padding zone without boundary clipping. Resolves #82. --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #81 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #81.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
namespace NexusReader.Application.Abstractions.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for sanitizing raw input text (e.g. Markdown/HTML) to protect against XSS injection.
|
||||
/// Intended to have a Singleton lifetime.
|
||||
/// </summary>
|
||||
public interface ISanitizerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Sanitizes the input string and returns a clean, safe version.
|
||||
/// </summary>
|
||||
string Sanitize(string input);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace NexusReader.Application.Abstractions.Services;
|
||||
|
||||
/// <summary>
|
||||
/// General file storage service interface for handling media uploads.
|
||||
/// Intended to have a Scoped lifetime.
|
||||
/// </summary>
|
||||
public interface IStorageService
|
||||
{
|
||||
/// <summary>
|
||||
/// Uploads a file stream and returns its public URL/path.
|
||||
/// </summary>
|
||||
Task<string> UploadFileAsync(Stream fileStream, string fileName, string contentType);
|
||||
|
||||
/// <summary>
|
||||
/// Uploads file bytes and returns its public URL/path.
|
||||
/// </summary>
|
||||
Task<string> UploadFileAsync(byte[] fileBytes, string fileName, string contentType);
|
||||
}
|
||||
@@ -18,6 +18,9 @@ namespace NexusReader.Application.Common;
|
||||
[JsonSerializable(typeof(List<NexusReader.Application.Queries.Recommendations.RecommendationDto>))]
|
||||
[JsonSerializable(typeof(NexusReader.Application.DTOs.User.UpdateThemeRequest))]
|
||||
[JsonSerializable(typeof(NexusReader.Domain.Enums.ThemeMode))]
|
||||
[JsonSerializable(typeof(NexusReader.Application.DTOs.Media.ValidateChapterRequest))]
|
||||
[JsonSerializable(typeof(NexusReader.Application.DTOs.Media.ValidateChapterResponse))]
|
||||
[JsonSerializable(typeof(NexusReader.Application.DTOs.Media.UploadResultDto))]
|
||||
public partial class AppJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace NexusReader.Application.DTOs.Media;
|
||||
|
||||
// Note: These DTOs are registered in AppJsonContext.cs for JSON source generation.
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for chapter validation/sanitization.
|
||||
/// </summary>
|
||||
public record ValidateChapterRequest(string Content);
|
||||
|
||||
/// <summary>
|
||||
/// Response DTO containing sanitized chapter content.
|
||||
/// </summary>
|
||||
public record ValidateChapterResponse(string SanitizedContent);
|
||||
|
||||
/// <summary>
|
||||
/// Response DTO containing the uploaded media file URL.
|
||||
/// </summary>
|
||||
public record UploadResultDto(string Url);
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NexusReader.Infrastructure.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for configuring allowed tags, attributes, CSS properties, and schemes in HtmlSanitizerService.
|
||||
/// </summary>
|
||||
public class HtmlSanitizerSettings
|
||||
{
|
||||
public const string SectionName = "HtmlSanitizer";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of HTML tags that are allowed.
|
||||
/// If null or empty, the default allowed tags list is used.
|
||||
/// </summary>
|
||||
public List<string>? AllowedTags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of HTML attributes that are allowed.
|
||||
/// If null or empty, the default allowed attributes list is used.
|
||||
/// </summary>
|
||||
public List<string>? AllowedAttributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of CSS properties that are allowed.
|
||||
/// If null or empty, the default allowed CSS properties list is used.
|
||||
/// </summary>
|
||||
public List<string>? AllowedCssProperties { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of URI schemes that are allowed (e.g. "http", "https").
|
||||
/// If null or empty, the default allowed schemes list is used.
|
||||
/// </summary>
|
||||
public List<string>? AllowedSchemes { get; set; }
|
||||
}
|
||||
@@ -78,6 +78,7 @@ public static class DependencyInjection
|
||||
services.Configure<AiSettings>(configuration.GetSection(AiSettings.SectionName));
|
||||
services.Configure<StripeSettings>(configuration.GetSection(StripeSettings.SectionName));
|
||||
services.Configure<RagMonetizationOptions>(configuration.GetSection(RagMonetizationOptions.SectionName));
|
||||
services.Configure<HtmlSanitizerSettings>(configuration.GetSection(HtmlSanitizerSettings.SectionName));
|
||||
var aiSettings = configuration.GetSection(AiSettings.SectionName).Get<AiSettings>() ?? new AiSettings();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(aiSettings.ApiKey) || aiSettings.ApiKey == "PLACEHOLDER")
|
||||
@@ -124,6 +125,8 @@ public static class DependencyInjection
|
||||
// Fix #4: Scoped instead of Singleton — BookStorageService uses path resolution
|
||||
// that is environment-specific and incompatible with Singleton lifetime in MAUI.
|
||||
services.AddScoped<IBookStorageService, BookStorageService>();
|
||||
services.AddScoped<IStorageService, LocalStorageService>();
|
||||
services.AddSingleton<ISanitizerService, HtmlSanitizerService>();
|
||||
|
||||
// Fix #1: Ebook repository (scoped, matches AppDbContext lifetime)
|
||||
services.AddScoped<IEbookRepository, EbookRepository>();
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
<PackageReference Include="Polly" />
|
||||
<PackageReference Include="Polly.Extensions.Http" />
|
||||
<PackageReference Include="HtmlSanitizer" />
|
||||
<PackageReference Include="Qdrant.Client" />
|
||||
<PackageReference Include="Stripe.net" />
|
||||
<PackageReference Include="VersOne.Epub" />
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using Ganss.Xss;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using NexusReader.Infrastructure.Configuration;
|
||||
|
||||
namespace NexusReader.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Infrastructure implementation of ISanitizerService using the Ganss.Xss HtmlSanitizer library.
|
||||
/// </summary>
|
||||
public class HtmlSanitizerService : ISanitizerService
|
||||
{
|
||||
private readonly HtmlSanitizer _sanitizer;
|
||||
|
||||
public HtmlSanitizerService(IOptions<HtmlSanitizerSettings>? options = null)
|
||||
{
|
||||
_sanitizer = new HtmlSanitizer();
|
||||
|
||||
if (options?.Value != null)
|
||||
{
|
||||
var settings = options.Value;
|
||||
|
||||
if (settings.AllowedTags != null && settings.AllowedTags.Count > 0)
|
||||
{
|
||||
_sanitizer.AllowedTags.Clear();
|
||||
foreach (var tag in settings.AllowedTags)
|
||||
{
|
||||
_sanitizer.AllowedTags.Add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.AllowedAttributes != null && settings.AllowedAttributes.Count > 0)
|
||||
{
|
||||
_sanitizer.AllowedAttributes.Clear();
|
||||
foreach (var attr in settings.AllowedAttributes)
|
||||
{
|
||||
_sanitizer.AllowedAttributes.Add(attr);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.AllowedCssProperties != null && settings.AllowedCssProperties.Count > 0)
|
||||
{
|
||||
_sanitizer.AllowedCssProperties.Clear();
|
||||
foreach (var prop in settings.AllowedCssProperties)
|
||||
{
|
||||
_sanitizer.AllowedCssProperties.Add(prop);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.AllowedSchemes != null && settings.AllowedSchemes.Count > 0)
|
||||
{
|
||||
_sanitizer.AllowedSchemes.Clear();
|
||||
foreach (var scheme in settings.AllowedSchemes)
|
||||
{
|
||||
_sanitizer.AllowedSchemes.Add(scheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Sanitize(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return _sanitizer.Sanitize(input);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
|
||||
namespace NexusReader.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Infrastructure implementation of general storage utilizing local filesystem.
|
||||
/// Files are saved in wwwroot/uploads/media.
|
||||
/// </summary>
|
||||
public class LocalStorageService : IStorageService
|
||||
{
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
public LocalStorageService(IWebHostEnvironment environment)
|
||||
{
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public async Task<string> UploadFileAsync(byte[] fileBytes, string fileName, string contentType)
|
||||
{
|
||||
using var stream = new MemoryStream(fileBytes);
|
||||
return await UploadFileAsync(stream, fileName, contentType);
|
||||
}
|
||||
|
||||
public async Task<string> UploadFileAsync(Stream fileStream, string fileName, string contentType)
|
||||
{
|
||||
var mediaFolder = Path.Combine(_environment.WebRootPath, "uploads", "media");
|
||||
var resolvedMediaFolder = Path.GetFullPath(mediaFolder);
|
||||
var folderWithSeparator = resolvedMediaFolder.EndsWith(Path.DirectorySeparatorChar)
|
||||
? resolvedMediaFolder
|
||||
: resolvedMediaFolder + Path.DirectorySeparatorChar;
|
||||
|
||||
if (!Directory.Exists(resolvedMediaFolder))
|
||||
{
|
||||
Directory.CreateDirectory(resolvedMediaFolder);
|
||||
}
|
||||
|
||||
// Clean file name to prevent path traversal issues
|
||||
var safeFileName = Path.GetFileName(fileName);
|
||||
var uniqueFileName = $"{Guid.NewGuid()}_{safeFileName}";
|
||||
var filePath = Path.Combine(resolvedMediaFolder, uniqueFileName);
|
||||
|
||||
// Guard against path traversal
|
||||
var fullPath = Path.GetFullPath(filePath);
|
||||
if (!fullPath.StartsWith(folderWithSeparator, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Path traversal detected.");
|
||||
}
|
||||
|
||||
using (var outputStream = new FileStream(fullPath, FileMode.Create))
|
||||
{
|
||||
await fileStream.CopyToAsync(outputStream);
|
||||
}
|
||||
|
||||
// Return the public web-relative URL
|
||||
return $"/uploads/media/{uniqueFileName}";
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,11 @@
|
||||
<line x1="5" y1="12" x2="19" y2="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<polyline points="12 5 19 12 12 19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "edit":
|
||||
case "edit-2":
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4 9.5-9.5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "log-out":
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,154 @@
|
||||
@using Microsoft.JSInterop
|
||||
@implements IAsyncDisposable
|
||||
@inject IJSRuntime JS
|
||||
@inject HttpClient Http
|
||||
|
||||
<div class="markdown-editor-container" style="height: @Height; width: @Width;">
|
||||
<div id="@EditorId" class="milkdown-editor-wrapper"></div>
|
||||
@if (ShowFetchButton)
|
||||
{
|
||||
<div class="editor-actions">
|
||||
<button type="button" @onclick="FetchContentAsync" class="nexus-btn">
|
||||
Fetch Markdown Content
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private readonly string EditorId = $"milkdown-editor-{Guid.NewGuid():N}";
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private IJSObjectReference? _module;
|
||||
private DotNetObjectReference<MarkdownEditor>? _dotNetHelper;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowFetchButton { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public string InitialMarkdown { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> OnSave { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Height { get; set; } = "500px";
|
||||
|
||||
[Parameter]
|
||||
public string Width { get; set; } = "100%";
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_dotNetHelper = DotNetObjectReference.Create(this);
|
||||
try
|
||||
{
|
||||
// Import the isolated JavaScript module
|
||||
_module = await JS.InvokeAsync<IJSObjectReference>(
|
||||
"import",
|
||||
"./_content/NexusReader.UI.Shared/js/milkdownWrapper.js"
|
||||
);
|
||||
|
||||
// Call the initialization function in the wrapper
|
||||
await _module.InvokeVoidAsync("initEditor", EditorId, _dotNetHelper, InitialMarkdown);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the exception gracefully and do not crash the component
|
||||
Console.WriteLine($"[MarkdownEditor] Error initializing Milkdown editor: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task FetchContentAsync()
|
||||
{
|
||||
if (_module is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Retrieve the updated markdown from JS
|
||||
var markdown = await _module.InvokeAsync<string>("getMarkdownContent", EditorId);
|
||||
|
||||
if (OnSave.HasDelegate)
|
||||
{
|
||||
await OnSave.InvokeAsync(markdown);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[MarkdownEditor] Error fetching markdown content: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task<string> UploadImageFromJs(string filename, string contentType, byte[] fileBytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var content = new MultipartFormDataContent();
|
||||
var fileContent = new ByteArrayContent(fileBytes);
|
||||
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
|
||||
content.Add(fileContent, "file", filename);
|
||||
|
||||
var response = await Http.PostAsync("/api/media/upload", content, _cts.Token);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<NexusReader.Application.DTOs.Media.UploadResultDto>(
|
||||
NexusReader.Application.Common.AppJsonContext.Default.UploadResultDto, _cts.Token);
|
||||
return result?.Url ?? string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorMsg = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine($"[MarkdownEditor] Image upload failed: {response.StatusCode} - {errorMsg}");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[MarkdownEditor] Exception during image upload: {ex.Message}");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fail silently if cancellation token disposal fails
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_module is not null)
|
||||
{
|
||||
// Clean up the JS editor instance to prevent memory leaks
|
||||
await _module.InvokeVoidAsync("destroyEditor", EditorId);
|
||||
await _module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
// Fail silently during circuit disconnection
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Fail silently if JS runtime/module is already disposed
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log other unexpected errors
|
||||
Console.WriteLine($"[MarkdownEditor] Unexpected error during JS cleanup: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_dotNetHelper?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
.markdown-editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.milkdown-editor-wrapper {
|
||||
flex: 1;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-surface);
|
||||
overflow: auto;
|
||||
padding: 1.5rem;
|
||||
position: relative;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.milkdown-editor-wrapper:focus-within {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--accent-glow);
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 3. Bypassing Blazor CSS Isolation for Dynamic JS DOMs using ::deep */
|
||||
::deep .milkdown-editor-wrapper .crepe {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
::deep .milkdown-editor-wrapper .milkdown {
|
||||
background-color: var(--bg-surface) !important;
|
||||
color: var(--text-main) !important;
|
||||
font-family: var(--nexus-font-sans) !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
/* Map Crepe's internal variables to our design tokens */
|
||||
--crepe-color-background: var(--bg-surface);
|
||||
--crepe-color-on-background: var(--text-main);
|
||||
--crepe-color-surface: rgba(255, 255, 255, 0.03);
|
||||
--crepe-color-surface-low: rgba(255, 255, 255, 0.01);
|
||||
--crepe-color-primary: var(--accent);
|
||||
--crepe-color-outline: var(--border);
|
||||
}
|
||||
|
||||
::deep .milkdown-editor-wrapper .milkdown .editor {
|
||||
color: var(--text-main) !important;
|
||||
background: transparent !important;
|
||||
outline: none !important;
|
||||
padding: 0.5rem 0 !important;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* Style the buttons using variables from app.css */
|
||||
.nexus-btn {
|
||||
font-family: var(--nexus-font-sans);
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
background: var(--nexus-neon);
|
||||
color: #000000;
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9rem;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.nexus-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.1);
|
||||
box-shadow: 0 4px 15px var(--nexus-primary-glow);
|
||||
}
|
||||
|
||||
.nexus-btn:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@@ -95,6 +95,12 @@
|
||||
</div>
|
||||
<span class="nav-text">Koncentry</span>
|
||||
</NavLink>
|
||||
<NavLink class="nav-item" href="/creator" @onclick="CloseMobileMenu" title="Kreator" aria-label="Kreator">
|
||||
<div class="nav-icon">
|
||||
<NexusIcon Name="edit" Size="20" />
|
||||
</div>
|
||||
<span class="nav-text">Kreator</span>
|
||||
</NavLink>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
@page "/creator"
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>Kreator Treści (Zen Mode)</PageTitle>
|
||||
|
||||
<div class="creator-fullscreen-wrapper">
|
||||
<div class="creator-header">
|
||||
<h1>Kreator Treści</h1>
|
||||
<p class="subtitle">Zen publishing workspace mapping standard Markdown into clean visual blocks.</p>
|
||||
</div>
|
||||
|
||||
<div class="milkdown-premium-container creator-workspace-card" spellcheck="false">
|
||||
<div class="editor-growing-area">
|
||||
<MarkdownEditor @ref="_editorRef" InitialMarkdown="@_initialMarkdown" OnSave="HandleSave" ShowFetchButton="false" Height="100%" />
|
||||
</div>
|
||||
|
||||
<div class="creator-actions-bar">
|
||||
<button type="button" @onclick="TriggerFetchAsync" class="nexus-btn premium-fetch-btn btn-nexus-premium">
|
||||
<span>Fetch Markdown Content</span>
|
||||
<svg class="arrow-icon" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_savedMarkdown))
|
||||
{
|
||||
<div class="creator-preview-card">
|
||||
<div class="preview-header">
|
||||
<h3>Retrieved Markdown Preview</h3>
|
||||
</div>
|
||||
<div class="pre-wrapper">
|
||||
<pre class="code-preview-block"><code>@_savedMarkdown</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private MarkdownEditor? _editorRef;
|
||||
private string _savedMarkdown = string.Empty;
|
||||
|
||||
private readonly string _initialMarkdown = @"# Zen Mode Editor
|
||||
|
||||
Welcome to your dedicated workspace. This premium panel supports Notion-like WYSIWYG editing.
|
||||
|
||||
## Features:
|
||||
- **Zero Distraction**: Simple elevation and border framing.
|
||||
- **GFM Tables**: Consistent cell padding and hover striping.
|
||||
- **Clean Code Blocks**: Pre-rendered base64 font-loaded code-preview blocks.
|
||||
|
||||
| Option | Active | Value |
|
||||
| :--- | :---: | :--- |
|
||||
| Zen Mode | Yes | High Focus |
|
||||
| Responsive | Yes | 1200px Max |
|
||||
| Theme Sync | Yes | Automatic |
|
||||
|
||||
Start writing your masterpiece...";
|
||||
|
||||
private async Task TriggerFetchAsync()
|
||||
{
|
||||
if (_editorRef is not null)
|
||||
{
|
||||
await _editorRef.FetchContentAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSave(string markdown)
|
||||
{
|
||||
_savedMarkdown = markdown;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/* ==========================================================================
|
||||
Creator.razor.css - Isolated Styles for Zen Mode Creator Workspace
|
||||
========================================================================== */
|
||||
|
||||
/* 1. BOUNDARY & SCROLLING RE-ENGINEERING */
|
||||
|
||||
/* Strict flexbox layout context eliminating global browser scrollbars */
|
||||
.creator-fullscreen-wrapper {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important; /* Likwidujemy sztuczne ograniczenia szerokości */
|
||||
margin: 0;
|
||||
padding: 1.5rem; /* Elastyczny, bezpieczny margines od krawędzi bocznych menu i ekranu */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 4rem);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.creator-header {
|
||||
flex-shrink: 0;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.creator-header h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.creator-header .subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 2. Full Viewport Workspace Card stretching smoothly */
|
||||
.creator-workspace-card {
|
||||
background-color: var(--bg-surface) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-growing-area {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 3. Deep Cascading Overrides to target dynamic editor components */
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .markdown-editor-container {
|
||||
height: 100% !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
flex-grow: 1 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .milkdown-editor-wrapper {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
flex-grow: 1 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* Force crepe and milkdown inner wrappers to stretch */
|
||||
.creator-fullscreen-wrapper ::deep .crepe,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
flex-grow: 1 !important;
|
||||
overflow: hidden !important;
|
||||
background-color: transparent !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Pin the toolbar at the top */
|
||||
.creator-fullscreen-wrapper ::deep .crepe .toolbar,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown-menu,
|
||||
.creator-fullscreen-wrapper ::deep .crepe-menu-wrapper {
|
||||
flex-shrink: 0 !important;
|
||||
background-color: var(--bg-base) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 0.5rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .crepe .toolbar button:hover,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown-menu button:hover,
|
||||
.creator-fullscreen-wrapper ::deep .crepe-menu-wrapper button:hover,
|
||||
.creator-fullscreen-wrapper ::deep .crepe .toolbar .button:hover,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown-menu .button:hover {
|
||||
color: var(--accent) !important;
|
||||
background-color: rgba(16, 185, 129, 0.1) !important;
|
||||
border-radius: var(--radius-sm, 4px) !important;
|
||||
}
|
||||
|
||||
/* Relocate scrolling directly to ProseMirror editor layer and fix text clipping */
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror,
|
||||
.creator-fullscreen-wrapper ::deep .crepe .editor,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown .editor {
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
transform: none !important;
|
||||
flex-grow: 1 !important;
|
||||
overflow-y: auto !important;
|
||||
padding: 1.5rem !important;
|
||||
padding-right: 15px !important;
|
||||
background-color: var(--bg-surface) !important;
|
||||
color: var(--text-main) !important;
|
||||
font-size: 1.1rem !important;
|
||||
line-height: 1.7 !important;
|
||||
outline: none !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Custom narrow scrollbar mapped to var(--border) */
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror::-webkit-scrollbar,
|
||||
.creator-fullscreen-wrapper ::deep .crepe .editor::-webkit-scrollbar,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown .editor::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror::-webkit-scrollbar-track,
|
||||
.creator-fullscreen-wrapper ::deep .crepe .editor::-webkit-scrollbar-track,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown .editor::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror::-webkit-scrollbar-thumb,
|
||||
.creator-fullscreen-wrapper ::deep .crepe .editor::-webkit-scrollbar-thumb,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown .editor::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror::-webkit-scrollbar-thumb:hover,
|
||||
.creator-fullscreen-wrapper ::deep .crepe .editor::-webkit-scrollbar-thumb:hover,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown .editor::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Editorial Typography */
|
||||
.creator-fullscreen-wrapper ::deep .milkdown .editor h1,
|
||||
.creator-fullscreen-wrapper ::deep .crepe h1,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror h1 {
|
||||
margin-top: 1.8rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
font-size: 2.25rem !important;
|
||||
font-weight: 700 !important;
|
||||
color: var(--text-main) !important;
|
||||
line-height: 1.25 !important;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .milkdown .editor h2,
|
||||
.creator-fullscreen-wrapper ::deep .crepe h2,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror h2 {
|
||||
margin-top: 1.5rem !important;
|
||||
margin-bottom: 0.8rem !important;
|
||||
font-size: 1.6rem !important;
|
||||
font-weight: 700 !important;
|
||||
color: var(--text-main) !important;
|
||||
line-height: 1.3 !important;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .milkdown .editor h3,
|
||||
.creator-fullscreen-wrapper ::deep .crepe h3,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror h3 {
|
||||
margin-top: 1.3rem !important;
|
||||
margin-bottom: 0.7rem !important;
|
||||
font-size: 1.3rem !important;
|
||||
font-weight: 700 !important;
|
||||
color: var(--text-main) !important;
|
||||
line-height: 1.35 !important;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .milkdown .editor code,
|
||||
.creator-fullscreen-wrapper ::deep .crepe code,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror code {
|
||||
background-color: rgba(16, 185, 129, 0.1) !important;
|
||||
color: var(--accent) !important;
|
||||
padding: 0.2rem 0.4rem !important;
|
||||
border-radius: var(--radius-sm, 4px) !important;
|
||||
font-family: var(--nexus-font-mono) !important;
|
||||
font-size: 0.85em !important;
|
||||
}
|
||||
|
||||
/* Premium GFM Table Layouts */
|
||||
.creator-fullscreen-wrapper ::deep .milkdown-premium-container table,
|
||||
.creator-fullscreen-wrapper ::deep .crepe table,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown table,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror table {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
margin: 1.5rem 0 !important;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .milkdown-premium-container th,
|
||||
.creator-fullscreen-wrapper ::deep .crepe th,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown th,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror th,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown-premium-container td,
|
||||
.creator-fullscreen-wrapper ::deep .crepe td,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown td,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror td {
|
||||
padding: 14px 18px !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .milkdown-premium-container th,
|
||||
.creator-fullscreen-wrapper ::deep .crepe th,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown th,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror th {
|
||||
background-color: var(--bg-base) !important;
|
||||
color: var(--text-main) !important;
|
||||
font-weight: 700 !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.creator-fullscreen-wrapper ::deep .milkdown-premium-container td,
|
||||
.creator-fullscreen-wrapper ::deep .crepe td,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown td,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror td {
|
||||
color: var(--text-main) !important;
|
||||
}
|
||||
|
||||
/* Zebra row background tints (Dark Mode default) */
|
||||
.creator-fullscreen-wrapper ::deep .milkdown-premium-container tr:nth-child(even),
|
||||
.creator-fullscreen-wrapper ::deep .crepe tr:nth-child(even),
|
||||
.creator-fullscreen-wrapper ::deep .milkdown tr:nth-child(even),
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror tr:nth-child(even) {
|
||||
background-color: rgba(255, 255, 255, 0.01) !important;
|
||||
}
|
||||
|
||||
/* Zebra row background tints (Light Mode override) */
|
||||
.theme-light .creator-fullscreen-wrapper ::deep .milkdown-premium-container tr:nth-child(even),
|
||||
.theme-light .creator-fullscreen-wrapper ::deep .crepe tr:nth-child(even),
|
||||
.theme-light .creator-fullscreen-wrapper ::deep .milkdown tr:nth-child(even),
|
||||
.theme-light .creator-fullscreen-wrapper ::deep .ProseMirror tr:nth-child(even) {
|
||||
background-color: rgba(0, 0, 0, 0.015) !important;
|
||||
}
|
||||
|
||||
/* Lists and Task Lists */
|
||||
.creator-fullscreen-wrapper ::deep .crepe ul,
|
||||
.creator-fullscreen-wrapper ::deep .crepe ol,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown ul,
|
||||
.creator-fullscreen-wrapper ::deep .milkdown ol,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror ul,
|
||||
.creator-fullscreen-wrapper ::deep .ProseMirror ol {
|
||||
line-height: 1.7 !important;
|
||||
}
|
||||
|
||||
/* 4. Bottom Actions Panel locked at floor zone of the card structure */
|
||||
.creator-actions-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem 0 0 0;
|
||||
border-top: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-nexus-premium {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background-color: var(--accent) !important;
|
||||
background: var(--accent) !important;
|
||||
color: #000000 !important;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 8px 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: var(--nexus-font-sans);
|
||||
font-size: 0.9rem;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.btn-nexus-premium:hover {
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.1);
|
||||
box-shadow: 0 4px 15px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-nexus-premium:hover .arrow-icon {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* 5. Dedicated Preview Card */
|
||||
.creator-preview-card {
|
||||
background: #121214;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
max-height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.preview-header h3 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
font-family: var(--nexus-font-sans);
|
||||
}
|
||||
|
||||
.pre-wrapper {
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.code-preview-block {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: 'Azeret Mono', SFMono-Regular, Consolas, Menlo, monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #e4e4e7;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Merriweather:ital,wght@0,300;0,400;0,700;1,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Merriweather:ital,wght@0,300;0,400;0,700;1,400&family=Azeret+Mono:wght@300;400;500;600&display=swap');
|
||||
|
||||
:root {
|
||||
/* Semantic design tokens - default to Modern Deep Dark (Dark Mode) */
|
||||
@@ -19,6 +19,7 @@
|
||||
--nexus-paper: #F9F9F9;
|
||||
--nexus-font-sans: 'Inter', sans-serif;
|
||||
--nexus-font-serif: 'Merriweather', serif;
|
||||
--nexus-font-mono: 'Azeret Mono', monospace;
|
||||
|
||||
/* Global Selection Style Override */
|
||||
--nexus-selection: rgba(0, 255, 153, 0.25);
|
||||
@@ -60,9 +61,12 @@
|
||||
--nexus-node-concept-text: #e0e0e0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--nexus-selection);
|
||||
color: inherit;
|
||||
::selection,
|
||||
.ProseMirror ::selection,
|
||||
.ProseMirror::selection,
|
||||
.ProseMirror *::selection {
|
||||
background-color: var(--nexus-selection) !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -444,4 +448,547 @@ h1:focus {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Selection Pop-up Menu & Crepe Toolbar Unification (SelectionAiPanel Style)
|
||||
========================================================================== */
|
||||
|
||||
/* 1. Pop-up Containers (Glassmorphism Capsule) */
|
||||
.milkdown-popover,
|
||||
.popover,
|
||||
.prosemirror-bubble-menu,
|
||||
.milkdown .popover,
|
||||
.milkdown-popover.popover,
|
||||
.milkdown-toolbar {
|
||||
background: rgba(24, 24, 28, 0.85) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
-webkit-backdrop-filter: blur(12px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 4px 6px !important;
|
||||
margin: 0 !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 12px 28px rgba(0, 0, 0, 0.4) !important;
|
||||
display: inline-flex; /* Removed !important to allow Tippy.js to hide popovers via inline style */
|
||||
align-items: center !important;
|
||||
gap: 4px !important;
|
||||
z-index: 10000 !important;
|
||||
box-sizing: border-box !important;
|
||||
animation: fadeInScaleGlobal 0.18s cubic-bezier(0.16, 1, 0.3, 1) !important;
|
||||
}
|
||||
|
||||
/* Light Theme (Warm Paper) Overrides for Container */
|
||||
.theme-light .milkdown-popover,
|
||||
.theme-light .popover,
|
||||
.theme-light .prosemirror-bubble-menu,
|
||||
.theme-light .milkdown .popover,
|
||||
.theme-light .milkdown-popover.popover,
|
||||
.theme-light .milkdown-toolbar {
|
||||
background: rgba(254, 254, 254, 0.95) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.04) !important;
|
||||
}
|
||||
|
||||
/* 2. Button & Item Formatting (Reader Selection Toolbar Style) */
|
||||
.milkdown-popover button,
|
||||
.popover button,
|
||||
.prosemirror-bubble-menu button,
|
||||
.milkdown .popover button,
|
||||
.milkdown-toolbar button {
|
||||
text-transform: none !important;
|
||||
font-size: 0.8rem !important;
|
||||
font-weight: 500 !important;
|
||||
color: #e4e4e7 !important; /* zinc-200 */
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
padding: 6px 12px !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
cursor: pointer !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
|
||||
/* Crepe's Specific Square Icon-Only Toolbar Items */
|
||||
.milkdown-toolbar .toolbar-item {
|
||||
text-transform: none !important;
|
||||
color: #e4e4e7 !important; /* zinc-200 */
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
cursor: pointer !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
padding: 6px !important;
|
||||
display: inline-flex !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Icon overrides inside buttons */
|
||||
.milkdown-popover button svg,
|
||||
.popover button svg,
|
||||
.prosemirror-bubble-menu button svg,
|
||||
.milkdown .popover button svg,
|
||||
.milkdown-popover button i,
|
||||
.popover button i,
|
||||
.prosemirror-bubble-menu button i,
|
||||
.milkdown .popover button i,
|
||||
.milkdown-toolbar button svg,
|
||||
.milkdown-toolbar .toolbar-item svg,
|
||||
.milkdown-toolbar button i,
|
||||
.milkdown-toolbar .toolbar-item i {
|
||||
color: currentColor !important;
|
||||
fill: currentColor !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
/* Hover effects (zinc-200 / zinc-100 highlight) */
|
||||
.milkdown-popover button:hover:not(.active),
|
||||
.popover button:hover:not(.active),
|
||||
.prosemirror-bubble-menu button:hover:not(.active),
|
||||
.milkdown-toolbar button:hover:not(.active),
|
||||
.milkdown-toolbar .toolbar-item:hover:not(.active) {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Active formatting state colors (var(--accent, #00ff99)) */
|
||||
.milkdown-popover button.active,
|
||||
.popover button.active,
|
||||
.prosemirror-bubble-menu button.active,
|
||||
.milkdown-popover button[aria-pressed="true"],
|
||||
.popover button[aria-pressed="true"],
|
||||
.prosemirror-bubble-menu button[aria-pressed="true"],
|
||||
.milkdown-toolbar button.active,
|
||||
.milkdown-toolbar .toolbar-item.active,
|
||||
.milkdown-toolbar button[aria-pressed="true"],
|
||||
.milkdown-toolbar .toolbar-item[aria-pressed="true"] {
|
||||
color: var(--accent, #00ff99) !important;
|
||||
}
|
||||
|
||||
.milkdown-popover button.active:hover,
|
||||
.popover button.active:hover,
|
||||
.prosemirror-bubble-menu button.active:hover,
|
||||
.milkdown-popover button[aria-pressed="true"]:hover,
|
||||
.popover button[aria-pressed="true"]:hover,
|
||||
.prosemirror-bubble-menu button[aria-pressed="true"]:hover,
|
||||
.milkdown-toolbar button.active:hover,
|
||||
.milkdown-toolbar .toolbar-item.active:hover,
|
||||
.milkdown-toolbar button[aria-pressed="true"]:hover,
|
||||
.milkdown-toolbar .toolbar-item[aria-pressed="true"]:hover {
|
||||
background: rgba(0, 255, 153, 0.08) !important;
|
||||
box-shadow: 0 0 12px rgba(0, 255, 153, 0.15) !important;
|
||||
color: var(--accent, #00ff99) !important;
|
||||
}
|
||||
|
||||
/* 3. Light Theme Overrides for Buttons */
|
||||
.theme-light .milkdown-popover button,
|
||||
.theme-light .popover button,
|
||||
.theme-light .prosemirror-bubble-menu button,
|
||||
.theme-light .milkdown-toolbar button,
|
||||
.theme-light .milkdown-toolbar .toolbar-item {
|
||||
color: #57524e !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown-popover button:hover:not(.active),
|
||||
.theme-light .popover button:hover:not(.active),
|
||||
.theme-light .prosemirror-bubble-menu button:hover:not(.active),
|
||||
.theme-light .milkdown-toolbar button:hover:not(.active),
|
||||
.theme-light .milkdown-toolbar .toolbar-item:hover:not(.active) {
|
||||
background: rgba(0, 0, 0, 0.04) !important;
|
||||
color: #1c1917 !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown-popover button.active,
|
||||
.theme-light .popover button.active,
|
||||
.theme-light .prosemirror-bubble-menu button.active,
|
||||
.theme-light .milkdown-popover button[aria-pressed="true"],
|
||||
.theme-light .popover button[aria-pressed="true"],
|
||||
.theme-light .prosemirror-bubble-menu button[aria-pressed="true"],
|
||||
.theme-light .milkdown-toolbar button.active,
|
||||
.theme-light .milkdown-toolbar .toolbar-item.active,
|
||||
.theme-light .milkdown-toolbar button[aria-pressed="true"],
|
||||
.theme-light .milkdown-toolbar .toolbar-item[aria-pressed="true"] {
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown-popover button.active:hover,
|
||||
.theme-light .popover button.active:hover,
|
||||
.theme-light .prosemirror-bubble-menu button.active:hover,
|
||||
.theme-light .milkdown-popover button[aria-pressed="true"]:hover,
|
||||
.theme-light .popover button[aria-pressed="true"]:hover,
|
||||
.theme-light .prosemirror-bubble-menu button[aria-pressed="true"]:hover,
|
||||
.theme-light .milkdown-toolbar button.active:hover,
|
||||
.theme-light .milkdown-toolbar .toolbar-item.active:hover,
|
||||
.theme-light .milkdown-toolbar button[aria-pressed="true"]:hover,
|
||||
.theme-light .milkdown-toolbar .toolbar-item[aria-pressed="true"]:hover {
|
||||
background: rgba(16, 185, 129, 0.06) !important;
|
||||
box-shadow: 0 0 12px rgba(16, 185, 129, 0.1) !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
/* 4. Dividers Alignment */
|
||||
.milkdown-popover .divider,
|
||||
.popover .divider,
|
||||
.prosemirror-bubble-menu .divider,
|
||||
.milkdown .popover .divider,
|
||||
.milkdown-toolbar .divider {
|
||||
width: 1px !important;
|
||||
height: 16px !important;
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
margin: 0 4px !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown-popover .divider,
|
||||
.theme-light .popover .divider,
|
||||
.theme-light .prosemirror-bubble-menu .divider,
|
||||
.theme-light .milkdown-toolbar .divider {
|
||||
background-color: rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Editor Table & General Control Elements Theming (SelectionAiPanel Style)
|
||||
========================================================================== */
|
||||
|
||||
/* 1. Cell Drag Handles (Column and Row Drag Handles) */
|
||||
.milkdown .milkdown-table-block .cell-handle {
|
||||
background-color: rgba(24, 24, 28, 0.85) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
-webkit-backdrop-filter: blur(12px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
color: #e4e4e7 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-table-block .cell-handle {
|
||||
background-color: rgba(254, 254, 254, 0.95) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
color: #57524e !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-table-block .cell-handle:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15) !important;
|
||||
color: #ffffff !important;
|
||||
border-color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-table-block .cell-handle:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08) !important;
|
||||
color: #1c1917 !important;
|
||||
border-color: #10b981 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-table-block .cell-handle svg {
|
||||
fill: currentColor !important;
|
||||
color: currentColor !important;
|
||||
}
|
||||
|
||||
/* 2. Drag Handle Options Popup Menu (.button-group) */
|
||||
.milkdown .milkdown-table-block .cell-handle .button-group {
|
||||
background: rgba(24, 24, 28, 0.85) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
-webkit-backdrop-filter: blur(12px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 12px 28px rgba(0, 0, 0, 0.4) !important;
|
||||
padding: 4px !important;
|
||||
gap: 4px !important;
|
||||
display: flex; /* Removed !important to allow toggling via data-show attribute */
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-table-block .cell-handle .button-group {
|
||||
background: rgba(254, 254, 254, 0.95) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.04) !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-table-block .cell-handle .button-group button {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
cursor: pointer !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
padding: 6px !important;
|
||||
display: inline-flex !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
box-sizing: border-box !important;
|
||||
color: #e4e4e7 !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-table-block .cell-handle .button-group button {
|
||||
color: #57524e !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-table-block .cell-handle .button-group button:hover {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-table-block .cell-handle .button-group button:hover {
|
||||
background: rgba(0, 0, 0, 0.04) !important;
|
||||
color: #1c1917 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-table-block .cell-handle .button-group button:active {
|
||||
background: rgba(0, 255, 153, 0.08) !important;
|
||||
color: var(--accent, #00ff99) !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-table-block .cell-handle .button-group button:active {
|
||||
background: rgba(16, 185, 129, 0.06) !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-table-block .cell-handle .button-group button svg {
|
||||
color: currentColor !important;
|
||||
fill: currentColor !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
/* 3. Table Column/Row Insertion Lines & Add Buttons */
|
||||
.milkdown .milkdown-table-block .line-handle {
|
||||
background-color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-table-block .line-handle {
|
||||
background-color: #10b981 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-table-block .line-handle .add-button {
|
||||
background-color: rgba(24, 24, 28, 0.85) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
-webkit-backdrop-filter: blur(12px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
color: #e4e4e7 !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-table-block .line-handle .add-button {
|
||||
background-color: rgba(254, 254, 254, 0.95) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
color: #57524e !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-table-block .line-handle .add-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15) !important;
|
||||
color: #ffffff !important;
|
||||
border-color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-table-block .line-handle .add-button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08) !important;
|
||||
color: #1c1917 !important;
|
||||
border-color: #10b981 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-table-block .line-handle .add-button svg {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
color: currentColor !important;
|
||||
fill: currentColor !important;
|
||||
}
|
||||
|
||||
/* 4. Paragraph Block Drag Handles */
|
||||
.milkdown .milkdown-block-handle {
|
||||
background-color: rgba(24, 24, 28, 0.85) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
-webkit-backdrop-filter: blur(12px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
border-radius: 6px !important;
|
||||
padding: 2px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
display: flex; /* Removed !important to allow toggling */
|
||||
gap: 2px !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-block-handle {
|
||||
background-color: rgba(254, 254, 254, 0.95) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-block-handle .operation-item {
|
||||
border-radius: 4px !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
padding: 4px !important;
|
||||
display: inline-flex !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
color: #e4e4e7 !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-block-handle .operation-item {
|
||||
color: #57524e !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-block-handle .operation-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-block-handle .operation-item:hover {
|
||||
background: rgba(0, 0, 0, 0.04) !important;
|
||||
color: #1c1917 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-block-handle .operation-item.active {
|
||||
background: rgba(0, 255, 153, 0.08) !important;
|
||||
color: var(--accent, #00ff99) !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-block-handle .operation-item.active {
|
||||
background: rgba(16, 185, 129, 0.06) !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-block-handle .operation-item svg {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
fill: currentColor !important;
|
||||
color: currentColor !important;
|
||||
}
|
||||
|
||||
/* 5. Slash Commands Menu */
|
||||
.milkdown .milkdown-slash-menu {
|
||||
background: rgba(24, 24, 28, 0.85) !important;
|
||||
backdrop-filter: blur(12px) !important;
|
||||
-webkit-backdrop-filter: blur(12px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 12px 28px rgba(0, 0, 0, 0.4) !important;
|
||||
font-family: var(--nexus-font-sans) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-slash-menu {
|
||||
background: rgba(254, 254, 254, 0.95) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.04) !important;
|
||||
color: #2d2a26 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-slash-menu .tab-group ul li {
|
||||
color: #a1a1aa !important;
|
||||
border-radius: 6px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-slash-menu .tab-group ul li {
|
||||
color: #78716c !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-slash-menu .tab-group ul li:hover {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-slash-menu .tab-group ul li:hover {
|
||||
background: rgba(0, 0, 0, 0.04) !important;
|
||||
color: #1c1917 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-slash-menu .tab-group ul li.selected {
|
||||
background: rgba(0, 255, 153, 0.08) !important;
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-slash-menu .tab-group ul li.selected {
|
||||
background: rgba(16, 185, 129, 0.06) !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-slash-menu .menu-groups .menu-group li {
|
||||
color: #e4e4e7 !important;
|
||||
border-radius: 6px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-slash-menu .menu-groups .menu-group li {
|
||||
color: #57524e !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-slash-menu .menu-groups .menu-group li svg {
|
||||
color: #a1a1aa !important;
|
||||
fill: #a1a1aa !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-slash-menu .menu-groups .menu-group li svg {
|
||||
color: #78716c !important;
|
||||
fill: #78716c !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-slash-menu .menu-groups .menu-group li.hover {
|
||||
background: rgba(255, 255, 255, 0.05) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-slash-menu .menu-groups .menu-group li.hover {
|
||||
background: rgba(0, 0, 0, 0.04) !important;
|
||||
color: #1c1917 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-slash-menu .menu-groups .menu-group li.active {
|
||||
background: rgba(0, 255, 153, 0.08) !important;
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-slash-menu .menu-groups .menu-group li.active {
|
||||
background: rgba(16, 185, 129, 0.06) !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
.milkdown .milkdown-slash-menu .menu-groups .menu-group li.active svg {
|
||||
color: var(--accent) !important;
|
||||
fill: var(--accent) !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown .milkdown-slash-menu .menu-groups .menu-group li.active svg {
|
||||
color: #10b981 !important;
|
||||
fill: #10b981 !important;
|
||||
}
|
||||
|
||||
/* 6. Explicit Visibility State Overrides */
|
||||
.milkdown-popover[data-show="false"],
|
||||
.popover[data-show="false"],
|
||||
.prosemirror-bubble-menu[data-show="false"],
|
||||
.milkdown-toolbar[data-show="false"],
|
||||
.milkdown-slash-menu[data-show="false"],
|
||||
.milkdown-table-block .cell-handle .button-group[data-show="false"],
|
||||
.milkdown-link-preview[data-show="false"],
|
||||
.milkdown-link-edit[data-show="false"] {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* 7. Table Overflow Clipping Fix for Handles */
|
||||
.milkdown .tableWrapper {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,104 @@
|
||||
// Map to keep track of active Crepe editor instances by elementId (container ID)
|
||||
const editorCache = new Map();
|
||||
|
||||
/**
|
||||
* Asynchronously injects a stylesheet link tag into the document head
|
||||
* and returns a Promise that resolves when the stylesheet is fully loaded.
|
||||
*/
|
||||
async function ensureStylesheet(href) {
|
||||
if (document.querySelector(`link[href="${href}"]`)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = href;
|
||||
link.onload = () => resolve();
|
||||
link.onerror = (err) => reject(new Error(`Failed to load stylesheet: ${href}. ${err}`));
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a Milkdown Crepe editor on the specified element.
|
||||
*/
|
||||
export async function initEditor(elementId, dotNetHelper, initialMarkdown) {
|
||||
const container = document.getElementById(elementId);
|
||||
if (!container) {
|
||||
console.error(`[Milkdown] Container with ID "${elementId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Condition 2: Prevent FOUC by loading stylesheets before instantiating the editor
|
||||
await ensureStylesheet('/_content/NexusReader.UI.Shared/css/vendor/milkdown-crepe.css');
|
||||
|
||||
// Dynamically import the local JS bundle
|
||||
await import('/_content/NexusReader.UI.Shared/js/vendor/milkdown-crepe.js');
|
||||
|
||||
// Get Crepe constructor from the global window.milkdownCrepe namespace
|
||||
const Crepe = window.milkdownCrepe?.Crepe;
|
||||
if (!Crepe) {
|
||||
throw new Error("Crepe constructor not found on window.milkdownCrepe");
|
||||
}
|
||||
|
||||
// Initialize the Crepe editor instance with custom ImageBlock upload handler
|
||||
const crepe = new Crepe({
|
||||
root: container,
|
||||
defaultValue: initialMarkdown || "",
|
||||
featureConfigs: {
|
||||
[Crepe.Feature.ImageBlock]: {
|
||||
onUpload: async (file) => {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
const url = await dotNetHelper.invokeMethodAsync('UploadImageFromJs', file.name, file.type, uint8Array);
|
||||
return url;
|
||||
} catch (err) {
|
||||
console.error("[Milkdown] Failed to upload image from JS:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Store the editor instance in the map
|
||||
editorCache.set(elementId, crepe);
|
||||
|
||||
// Create the editor view asynchronously
|
||||
await crepe.create();
|
||||
|
||||
console.log(`[Milkdown] Editor successfully initialized on element: ${elementId}`);
|
||||
} catch (error) {
|
||||
console.error(`[Milkdown] Failed to initialize editor on "${elementId}":`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current Markdown content from a specific editor instance.
|
||||
*/
|
||||
export function getMarkdownContent(elementId) {
|
||||
const crepe = editorCache.get(elementId);
|
||||
if (!crepe) {
|
||||
console.warn(`[Milkdown] No editor instance found for element: ${elementId}`);
|
||||
return "";
|
||||
}
|
||||
return crepe.getMarkdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely disposes of the editor instance to prevent memory leaks in WASM.
|
||||
*/
|
||||
export async function destroyEditor(elementId) {
|
||||
const crepe = editorCache.get(elementId);
|
||||
if (crepe) {
|
||||
try {
|
||||
await crepe.destroy();
|
||||
console.log(`[Milkdown] Editor instance successfully destroyed: ${elementId}`);
|
||||
} catch (error) {
|
||||
console.error(`[Milkdown] Error destroying editor for element "${elementId}":`, error);
|
||||
}
|
||||
editorCache.delete(elementId);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -769,6 +769,64 @@ app.MapPost("/identity/theme", async (
|
||||
return Results.Ok();
|
||||
}).RequireAuthorization();
|
||||
|
||||
app.MapPost("/api/media/upload", async (
|
||||
HttpRequest request,
|
||||
NexusReader.Application.Abstractions.Services.IStorageService storageService,
|
||||
ILogger<Program> logger) =>
|
||||
{
|
||||
if (!request.HasFormContentType)
|
||||
{
|
||||
return Results.BadRequest("Request must be a multipart form.");
|
||||
}
|
||||
|
||||
var form = await request.ReadFormAsync();
|
||||
var file = form.Files.GetFile("file");
|
||||
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return Results.BadRequest("No file uploaded.");
|
||||
}
|
||||
|
||||
// Size limit check (max 5MB)
|
||||
const long maxFileSize = 5 * 1024 * 1024;
|
||||
if (file.Length > maxFileSize)
|
||||
{
|
||||
return Results.BadRequest("File size exceeds the 5MB limit.");
|
||||
}
|
||||
|
||||
// Read file bytes for signature check
|
||||
byte[] fileBytes;
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
await file.CopyToAsync(memoryStream);
|
||||
fileBytes = memoryStream.ToArray();
|
||||
}
|
||||
|
||||
// Validate signature
|
||||
if (!ValidateImageSignature(fileBytes, file.ContentType))
|
||||
{
|
||||
logger.LogWarning("File signature validation failed for file {FileName} with content type {ContentType}.", file.FileName, file.ContentType);
|
||||
return Results.BadRequest("Invalid image signature. Legitimate JPEG, PNG, or WEBP images only.");
|
||||
}
|
||||
|
||||
// Save using IStorageService
|
||||
var fileUrl = await storageService.UploadFileAsync(fileBytes, file.FileName, file.ContentType);
|
||||
return Results.Ok(new NexusReader.Application.DTOs.Media.UploadResultDto(fileUrl));
|
||||
}).DisableAntiforgery();
|
||||
|
||||
app.MapPost("/api/chapters/validate", (
|
||||
[Microsoft.AspNetCore.Mvc.FromBody] NexusReader.Application.DTOs.Media.ValidateChapterRequest request,
|
||||
NexusReader.Application.Abstractions.Services.ISanitizerService sanitizerService) =>
|
||||
{
|
||||
if (request == null || string.IsNullOrEmpty(request.Content))
|
||||
{
|
||||
return Results.Ok(new NexusReader.Application.DTOs.Media.ValidateChapterResponse(string.Empty));
|
||||
}
|
||||
|
||||
var sanitized = sanitizerService.Sanitize(request.Content);
|
||||
return Results.Ok(new NexusReader.Application.DTOs.Media.ValidateChapterResponse(sanitized));
|
||||
}).DisableAntiforgery();
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
@@ -820,6 +878,34 @@ async Task TriggerBackgroundProcessingForUnindexedBooksAsync(IServiceProvider se
|
||||
}
|
||||
}
|
||||
|
||||
static bool ValidateImageSignature(byte[] bytes, string contentType)
|
||||
{
|
||||
if (bytes.Length < 4) return false;
|
||||
|
||||
// Check PNG signature: 89 50 4E 47
|
||||
if (bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47)
|
||||
{
|
||||
return contentType.Equals("image/png", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Check JPEG signature: FF D8 FF
|
||||
if (bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF)
|
||||
{
|
||||
return contentType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase) ||
|
||||
contentType.Equals("image/jpg", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Check WEBP signature: RIFF ... WEBP
|
||||
if (bytes.Length >= 12 &&
|
||||
bytes[0] == 0x52 && bytes[1] == 0x49 && bytes[2] == 0x46 && bytes[3] == 0x46 && // RIFF
|
||||
bytes[8] == 0x57 && bytes[9] == 0x45 && bytes[10] == 0x42 && bytes[11] == 0x50) // WEBP
|
||||
{
|
||||
return contentType.Equals("image/webp", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public record KnowledgeRequest(string Text, Guid? EbookId = null);
|
||||
public record GroundednessRequest(string Answer, string Context);
|
||||
public record SemanticSearchRequest(string QueryText, int Limit = 5);
|
||||
|
||||
Reference in New Issue
Block a user