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:
@@ -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