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,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
Reference in New Issue
Block a user