mirror of
https://github.com/uthvah/sync-embeds.git
synced 2026-07-22 06:43:40 +00:00
Critical fixes
- Add build tools - Fix properties offset - Fix delete/backsapce edge case errors - Unified header logic - Padding still needs some tweaking (themes? minimal, spacious etc)
This commit is contained in:
parent
bfa0c26c1c
commit
d8e8566e15
5 changed files with 371 additions and 470 deletions
51
esbuild.config.mjs
Normal file
51
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = process.env.NODE_ENV === 'production';
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.js"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js", // Outputs to root for Obsidian
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
|
||||
},
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
22
package.json
Normal file
22
package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "obsidian-sync-embeds",
|
||||
"version": "2.0.0",
|
||||
"description": "Create fully editable, live-synced note embeds",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "NODE_ENV=production node esbuild.config.mjs"
|
||||
},
|
||||
"keywords": ["obsidian", "plugin", "embeds"],
|
||||
"author": "uthvah",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"monkey-around": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,31 +5,19 @@ const DynamicPaths = require('./dynamic-paths');
|
|||
class EmbedManager {
|
||||
constructor(plugin) {
|
||||
this.plugin = plugin;
|
||||
// Use WeakMap to prevent memory leaks
|
||||
this.embedRegistry = new WeakMap();
|
||||
// Keep a Set for tracking active embeds for cleanup
|
||||
this.activeEmbeds = new Set();
|
||||
this.viewportController = new ViewportController(plugin);
|
||||
this.dynamicPaths = new DynamicPaths(plugin);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// Clean up all active embeds
|
||||
this.activeEmbeds.forEach(embedData => {
|
||||
if (embedData.component) {
|
||||
embedData.component.unload();
|
||||
}
|
||||
// Properly detach leaf and view
|
||||
if (embedData.leaf) {
|
||||
embedData.leaf.detach();
|
||||
}
|
||||
if (embedData.component) embedData.component.unload();
|
||||
if (embedData.leaf) embedData.leaf.detach();
|
||||
});
|
||||
this.activeEmbeds.clear();
|
||||
|
||||
// Clean up dynamic paths cache
|
||||
if (this.dynamicPaths) {
|
||||
this.dynamicPaths.cleanup();
|
||||
}
|
||||
if (this.dynamicPaths) this.dynamicPaths.cleanup();
|
||||
}
|
||||
|
||||
getEmbedFromElement(element) {
|
||||
|
|
@ -49,7 +37,6 @@ class EmbedManager {
|
|||
el.empty();
|
||||
const syncContainer = el.createDiv('sync-container');
|
||||
|
||||
// Apply CSS custom properties
|
||||
syncContainer.style.setProperty('--sync-embed-height', this.plugin.settings.embedHeight);
|
||||
syncContainer.style.setProperty('--sync-max-height', this.plugin.settings.maxEmbedHeight);
|
||||
syncContainer.style.setProperty('--sync-gap', this.plugin.settings.gapBetweenEmbeds);
|
||||
|
|
@ -63,89 +50,59 @@ class EmbedManager {
|
|||
return;
|
||||
}
|
||||
|
||||
// Calculate total height for lazy loading to prevent scrollbar jumps
|
||||
const estimatedHeight = embedLines.length * 200; // Rough estimate
|
||||
const estimatedHeight = embedLines.length * 200;
|
||||
syncContainer.style.minHeight = `${estimatedHeight}px`;
|
||||
|
||||
for (let i = 0; i < embedLines.length; i++) {
|
||||
await this.processEmbed(embedLines[i], syncContainer, ctx, i > 0);
|
||||
}
|
||||
|
||||
// Remove min-height after all loaded
|
||||
setTimeout(() => {
|
||||
syncContainer.style.minHeight = '';
|
||||
}, 100);
|
||||
setTimeout(() => { syncContainer.style.minHeight = ''; }, 100);
|
||||
}
|
||||
|
||||
parseEmbedOptions(line) {
|
||||
// Parse options like: ![[note|alias{height:500px,title:false}]]
|
||||
const optionsMatch = line.match(/\{([^}]+)\}\]\]$/);
|
||||
const options = {};
|
||||
|
||||
if (optionsMatch) {
|
||||
const optionsStr = optionsMatch[1];
|
||||
const pairs = optionsStr.split(',');
|
||||
|
||||
pairs.forEach(pair => {
|
||||
const [key, value] = pair.split(':').map(s => s.trim());
|
||||
if (key && value !== undefined) {
|
||||
// Parse boolean values
|
||||
if (value === 'true') options[key] = true;
|
||||
else if (value === 'false') options[key] = false;
|
||||
else options[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Remove options from line for further parsing
|
||||
line = line.replace(/\{[^}]+\}\]\]$/, ']]');
|
||||
}
|
||||
|
||||
return { line, options };
|
||||
}
|
||||
|
||||
async processEmbed(embedLine, container, ctx, addGap) {
|
||||
try {
|
||||
// Parse custom options first
|
||||
const { line: cleanedLine, options } = this.parseEmbedOptions(embedLine);
|
||||
|
||||
// Parse embed syntax: ![[path#section|alias]]
|
||||
const match = cleanedLine.match(/!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/);
|
||||
if (!match) return;
|
||||
|
||||
let linkText = match[1];
|
||||
let displayAlias = match[2]?.trim();
|
||||
|
||||
// Check if this has dynamic patterns
|
||||
const hasDynamicPattern = /\{\{(date|time|title)/.test(linkText);
|
||||
|
||||
if (hasDynamicPattern) {
|
||||
// Check cache first (with 1 second TTL for date patterns)
|
||||
const cacheKey = `${linkText}-${ctx.sourcePath}`;
|
||||
const cached = this.dynamicPaths.pathCache.get(cacheKey);
|
||||
const now = Date.now();
|
||||
|
||||
let resolvedText;
|
||||
|
||||
if (cached && (now - cached.timestamp < 1000)) {
|
||||
resolvedText = cached.value;
|
||||
if (this.plugin.settings.debugMode) {
|
||||
console.log('[Sync Embeds] Using cached resolution:', linkText, '→', resolvedText);
|
||||
}
|
||||
} else {
|
||||
// Resolve dynamic patterns
|
||||
resolvedText = this.dynamicPaths.resolve(linkText, ctx);
|
||||
this.dynamicPaths.pathCache.set(cacheKey, { value: resolvedText, timestamp: now });
|
||||
|
||||
if (this.plugin.settings.debugMode) {
|
||||
console.log('[Sync Embeds] Fresh resolution:', linkText, '→', resolvedText);
|
||||
}
|
||||
}
|
||||
|
||||
// If no alias provided, use the original pattern as display name
|
||||
if (!displayAlias) {
|
||||
displayAlias = linkText;
|
||||
}
|
||||
|
||||
// CRITICAL: Use resolved text as the actual file path
|
||||
if (!displayAlias) displayAlias = linkText;
|
||||
linkText = resolvedText;
|
||||
}
|
||||
|
||||
|
|
@ -161,39 +118,30 @@ class EmbedManager {
|
|||
return;
|
||||
}
|
||||
|
||||
// Only check for direct recursion
|
||||
if (file.path === ctx.sourcePath) {
|
||||
this.renderError(container, "Cannot create a recursive embed of the same note.", addGap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create embed container
|
||||
const embedContainer = container.createDiv('sync-embed');
|
||||
if (addGap) embedContainer.addClass('sync-embed-gap');
|
||||
embedContainer.addClass('sync-embed-loading');
|
||||
|
||||
// Store custom options on container
|
||||
if (Object.keys(options).length > 0) {
|
||||
embedContainer.dataset.customOptions = JSON.stringify(options);
|
||||
}
|
||||
|
||||
// Create placeholder for lazy loading
|
||||
const placeholderText = displayAlias || `${file.basename}${section ? '#' + section : ''}`;
|
||||
const placeholder = embedContainer.createDiv('sync-embed-placeholder');
|
||||
placeholder.setText(`Loading ${placeholderText}...`);
|
||||
|
||||
const renderAsCallout = options.callout !== undefined ? options.callout : this.plugin.settings.renderAsCallout;
|
||||
if (renderAsCallout) embedContainer.addClass('is-callout-style');
|
||||
|
||||
if (renderAsCallout) {
|
||||
embedContainer.addClass('is-callout-style');
|
||||
}
|
||||
|
||||
// Aggressive lazy loading to prevent scrollbar jumps
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
observer.disconnect();
|
||||
// Small delay to ensure smooth scrolling
|
||||
requestAnimationFrame(() => {
|
||||
this.loadEmbed(embedContainer, file, section, displayAlias, ctx, placeholder, options);
|
||||
});
|
||||
|
|
@ -201,7 +149,7 @@ class EmbedManager {
|
|||
});
|
||||
}, {
|
||||
rootMargin: this.plugin.settings.lazyLoadThreshold,
|
||||
threshold: 0.01 // Start loading as soon as 1% is visible
|
||||
threshold: 0.01
|
||||
});
|
||||
|
||||
observer.observe(embedContainer);
|
||||
|
|
@ -218,7 +166,6 @@ class EmbedManager {
|
|||
const leaf = new WorkspaceLeaf(this.plugin.app);
|
||||
component.load();
|
||||
|
||||
// Store leaf for proper cleanup
|
||||
const embedData = {
|
||||
containerEl: embedContainer,
|
||||
file,
|
||||
|
|
@ -230,7 +177,6 @@ class EmbedManager {
|
|||
sourcePath: ctx.sourcePath
|
||||
};
|
||||
|
||||
// Register cleanup
|
||||
component.addChild(new (class extends Component {
|
||||
constructor(manager, data) {
|
||||
super();
|
||||
|
|
@ -238,26 +184,15 @@ class EmbedManager {
|
|||
this.embedData = data;
|
||||
}
|
||||
async onunload() {
|
||||
// Remove from active embeds
|
||||
this.manager.activeEmbeds.delete(this.embedData);
|
||||
|
||||
// Clear focus if this was focused
|
||||
if (this.manager.plugin.currentFocusedEmbed?.containerEl === this.embedData.containerEl) {
|
||||
this.manager.plugin.currentFocusedEmbed = null;
|
||||
}
|
||||
|
||||
// Properly detach the leaf and clean up the view
|
||||
if (this.embedData.leaf) {
|
||||
this.embedData.leaf.detach();
|
||||
}
|
||||
if (this.embedData.leaf) this.embedData.leaf.detach();
|
||||
}
|
||||
})(this, embedData));
|
||||
|
||||
// Open the full file in source mode
|
||||
await leaf.openFile(file, {
|
||||
state: { mode: 'source' }
|
||||
});
|
||||
|
||||
await leaf.openFile(file, { state: { mode: 'source' } });
|
||||
const view = leaf.view;
|
||||
|
||||
if (!(view instanceof MarkdownView)) {
|
||||
|
|
@ -266,85 +201,53 @@ class EmbedManager {
|
|||
return;
|
||||
}
|
||||
|
||||
// Update embedData with view and editor
|
||||
embedData.view = view;
|
||||
embedData.editor = view.editor;
|
||||
|
||||
this.embedRegistry.set(embedContainer, embedData);
|
||||
this.activeEmbeds.add(embedData);
|
||||
|
||||
// Apply custom height if specified
|
||||
if (customOptions.height) {
|
||||
embedContainer.style.setProperty('--sync-embed-height', customOptions.height);
|
||||
}
|
||||
|
||||
if (customOptions.maxHeight) {
|
||||
embedContainer.style.setProperty('--sync-max-height', customOptions.maxHeight);
|
||||
}
|
||||
|
||||
// Handle collapse option
|
||||
if (customOptions.collapse === true) {
|
||||
embedContainer.addClass('is-collapsed');
|
||||
}
|
||||
if (customOptions.height) embedContainer.style.setProperty('--sync-embed-height', customOptions.height);
|
||||
if (customOptions.maxHeight) embedContainer.style.setProperty('--sync-max-height', customOptions.maxHeight);
|
||||
if (customOptions.collapse === true) embedContainer.addClass('is-collapsed');
|
||||
|
||||
const renderAsCallout = customOptions.callout !== undefined ? customOptions.callout : this.plugin.settings.renderAsCallout;
|
||||
|
||||
// Construct the display title: prefer alias, then "Note > Section", then just Note
|
||||
const headerTitle = alias || (section ? `${file.basename} > ${section}` : file.basename);
|
||||
|
||||
// For section embeds, validate section exists then set up
|
||||
if (section) {
|
||||
const content = view.editor.getValue();
|
||||
const sectionInfo = this.viewportController.findSectionBounds(content, section);
|
||||
|
||||
if (sectionInfo.startLine === -1) {
|
||||
// Cleanup
|
||||
embedContainer.empty();
|
||||
embedContainer.removeClass('sync-embed-loading');
|
||||
embedContainer.style.height = 'auto';
|
||||
embedContainer.style.minHeight = '0';
|
||||
|
||||
// Render the error
|
||||
this.renderError(embedContainer, `Section not found: ${section}`, false);
|
||||
|
||||
leaf.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.viewportController.setupSectionViewport(embedData);
|
||||
|
||||
// If there's an alias, show it instead of the actual header
|
||||
if (alias || renderAsCallout) {
|
||||
this.setupAliasDisplay(embedData, headerTitle, true);
|
||||
}
|
||||
} else if (alias || renderAsCallout) {
|
||||
// For whole note embeds with alias or callout style, show title
|
||||
this.setupAliasDisplay(embedData, headerTitle, false);
|
||||
}
|
||||
|
||||
// Handle properties collapse
|
||||
if (this.plugin.settings.collapsePropertiesByDefault) {
|
||||
this.setupPropertiesCollapse(embedData);
|
||||
}
|
||||
|
||||
// Handle inline title visibility
|
||||
const showTitle = customOptions.title !== undefined
|
||||
? customOptions.title
|
||||
: this.plugin.settings.showInlineTitle;
|
||||
|
||||
// Hide title if disabled, if it's a section, OR if we're rendering as a callout (since callout has its own title)
|
||||
if (!showTitle || section || renderAsCallout) {
|
||||
this.hideInlineTitle(embedData);
|
||||
}
|
||||
|
||||
// Remove placeholder and show actual content
|
||||
// placeholder.remove();
|
||||
// embedContainer.appendChild(view.containerEl);
|
||||
// Using replaceWith instead of remove + appendChild
|
||||
placeholder.replaceWith(view.containerEl);
|
||||
// UNIFIED HEADER/TITLE LOGIC
|
||||
const userWantsTitle = customOptions.title !== undefined ? customOptions.title : this.plugin.settings.showInlineTitle;
|
||||
|
||||
embedContainer.removeClass('sync-embed-loading');
|
||||
// Callouts ALWAYS generate a header (for folding). Normal embeds respect settings.
|
||||
if (renderAsCallout || userWantsTitle) {
|
||||
this.setupHeaderUI(embedData, headerTitle, renderAsCallout, !!section);
|
||||
}
|
||||
|
||||
// PROPERTIES LOGIC
|
||||
if (section) {
|
||||
this.hideProperties(embedData); // Sections shouldn't have properties
|
||||
} else if (this.plugin.settings.collapsePropertiesByDefault) {
|
||||
this.setupPropertiesCollapse(embedData); // Whole notes conditionally collapse natively
|
||||
}
|
||||
|
||||
placeholder.replaceWith(view.containerEl);
|
||||
embedContainer.removeClass('sync-embed-loading');
|
||||
ctx.addChild(component);
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -354,101 +257,78 @@ class EmbedManager {
|
|||
}
|
||||
}
|
||||
|
||||
setupAliasDisplay(embedData, displayAlias, isSection) {
|
||||
const { view, file, section } = embedData;
|
||||
const renderAsCallout = embedData.customOptions.callout !== undefined
|
||||
? embedData.customOptions.callout
|
||||
: this.plugin.settings.renderAsCallout;
|
||||
setupHeaderUI(embedData, displayTitle, renderAsCallout, isSection) {
|
||||
const { view, file, section, containerEl } = embedData;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
// Hide the inline title
|
||||
// Completely kill the native title to prevent duplication
|
||||
const titleEl = view.containerEl.querySelector('.inline-title');
|
||||
if (titleEl) {
|
||||
titleEl.style.display = 'none';
|
||||
}
|
||||
if (titleEl) titleEl.style.display = 'none';
|
||||
|
||||
// For section embeds, also hide the section header
|
||||
if (isSection && embedData.sectionInfo) {
|
||||
const cmContent = view.containerEl.querySelector('.cm-content');
|
||||
if (cmContent) {
|
||||
const headerLineNumber = embedData.sectionInfo.startLine + 1;
|
||||
const firstLine = cmContent.querySelector(`.cm-line:nth-child(${headerLineNumber})`);
|
||||
if (firstLine) {
|
||||
firstLine.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
const viewContent = view.containerEl.querySelector('.view-content');
|
||||
if (!viewContent) return;
|
||||
if (view.containerEl.querySelector('.sync-embed-header')) return; // Prevent dupes
|
||||
|
||||
const headerUI = document.createElement('div');
|
||||
headerUI.className = 'sync-embed-header';
|
||||
|
||||
// Create and insert alias header with consistent styling
|
||||
const aliasHeader = view.containerEl.createDiv('sync-embed-alias-header');
|
||||
|
||||
if (renderAsCallout) {
|
||||
aliasHeader.addClass('is-sticky');
|
||||
headerUI.classList.add('is-sticky');
|
||||
|
||||
// Add fold button
|
||||
const foldBtn = aliasHeader.createDiv('sync-embed-fold');
|
||||
const foldBtn = headerUI.createDiv('sync-embed-fold');
|
||||
setIcon(foldBtn, 'chevron-down');
|
||||
|
||||
// Create a clickable link
|
||||
const linkPath = section ? `${file.path}#${section}` : file.path;
|
||||
aliasHeader.createEl('a', {
|
||||
headerUI.createEl('a', {
|
||||
cls: 'internal-link',
|
||||
text: displayAlias,
|
||||
attr: {
|
||||
'href': linkPath,
|
||||
'data-href': linkPath
|
||||
}
|
||||
text: displayTitle,
|
||||
attr: { 'href': linkPath, 'data-href': linkPath }
|
||||
});
|
||||
|
||||
// Handle toggle (only if clicking the header background, not the link)
|
||||
aliasHeader.addEventListener('click', (e) => {
|
||||
headerUI.addEventListener('click', (e) => {
|
||||
if (e.target.closest('a')) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
embedData.containerEl.classList.toggle('is-collapsed');
|
||||
containerEl.classList.toggle('is-collapsed');
|
||||
});
|
||||
|
||||
// Callout headers sit ABOVE the content to stick perfectly
|
||||
view.containerEl.insertBefore(headerUI, viewContent);
|
||||
} else {
|
||||
aliasHeader.textContent = displayAlias;
|
||||
}
|
||||
|
||||
const viewContent = view.containerEl.querySelector('.view-content');
|
||||
if (viewContent) {
|
||||
viewContent.insertBefore(aliasHeader, viewContent.firstChild);
|
||||
// Standard inline alias header sits inside the content
|
||||
headerUI.textContent = displayTitle;
|
||||
viewContent.insertBefore(headerUI, viewContent.firstChild);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
hideProperties(embedData) {
|
||||
const { view } = embedData;
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
const propertiesEl = view.containerEl.querySelector('.metadata-container');
|
||||
if (propertiesEl) {
|
||||
propertiesEl.style.display = 'none';
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
setupPropertiesCollapse(embedData) {
|
||||
const { view } = embedData;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
const propertiesEl = view.containerEl.querySelector('.metadata-container');
|
||||
if (!propertiesEl) return;
|
||||
|
||||
propertiesEl.classList.add('is-collapsed');
|
||||
|
||||
const toggleBtn = propertiesEl.createDiv('properties-collapse-toggle');
|
||||
toggleBtn.innerHTML = 'â–¶';
|
||||
toggleBtn.onclick = () => {
|
||||
propertiesEl.classList.toggle('is-collapsed');
|
||||
toggleBtn.innerHTML = propertiesEl.classList.contains('is-collapsed') ? 'â–¶' : 'â–¼';
|
||||
};
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
hideInlineTitle(embedData) {
|
||||
const { view } = embedData;
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
const titleEl = view.containerEl.querySelector('.inline-title');
|
||||
if (titleEl) {
|
||||
titleEl.style.display = 'none';
|
||||
// Fire a real click event on the heading so Obsidian natively updates its internal state
|
||||
const heading = propertiesEl.querySelector('.metadata-properties-heading');
|
||||
if (heading && !propertiesEl.classList.contains('is-collapsed')) {
|
||||
heading.click();
|
||||
}
|
||||
}, 50);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -459,4 +339,4 @@ class EmbedManager {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = EmbedManager;
|
||||
module.exports = EmbedManager;
|
||||
|
|
@ -8,168 +8,160 @@ class ViewportController {
|
|||
async setupSectionViewport(embedData) {
|
||||
const { view, editor, file, section } = embedData;
|
||||
|
||||
// Wait for editor to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const content = editor.getValue();
|
||||
const sectionInfo = this.findSectionBounds(content, section);
|
||||
|
||||
// Missing section should be handled by the caller. Log to console for debugging if we reach this point.
|
||||
if (sectionInfo.startLine === -1) {
|
||||
console.warn('Sync Embeds: Section not found for viewport embedding:', section);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store section metadata
|
||||
embedData.sectionInfo = sectionInfo;
|
||||
embedData.viewportActive = true;
|
||||
|
||||
// Apply the viewport restriction
|
||||
this.applyViewportRestriction(embedData);
|
||||
|
||||
// Setup header protection (simplified, no auto-correction)
|
||||
this.setupHeaderProtection(embedData);
|
||||
|
||||
// NEW: Setup input interception to block # typing
|
||||
this.setupBoundaryProtection(embedData);
|
||||
this.setupHeaderInputInterception(embedData);
|
||||
|
||||
// Setup cursor and content constraints
|
||||
this.setupContentConstraints(embedData);
|
||||
|
||||
// Scroll to section
|
||||
this.scrollToSection(embedData);
|
||||
}
|
||||
|
||||
applyViewportRestriction(embedData) {
|
||||
const { view, sectionInfo } = embedData;
|
||||
const { startLine, endLine } = sectionInfo;
|
||||
|
||||
// Use CSS to hide lines outside the viewport
|
||||
const { view } = embedData;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.className = 'sync-viewport-style';
|
||||
|
||||
// Generate unique ID for this embed
|
||||
const embedId = 'embed-' + Math.random().toString(36).substr(2, 9);
|
||||
view.containerEl.setAttribute('data-embed-id', embedId);
|
||||
|
||||
// Store embed ID for dynamic updates
|
||||
embedData.embedId = embedId;
|
||||
|
||||
this.updateViewportCSS(embedData, style);
|
||||
|
||||
view.containerEl.appendChild(style);
|
||||
|
||||
// Store for cleanup and updates
|
||||
embedData.viewportStyle = style;
|
||||
}
|
||||
|
||||
updateViewportCSS(embedData, style) {
|
||||
const { sectionInfo, embedId } = embedData;
|
||||
const { sectionInfo, embedId, file } = embedData;
|
||||
const { startLine, endLine } = sectionInfo;
|
||||
|
||||
// PERFORMANCE: Use efficient CSS selectors
|
||||
// Frontmatter DOM Offset compensation
|
||||
// CodeMirror folds frontmatter, removing all but ONE line from the DOM.
|
||||
let domOffset = 0;
|
||||
const fileCache = this.plugin.app.metadataCache.getFileCache(file);
|
||||
if (fileCache && fileCache.frontmatterPosition) {
|
||||
// No +1 needed because the first line is retained by CM6 for the fold widget
|
||||
domOffset = fileCache.frontmatterPosition.end.line;
|
||||
}
|
||||
|
||||
const domStartLine = Math.max(0, startLine - domOffset);
|
||||
const domEndLine = Math.max(0, endLine - domOffset);
|
||||
|
||||
const css = `
|
||||
/* Hide all lines before the section */
|
||||
[data-embed-id="${embedId}"] .cm-line:nth-child(-n+${startLine}) {
|
||||
/* Hide all lines BEFORE and INCLUDING the section header */
|
||||
[data-embed-id="${embedId}"] .cm-line:nth-child(-n+${domStartLine + 1}) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide all lines after the section */
|
||||
[data-embed-id="${embedId}"] .cm-line:nth-child(n+${endLine + 1}) {
|
||||
/* Hide all lines AFTER the section */
|
||||
[data-embed-id="${embedId}"] .cm-line:nth-child(n+${domEndLine + 1}) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Style the header line - No background, proper theming */
|
||||
[data-embed-id="${embedId}"] .cm-line:nth-child(${startLine + 1}) {
|
||||
pointer-events: none !important;
|
||||
user-select: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
`;
|
||||
|
||||
style.textContent = css;
|
||||
}
|
||||
|
||||
setupHeaderProtection(embedData) {
|
||||
const { view, editor, sectionInfo, component } = embedData;
|
||||
const { startLine } = sectionInfo;
|
||||
|
||||
// Store original header to restore if modified
|
||||
const originalHeader = editor.getLine(startLine);
|
||||
embedData.originalHeader = originalHeader;
|
||||
|
||||
let isProgrammaticUpdate = false;
|
||||
|
||||
const changeHandler = (changedEditor) => {
|
||||
if (changedEditor !== editor || isProgrammaticUpdate) return;
|
||||
if (!embedData.viewportActive) return;
|
||||
|
||||
const cursorPos = editor.getCursor();
|
||||
const absoluteLine = cursorPos.line;
|
||||
|
||||
// Protect the header line from any edits
|
||||
if (absoluteLine === startLine) {
|
||||
const currentLine = editor.getLine(startLine);
|
||||
if (currentLine !== originalHeader) {
|
||||
isProgrammaticUpdate = true;
|
||||
editor.replaceRange(
|
||||
originalHeader,
|
||||
{ line: startLine, ch: 0 },
|
||||
{ line: startLine, ch: currentLine.length }
|
||||
);
|
||||
// Move cursor to line below
|
||||
editor.setCursor({ line: startLine + 1, ch: 0 });
|
||||
isProgrammaticUpdate = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
component.registerEvent(
|
||||
this.plugin.app.workspace.on('editor-change', changeHandler)
|
||||
);
|
||||
}
|
||||
|
||||
setupHeaderInputInterception(embedData) {
|
||||
// CHANGED: Removed `sectionInfo` from destructuring to avoid a stale closure.
|
||||
setupBoundaryProtection(embedData) {
|
||||
const { view, editor, component } = embedData;
|
||||
// The header level of the main section won't change, so it's safe to get it once.
|
||||
const { headerLevel } = embedData.sectionInfo;
|
||||
|
||||
// Always enforce in section embeds
|
||||
let lastNoticeTime = 0;
|
||||
const noticeDebounce = 5000; // 5 seconds between notices
|
||||
|
||||
// CRITICAL: Wait for the editor to be fully ready and focused
|
||||
// Use a small delay to ensure the CodeMirror editor is properly initialized
|
||||
const setupHandlers = () => {
|
||||
// Get the actual CodeMirror editor element
|
||||
const cmEditor = view.containerEl.querySelector('.cm-content');
|
||||
if (!cmEditor) {
|
||||
// Retry if not ready yet
|
||||
setTimeout(setupHandlers, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle input event to catch # at line start (use 'input' not 'beforeinput' for better timing)
|
||||
const keydownHandler = (event) => {
|
||||
if (!embedData.viewportActive || !embedData.sectionInfo) return;
|
||||
|
||||
const { startLine, endLine } = embedData.sectionInfo;
|
||||
const cursor = editor.getCursor();
|
||||
const selection = editor.getSelection();
|
||||
|
||||
// PROTECT TOP BOUNDARY (Block Backspace from deleting the invisible boundary newline)
|
||||
if (event.key === 'Backspace') {
|
||||
if (selection) {
|
||||
const from = editor.getCursor('from');
|
||||
if (from.line <= startLine) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (cursor.line === startLine + 1 && cursor.ch === 0) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PROTECT BOTTOM BOUNDARY (Block Delete from deleting the invisible boundary newline)
|
||||
if (event.key === 'Delete') {
|
||||
if (selection) {
|
||||
const to = editor.getCursor('to');
|
||||
if (to.line >= endLine) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const lastEditableLine = endLine - 1;
|
||||
const lastLineLength = editor.getLine(lastEditableLine)?.length || 0;
|
||||
if (cursor.line === lastEditableLine && cursor.ch === lastLineLength) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cmEditor.addEventListener('keydown', keydownHandler, true);
|
||||
component.register(() => {
|
||||
cmEditor.removeEventListener('keydown', keydownHandler, true);
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(setupHandlers, 100);
|
||||
}
|
||||
|
||||
setupHeaderInputInterception(embedData) {
|
||||
const { view, editor, component } = embedData;
|
||||
const { headerLevel } = embedData.sectionInfo;
|
||||
|
||||
let lastNoticeTime = 0;
|
||||
const noticeDebounce = 5000;
|
||||
|
||||
const setupHandlers = () => {
|
||||
const cmEditor = view.containerEl.querySelector('.cm-content');
|
||||
if (!cmEditor) {
|
||||
setTimeout(setupHandlers, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
const inputHandler = (event) => {
|
||||
// Only process insertText events
|
||||
if (event.inputType !== 'insertText' && event.inputType !== 'insertFromPaste') return;
|
||||
if (event.data !== '#') return;
|
||||
|
||||
// Check immediately if # was typed at line start
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
|
||||
// Check if the # is at the start of the line (accounting for whitespace)
|
||||
const beforeHash = line.substring(0, cursor.ch - 1);
|
||||
const isAtLineStart = /^\s*$/.test(beforeHash);
|
||||
|
||||
// CHANGED: Read directly from `embedData.sectionInfo` to get the latest, non-stale bounds.
|
||||
if (isAtLineStart &&
|
||||
cursor.line > embedData.sectionInfo.startLine &&
|
||||
cursor.line < embedData.sectionInfo.endLine) {
|
||||
|
||||
// Remove the # that was just typed
|
||||
const currentLine = editor.getLine(cursor.line);
|
||||
const newLine = currentLine.substring(0, cursor.ch - 1) + currentLine.substring(cursor.ch);
|
||||
editor.replaceRange(
|
||||
|
|
@ -179,146 +171,122 @@ class ViewportController {
|
|||
);
|
||||
editor.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
|
||||
|
||||
// Show notice (debounced)
|
||||
const now = Date.now();
|
||||
if (this.plugin.settings.showHeaderHints &&
|
||||
now - lastNoticeTime > noticeDebounce) {
|
||||
|
||||
lastNoticeTime = now;
|
||||
|
||||
const availableLevels = [];
|
||||
for (let i = headerLevel + 1; i <= 6; i++) {
|
||||
availableLevels.push(`H${i} (Alt+${i})`);
|
||||
}
|
||||
|
||||
new Notice(
|
||||
`⚠️ Cannot create H1-H${headerLevel} headers in this section.\n` +
|
||||
`Use: ${availableLevels.join(', ')}`,
|
||||
5000
|
||||
);
|
||||
new Notice(`⚠️ Cannot create H1-H${headerLevel} headers in this section.\nUse: ${availableLevels.join(', ')}`, 5000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Also handle paste events
|
||||
const pasteHandler = (event) => {
|
||||
const clipboardData = event.clipboardData?.getData('text');
|
||||
if (!clipboardData) return;
|
||||
|
||||
const cursor = editor.getCursor();
|
||||
|
||||
// CHANGED: Read directly from `embedData.sectionInfo` to get the latest, non-stale bounds.
|
||||
if (cursor.line > embedData.sectionInfo.startLine &&
|
||||
cursor.line < embedData.sectionInfo.endLine) {
|
||||
|
||||
const lines = clipboardData.split('\n');
|
||||
let hasInvalidHeaders = false;
|
||||
|
||||
// Check if pasted content has invalid headers
|
||||
const adjustedLines = lines.map(line => {
|
||||
const match = line.match(/^(#{1,6})\s+(.*)$/);
|
||||
if (!match) return line;
|
||||
|
||||
const [, hashes, content] = match;
|
||||
const level = hashes.length;
|
||||
|
||||
if (level <= headerLevel) {
|
||||
if (hashes.length <= headerLevel) {
|
||||
hasInvalidHeaders = true;
|
||||
// Adjust to minimum allowed level
|
||||
const newLevel = headerLevel + 1;
|
||||
return '#'.repeat(newLevel) + ' ' + content;
|
||||
return '#'.repeat(headerLevel + 1) + ' ' + content;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
if (hasInvalidHeaders) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.plugin.settings.showHeaderHints) {
|
||||
new Notice(
|
||||
'Pasted headers adjusted to maintain section hierarchy',
|
||||
4000
|
||||
);
|
||||
new Notice('Pasted headers adjusted to maintain section hierarchy', 4000);
|
||||
}
|
||||
|
||||
// Insert adjusted content
|
||||
editor.replaceSelection(adjustedLines.join('\n'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Use 'input' event which fires AFTER the character is inserted
|
||||
cmEditor.addEventListener('input', inputHandler, true);
|
||||
cmEditor.addEventListener('paste', pasteHandler, true);
|
||||
|
||||
// Cleanup
|
||||
component.register(() => {
|
||||
cmEditor.removeEventListener('input', inputHandler, true);
|
||||
cmEditor.removeEventListener('paste', pasteHandler, true);
|
||||
});
|
||||
};
|
||||
|
||||
// Start setup with a small delay to ensure editor is ready
|
||||
setTimeout(setupHandlers, 100);
|
||||
}
|
||||
|
||||
setupContentConstraints(embedData) {
|
||||
const { view, editor, sectionInfo, component } = embedData;
|
||||
|
||||
const { view, editor, component } = embedData;
|
||||
let isProgrammaticUpdate = false;
|
||||
|
||||
// CRITICAL FIX: Update viewport IMMEDIATELY before visible changes
|
||||
const updateViewportImmediately = () => {
|
||||
if (!embedData.viewportActive) return;
|
||||
// Force cursor strictly inside bounds (preventing up/down arrow drifting)
|
||||
const enforceCursorBounds = () => {
|
||||
if (isProgrammaticUpdate || !embedData.viewportActive || !embedData.sectionInfo) return;
|
||||
|
||||
const currentContent = editor.getValue();
|
||||
const newSectionInfo = this.findSectionBounds(currentContent, embedData.section);
|
||||
|
||||
if (newSectionInfo.startLine !== -1) {
|
||||
embedData.sectionInfo = newSectionInfo;
|
||||
|
||||
// Update CSS synchronously to prevent flashing
|
||||
if (embedData.viewportStyle) {
|
||||
this.updateViewportCSS(embedData, embedData.viewportStyle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Monitor for content changes with instant updates
|
||||
const constrainContent = () => {
|
||||
if (isProgrammaticUpdate || !embedData.viewportActive) return;
|
||||
|
||||
// Update viewport bounds IMMEDIATELY
|
||||
updateViewportImmediately();
|
||||
|
||||
// Constrain cursor to section bounds
|
||||
const { startLine, endLine } = embedData.sectionInfo;
|
||||
const cursor = editor.getCursor();
|
||||
const currentSectionInfo = embedData.sectionInfo;
|
||||
|
||||
if (cursor.line < currentSectionInfo.startLine) {
|
||||
if (cursor.line <= startLine) {
|
||||
isProgrammaticUpdate = true;
|
||||
editor.setCursor({ line: currentSectionInfo.startLine + 1, ch: 0 });
|
||||
editor.setCursor({ line: startLine + 1, ch: 0 });
|
||||
isProgrammaticUpdate = false;
|
||||
} else if (cursor.line >= currentSectionInfo.endLine) {
|
||||
} else if (cursor.line >= endLine) {
|
||||
isProgrammaticUpdate = true;
|
||||
const lastEditableLine = currentSectionInfo.endLine - 1;
|
||||
const lastEditableLine = endLine - 1;
|
||||
const lastLineLength = editor.getLine(lastEditableLine)?.length || 0;
|
||||
editor.setCursor({ line: lastEditableLine, ch: lastLineLength });
|
||||
isProgrammaticUpdate = false;
|
||||
}
|
||||
};
|
||||
|
||||
// CHANGED: Removed `requestAnimationFrame`. Updates must be synchronous to prevent a race condition
|
||||
// where the `input` event for typing a hash fires before the section bounds are updated.
|
||||
const setupDOMListeners = () => {
|
||||
const cmContent = view.containerEl.querySelector('.cm-content');
|
||||
if (!cmContent) {
|
||||
setTimeout(setupDOMListeners, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
cmContent.addEventListener('mouseup', enforceCursorBounds);
|
||||
cmContent.addEventListener('focusin', enforceCursorBounds);
|
||||
cmContent.addEventListener('keyup', (e) => {
|
||||
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown'].includes(e.key)) {
|
||||
enforceCursorBounds();
|
||||
}
|
||||
});
|
||||
|
||||
component.register(() => {
|
||||
cmContent.removeEventListener('mouseup', enforceCursorBounds);
|
||||
cmContent.removeEventListener('focusin', enforceCursorBounds);
|
||||
cmContent.removeEventListener('keyup', enforceCursorBounds);
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(setupDOMListeners, 100);
|
||||
|
||||
// Retain text-change bounds checking
|
||||
component.registerEvent(
|
||||
this.plugin.app.workspace.on('editor-change', (changedEditor) => {
|
||||
if (changedEditor === editor) {
|
||||
constrainContent();
|
||||
this.updateViewportImmediately(embedData);
|
||||
enforceCursorBounds();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Prevent scrolling outside section
|
||||
const cmScroller = view.containerEl.querySelector('.cm-scroller');
|
||||
if (cmScroller) {
|
||||
const preventScroll = (e) => {
|
||||
|
|
@ -327,13 +295,21 @@ class ViewportController {
|
|||
const scrollTop = cmScroller.scrollTop;
|
||||
const lineHeight = editor.defaultTextHeight || 20;
|
||||
const firstVisibleLine = Math.floor(scrollTop / lineHeight);
|
||||
const currentSectionInfo = embedData.sectionInfo;
|
||||
|
||||
// Frontmatter DOM Offset compensation for scrolling bounds
|
||||
let domOffset = 0;
|
||||
const fileCache = this.plugin.app.metadataCache.getFileCache(embedData.file);
|
||||
if (fileCache && fileCache.frontmatterPosition) {
|
||||
domOffset = fileCache.frontmatterPosition.end.line;
|
||||
}
|
||||
|
||||
const domStartLine = Math.max(0, embedData.sectionInfo.startLine - domOffset);
|
||||
const domEndLine = Math.max(0, embedData.sectionInfo.endLine - domOffset);
|
||||
|
||||
// Constrain scrolling to section
|
||||
if (firstVisibleLine < Math.max(0, currentSectionInfo.startLine - 2)) {
|
||||
cmScroller.scrollTop = Math.max(0, currentSectionInfo.startLine - 2) * lineHeight;
|
||||
} else if (firstVisibleLine > currentSectionInfo.endLine - 2) {
|
||||
cmScroller.scrollTop = (currentSectionInfo.endLine - 2) * lineHeight;
|
||||
if (firstVisibleLine < Math.max(0, domStartLine - 2)) {
|
||||
cmScroller.scrollTop = Math.max(0, domStartLine - 2) * lineHeight;
|
||||
} else if (firstVisibleLine > domEndLine - 2) {
|
||||
cmScroller.scrollTop = (domEndLine - 2) * lineHeight;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -344,15 +320,27 @@ class ViewportController {
|
|||
}
|
||||
}
|
||||
|
||||
updateViewportImmediately(embedData) {
|
||||
if (!embedData.viewportActive) return;
|
||||
|
||||
const currentContent = embedData.editor.getValue();
|
||||
const newSectionInfo = this.findSectionBounds(currentContent, embedData.section);
|
||||
|
||||
if (newSectionInfo.startLine !== -1) {
|
||||
embedData.sectionInfo = newSectionInfo;
|
||||
|
||||
if (embedData.viewportStyle) {
|
||||
this.updateViewportCSS(embedData, embedData.viewportStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollToSection(embedData) {
|
||||
const { view, editor, sectionInfo } = embedData;
|
||||
const { editor, sectionInfo } = embedData;
|
||||
const { startLine } = sectionInfo;
|
||||
|
||||
setTimeout(() => {
|
||||
// Scroll to the start of the section
|
||||
editor.scrollIntoView({ line: startLine, ch: 0 }, true);
|
||||
|
||||
// Set cursor at line after header
|
||||
editor.scrollIntoView({ line: startLine + 1, ch: 0 }, true);
|
||||
editor.setCursor({ line: startLine + 1, ch: 0 });
|
||||
}, 150);
|
||||
}
|
||||
|
|
@ -365,7 +353,6 @@ class ViewportController {
|
|||
let startLine = -1;
|
||||
let headerLevel = 0;
|
||||
|
||||
// Find start of section
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (headerRegex.test(lines[i])) {
|
||||
startLine = i;
|
||||
|
|
@ -378,7 +365,6 @@ class ViewportController {
|
|||
return { startLine: -1, endLine: -1, headerLevel: 0 };
|
||||
}
|
||||
|
||||
// Find end of section
|
||||
let endLine = lines.length;
|
||||
for (let i = startLine + 1; i < lines.length; i++) {
|
||||
const match = lines[i].match(/^#+/);
|
||||
|
|
@ -403,4 +389,4 @@ class ViewportController {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = ViewportController;
|
||||
module.exports = ViewportController;
|
||||
178
styles.css
178
styles.css
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
/* === EMBED WRAPPER === */
|
||||
.sync-embed {
|
||||
border: none !important;
|
||||
border: none !important; /* Normal embeds get no border */
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
|
|
@ -22,50 +22,82 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
/* border: 1px solid var(--background-modifier-border) !important; */
|
||||
}
|
||||
|
||||
/* === STICKY ALIAS HEADER (CALLOUT STYLE) === */
|
||||
.sync-embed .markdown-source-view,
|
||||
.sync-embed .cm-editor,
|
||||
.sync-embed .cm-scroller,
|
||||
.sync-embed .view-content,
|
||||
.sync-embed .workspace-leaf-content {
|
||||
overflow: visible !important;
|
||||
/* Callout view applies the border to the embed container */
|
||||
.sync-embed.is-callout-style {
|
||||
border: 1px solid var(--background-modifier-border) !important;
|
||||
border-radius: 4px; /* Optional rounding for callouts */
|
||||
}
|
||||
|
||||
.sync-embed-alias-header.is-sticky {
|
||||
/* Compensate top padding when in callout mode to match bottom padding */
|
||||
.sync-embed.is-callout-style .cm-content {
|
||||
padding-top: 12px !important;
|
||||
}
|
||||
|
||||
/* === UNIFIED UI HEADER === */
|
||||
.sync-embed-header {
|
||||
font-size: var(--inline-title-size, var(--h1-size, 2em));
|
||||
font-weight: var(--inline-title-weight, var(--h1-weight, 700));
|
||||
line-height: var(--inline-title-line-height, var(--h1-line-height, 1.3));
|
||||
color: var(--inline-title-color, var(--h1-color, var(--text-normal)));
|
||||
font-family: var(--inline-title-font, var(--h1-font, var(--font-text)));
|
||||
font-variant: var(--h1-variant, normal);
|
||||
letter-spacing: var(--h1-letter-spacing, normal);
|
||||
|
||||
/* Matches body content padding */
|
||||
padding-top: 14px !important;
|
||||
padding-left: 16px !important;
|
||||
padding-right: 16px !important;
|
||||
margin-bottom: 0;
|
||||
|
||||
user-select: none !important;
|
||||
cursor: default !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.theme-dark .sync-embed-header,
|
||||
.theme-light .sync-embed-header {
|
||||
color: var(--inline-title-color, var(--h1-color, var(--text-normal)));
|
||||
}
|
||||
|
||||
/* === STICKY CALLOUT HEADER VARIANT === */
|
||||
.sync-embed-header.is-sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
background-color: var(--background-primary) !important;
|
||||
padding: 8px 40px 8px 10px !important;
|
||||
font-size: var(--font-text-size);
|
||||
|
||||
/* ALIGNMENT: 16px left to match body, 40px right for fold icon */
|
||||
padding: 8px 40px 8px 16px !important;
|
||||
|
||||
font-size: var(--inline-title-size);
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
cursor: pointer;
|
||||
user-select: none !important;
|
||||
/* border-bottom: 1px solid var(--background-modifier-border) !important; */
|
||||
border-bottom: 1px solid var(--background-modifier-border) !important;
|
||||
}
|
||||
|
||||
.sync-embed-alias-header.is-sticky:hover {
|
||||
.sync-embed-header.is-sticky:hover {
|
||||
background-color: var(--background-primary-alt) !important;
|
||||
}
|
||||
|
||||
.sync-embed-alias-header.is-sticky a.internal-link {
|
||||
.sync-embed-header.is-sticky a.internal-link {
|
||||
text-decoration: none;
|
||||
color: var(--text-accent) !important;
|
||||
font-weight: 600 !important;
|
||||
/* Truncation */
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sync-embed-alias-header.is-sticky a.internal-link:hover {
|
||||
.sync-embed-header.is-sticky a.internal-link:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--text-accent-hover) !important;
|
||||
}
|
||||
|
|
@ -91,12 +123,13 @@
|
|||
}
|
||||
|
||||
/* Hide everything except header when collapsed */
|
||||
.sync-embed.is-collapsed .view-content > *:not(.sync-embed-alias-header) {
|
||||
.sync-embed.is-collapsed .view-content,
|
||||
.sync-embed.is-collapsed .markdown-source-view {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Remove bottom border of header when collapsed */
|
||||
.sync-embed.is-collapsed .sync-embed-alias-header.is-sticky {
|
||||
.sync-embed.is-collapsed .sync-embed-header.is-sticky {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
|
|
@ -111,20 +144,16 @@
|
|||
border-top: 1px solid var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
/* === NATIVE OBSIDIAN PADDING SYSTEM (REBUILT) === */
|
||||
|
||||
/* 1. Hide the view header */
|
||||
/* === PADDING SYSTEM === */
|
||||
.sync-embed .view-header {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 2. The view-content: NO horizontal padding here (we'll handle it in cm-content) */
|
||||
.sync-embed .view-content {
|
||||
background-color: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 3. All editor components - transparent, no padding */
|
||||
.sync-embed .markdown-source-view,
|
||||
.sync-embed .cm-editor,
|
||||
.sync-embed .cm-scroller {
|
||||
|
|
@ -133,104 +162,42 @@
|
|||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 4. Content area - FIXED PADDING SYSTEM */
|
||||
/* 16px padding creates a flush left edge */
|
||||
.sync-embed .cm-content {
|
||||
background-color: transparent !important;
|
||||
|
||||
/* Aesthetic bottom padding - matches native Obsidian feel */
|
||||
padding-bottom: 18px !important;
|
||||
|
||||
/* Native Obsidian horizontal padding - fixed at 10px regardless of readable line width */
|
||||
padding-left: 10px !important;
|
||||
padding-right: 10px !important;
|
||||
padding-top: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
padding-left: 16px !important;
|
||||
padding-right: 16px !important;
|
||||
}
|
||||
|
||||
/* 5. Ensure full width */
|
||||
.sync-embed .markdown-source-view.mod-cm6 .cm-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 6. CRITICAL: Disable readable line width padding inheritance */
|
||||
.sync-embed .cm-sizer {
|
||||
/* Prevent extra padding from readable line width setting */
|
||||
padding-inline-start: 0 !important;
|
||||
padding-inline-end: 0 !important;
|
||||
}
|
||||
|
||||
/* === INLINE TITLE === */
|
||||
/* === HIDE NATIVE INLINE TITLE COMPLETELY === */
|
||||
.sync-embed .inline-title {
|
||||
pointer-events: none !important;
|
||||
user-select: none !important;
|
||||
cursor: default !important;
|
||||
opacity: 1 !important;
|
||||
color: var(--inline-title-color, var(--h1-color, var(--text-normal))) !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
padding-top: 16px !important;
|
||||
padding-left: 10px !important;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
|
||||
/* === ALIAS HEADER === */
|
||||
.sync-embed-alias-header {
|
||||
font-size: var(--inline-title-size, var(--h1-size, 2em));
|
||||
font-weight: var(--inline-title-weight, var(--h1-weight, 700));
|
||||
line-height: var(--inline-title-line-height, var(--h1-line-height, 1.3));
|
||||
color: var(--inline-title-color, var(--h1-color, var(--text-normal)));
|
||||
font-family: var(--inline-title-font, var(--h1-font, var(--font-text)));
|
||||
font-variant: var(--h1-variant, normal);
|
||||
letter-spacing: var(--h1-letter-spacing, normal);
|
||||
|
||||
/* Natural spacing like inline-title */
|
||||
padding-top: 16px !important;
|
||||
padding-left: 10px !important;
|
||||
padding-right: 10px !important;
|
||||
margin-bottom: 0;
|
||||
|
||||
user-select: none !important;
|
||||
cursor: default !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.sync-embed-alias-header a.internal-link {
|
||||
color: inherit !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Theme inheritance */
|
||||
.theme-dark .sync-embed-alias-header,
|
||||
.theme-light .sync-embed-alias-header {
|
||||
color: var(--inline-title-color, var(--h1-color, var(--text-normal)));
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* === PROPERTIES/FRONTMATTER === */
|
||||
/* Scoped properly to prevent affecting parent note */
|
||||
.sync-embed .metadata-container {
|
||||
/* ALIGNMENT: Matches header and body padding */
|
||||
padding-left: 16px !important;
|
||||
padding-right: 16px !important;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.metadata-container.is-collapsed .metadata-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.properties-collapse-toggle {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 4px 8px;
|
||||
margin: 4px 0;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-muted);
|
||||
transition: all 0.2s ease;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.properties-collapse-toggle:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
/* === SECTION EMBEDS === */
|
||||
.sync-embed .cm-header-1,
|
||||
.sync-embed .HyperMD-header-1 {
|
||||
|
|
@ -419,7 +386,7 @@ body.sync-embeds-no-focus-highlight .sync-embed:focus-within {
|
|||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.sync-embed-alias-header {
|
||||
.sync-embed-header {
|
||||
font-size: calc(var(--inline-title-size, var(--h1-size, 2em)) * 0.85);
|
||||
}
|
||||
}
|
||||
|
|
@ -461,7 +428,6 @@ body.sync-embeds-no-focus-highlight .sync-embed:focus-within {
|
|||
}
|
||||
|
||||
.sync-embed,
|
||||
.properties-collapse-toggle,
|
||||
.sync-embed::-webkit-scrollbar-thumb {
|
||||
transition: none;
|
||||
}
|
||||
|
|
@ -483,10 +449,6 @@ body.sync-embeds-no-focus-highlight .sync-embed:focus-within {
|
|||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.properties-collapse-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.metadata-container.is-collapsed .metadata-content {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue