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:
2026-06-11 18:07:51 +00:00
committed by Marek Jaisński
parent 9fddafa423
commit ec3fc52a73
24 changed files with 2851 additions and 5 deletions
@@ -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();
}
}
}