Merge branch 'uthvah:main' into sync-callouts

This commit is contained in:
Oleh Ivaniuk 2026-02-21 17:49:16 +02:00 committed by GitHub
commit aa72dca846
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 170 additions and 20 deletions

View file

@ -1,4 +1,4 @@
const { Component, WorkspaceLeaf, MarkdownView } = require('obsidian');
const { Component, WorkspaceLeaf, MarkdownView, setIcon } = require('obsidian');
const ViewportController = require('./viewport-controller');
const DynamicPaths = require('./dynamic-paths');
@ -230,6 +230,12 @@ class EmbedManager {
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');
}
// Aggressive lazy loading to prevent scrollbar jumps
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
@ -268,7 +274,8 @@ class EmbedManager {
alias,
component,
leaf,
customOptions
customOptions,
sourcePath: ctx.sourcePath
};
// Register cleanup
@ -323,6 +330,16 @@ class EmbedManager {
embedContainer.style.setProperty('--sync-max-height', customOptions.maxHeight);
}
// Handle collapse option
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();
@ -345,12 +362,12 @@ class EmbedManager {
await this.viewportController.setupSectionViewport(embedData);
// If there's an alias, show it instead of the actual header
if (alias) {
this.setupAliasDisplay(embedData, alias, true);
if (alias || renderAsCallout) {
this.setupAliasDisplay(embedData, headerTitle, true);
}
} else if (alias) {
// For whole note embeds with alias, show alias as title
this.setupAliasDisplay(embedData, alias, false);
} else if (alias || renderAsCallout) {
// For whole note embeds with alias or callout style, show title
this.setupAliasDisplay(embedData, headerTitle, false);
}
// Handle properties collapse
@ -363,13 +380,17 @@ class EmbedManager {
? customOptions.title
: this.plugin.settings.showInlineTitle;
if (!showTitle || section) {
// 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);
// placeholder.remove();
// embedContainer.appendChild(view.containerEl);
// Using replaceWith instead of remove + appendChild
placeholder.replaceWith(view.containerEl);
embedContainer.removeClass('sync-embed-loading');
ctx.addChild(component);
@ -382,7 +403,10 @@ class EmbedManager {
}
setupAliasDisplay(embedData, displayAlias, isSection) {
const { view } = embedData;
const { view, file, section } = embedData;
const renderAsCallout = embedData.customOptions.callout !== undefined
? embedData.customOptions.callout
: this.plugin.settings.renderAsCallout;
requestAnimationFrame(() => {
setTimeout(() => {
@ -393,22 +417,48 @@ class EmbedManager {
}
// For section embeds, also hide the section header
if (isSection) {
if (isSection && embedData.sectionInfo) {
const cmContent = view.containerEl.querySelector('.cm-content');
if (cmContent) {
if (embedData.sectionInfo) {
const headerLineNumber = embedData.sectionInfo.startLine + 1;
const firstLine = cmContent.querySelector(`.cm-line:nth-child(${headerLineNumber})`);
if (firstLine) {
firstLine.style.display = 'none';
}
const headerLineNumber = embedData.sectionInfo.startLine + 1;
const firstLine = cmContent.querySelector(`.cm-line:nth-child(${headerLineNumber})`);
if (firstLine) {
firstLine.style.display = 'none';
}
}
}
// Create and insert alias header with consistent styling
const aliasHeader = view.containerEl.createDiv('sync-embed-alias-header');
aliasHeader.textContent = displayAlias;
if (renderAsCallout) {
aliasHeader.addClass('is-sticky');
// Add fold button
const foldBtn = aliasHeader.createDiv('sync-embed-fold');
setIcon(foldBtn, 'chevron-down');
// Create a clickable link
const linkPath = section ? `${file.path}#${section}` : file.path;
aliasHeader.createEl('a', {
cls: 'internal-link',
text: displayAlias,
attr: {
'href': linkPath,
'data-href': linkPath
}
});
// Handle toggle (only if clicking the header background, not the link)
aliasHeader.addEventListener('click', (e) => {
if (e.target.closest('a')) return;
e.stopPropagation();
e.preventDefault();
embedData.containerEl.classList.toggle('is-collapsed');
});
} else {
aliasHeader.textContent = displayAlias;
}
const viewContent = view.containerEl.querySelector('.view-content');
if (viewContent) {

View file

@ -9,6 +9,7 @@ const DEFAULT_SETTINGS = {
maxEmbedHeight: 'none',
collapsePropertiesByDefault: true,
showInlineTitle: true,
renderAsCallout: false,
enableCommandInterception: true,
gapBetweenEmbeds: '16px',
lazyLoadThreshold: '100px',

View file

@ -158,6 +158,16 @@ class SyncEmbedsSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Render as callout')
.setDesc('Render embeds as callouts with sticky headers and collapse functionality')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.renderAsCallout)
.onChange(async (value) => {
this.plugin.settings.renderAsCallout = value;
await this.plugin.saveSettings();
}));
// === HEADER MANAGEMENT SECTION ===
containerEl.createEl('h3', { text: 'Header Management' });
@ -231,6 +241,8 @@ class SyncEmbedsSettingTab extends PluginSettingTab {
<li><code>![[Note|Alias{height:500px}]]</code> - Custom height for this embed</li>
<li><code>![[Note|Alias{maxHeight:600px}]]</code> - Custom max height</li>
<li><code>![[Note|Alias{title:false}]]</code> - Hide title for this embed</li>
<li><code>![[Note|Alias{collapse:true}]]</code> - Start embed in collapsed state (requires callout style)</li>
<li><code>![[Note|Alias{callout:true}]]</code> - Force callout style for this embed</li>
<li><code>![[Note|Alias{height:400px,title:false}]]</code> - Multiple options</li>
</ul>
<p><em>Note: Options go inside curly braces before the closing ]]</em></p>
@ -277,6 +289,7 @@ class SyncEmbedsSettingTab extends PluginSettingTab {
<ul>
<li>Use aliases (text after <code>|</code>) with dynamic patterns for better display</li>
<li>Per-embed options override global settings: <code>{height:400px,title:false}</code></li>
<li>Use <code>collapse:true</code> to hide embed content by default (requires callout style)</li>
<li>Section embeds are fully editable and changes sync immediately</li>
<li>Press Tab/Shift+Tab to navigate between embeds</li>
<li>Keyboard shortcuts work inside embeds when command interception is enabled</li>

View file

@ -20,7 +20,89 @@
height: var(--sync-embed-height, auto);
max-height: var(--sync-max-height, none);
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;
}
.sync-embed-alias-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);
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; */
}
.sync-embed-alias-header.is-sticky:hover {
background-color: var(--background-primary-alt) !important;
}
.sync-embed-alias-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 {
text-decoration: underline;
color: var(--text-accent-hover) !important;
}
/* === FOLD ICON === */
.sync-embed-fold {
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease-in-out;
}
.sync-embed-fold svg {
color: var(--text-muted);
}
/* === COLLAPSED STATE === */
.sync-embed.is-collapsed {
height: auto !important;
min-height: 0 !important;
max-height: none !important;
overflow: hidden !important;
}
/* Hide everything except header when collapsed */
.sync-embed.is-collapsed .view-content > *:not(.sync-embed-alias-header) {
display: none !important;
}
/* Remove bottom border of header when collapsed */
.sync-embed.is-collapsed .sync-embed-alias-header.is-sticky {
border-bottom: none !important;
}
/* Rotate fold icon when collapsed */
.sync-embed.is-collapsed .sync-embed-fold {
transform: rotate(-90deg);
}
/* Gap between embeds */
@ -122,7 +204,6 @@
padding-right: 10px !important;
margin-bottom: 0;
pointer-events: none !important;
user-select: none !important;
cursor: default !important;
background: transparent !important;
@ -131,6 +212,11 @@
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 {