mirror of
https://github.com/sixtarocyan/smart-folder-view.git
synced 2026-07-22 06:50:05 +00:00
fix: address all required linter issues from Obsidian community review
- Replace localStorage with app.saveLocalStorage/loadLocalStorage - Replace window.confirm/prompt with custom Modal dialogs - Remove inline <style> element; move all CSS to styles.css - Replace all element.style.cssText with CSS class additions - Replace element.style.* with CSS classes and setCssProps for dynamic values - Fix command IDs to exclude plugin ID prefix - Remove detachLeavesOfType from onunload - Fix async methods/callbacks returning Promise where void expected - Add void operator to unhandled promise calls - Fix empty catch block - Remove unused variable - Fix type coercion in template literals
This commit is contained in:
parent
3a0fd4a35a
commit
eca4f285b1
3 changed files with 666 additions and 509 deletions
504
main.js
504
main.js
|
|
@ -281,7 +281,7 @@ function detectLanguage() {
|
|||
try {
|
||||
const locale = window.moment?.locale?.() || "";
|
||||
if (locale.toLowerCase().startsWith("zh")) return "zh";
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
}
|
||||
return "en";
|
||||
}
|
||||
|
|
@ -372,23 +372,23 @@ function makeSelectSearchable(selectEl, placeholder) {
|
|||
const host = selectEl?.parentElement;
|
||||
if (!host || selectEl.dataset.searchable === "1") return;
|
||||
selectEl.dataset.searchable = "1";
|
||||
selectEl.style.display = "none";
|
||||
selectEl.addClass("sfv-hidden");
|
||||
const box = host.createEl("details");
|
||||
box.style.cssText = "position:relative;display:block;z-index:1;";
|
||||
box.addClass("sfv-sel-host");
|
||||
const summary = box.createEl("summary");
|
||||
summary.style.cssText = "list-style:none;cursor:pointer;min-height:30px;padding:5px 10px;border-radius:8px;border:1px solid var(--background-modifier-border);";
|
||||
summary.addClass("sfv-sel-summary");
|
||||
const summaryText = summary.createEl("span");
|
||||
const panel = box.createEl("div");
|
||||
panel.style.cssText = "position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:60;max-height:220px;overflow:auto;padding:8px;border-radius:10px;border:1px solid var(--background-modifier-border);background:var(--background-primary);";
|
||||
panel.addClass("sfv-sel-panel");
|
||||
const input = panel.createEl("input");
|
||||
input.type = "search";
|
||||
input.placeholder = placeholder;
|
||||
input.style.cssText = "width:100%;height:28px;padding:2px 8px;border-radius:8px;border:1px solid var(--background-modifier-border);margin-bottom:6px;";
|
||||
input.addClass("sfv-sel-input");
|
||||
const list = panel.createEl("div");
|
||||
const rows = [];
|
||||
const optionTexts = Array.from(selectEl.options).map((opt) => opt.textContent || "");
|
||||
const meter = document.createElement("span");
|
||||
meter.style.cssText = "position:fixed;visibility:hidden;white-space:nowrap;font:var(--font-ui-medium);";
|
||||
meter.classList.add("sfv-sel-meter");
|
||||
document.body.appendChild(meter);
|
||||
let maxTextWidth = 0;
|
||||
for (const text of optionTexts) {
|
||||
|
|
@ -398,16 +398,12 @@ function makeSelectSearchable(selectEl, placeholder) {
|
|||
meter.remove();
|
||||
const fixedWidth = Math.max(170, Math.ceil(maxTextWidth) + 44);
|
||||
const clampedWidth = Math.min(fixedWidth, 340);
|
||||
box.style.width = "100%";
|
||||
box.style.maxWidth = `${clampedWidth}px`;
|
||||
panel.style.minWidth = `${Math.min(clampedWidth, 260)}px`;
|
||||
panel.style.maxWidth = `${clampedWidth}px`;
|
||||
obsidian.setCssProps(box, { "--sfv-sel-max-width": `${clampedWidth}px` });
|
||||
obsidian.setCssProps(panel, { "--sfv-sel-min-width": `${Math.min(clampedWidth, 260)}px`, "--sfv-sel-max-width": `${clampedWidth}px` });
|
||||
for (const opt of Array.from(selectEl.options)) {
|
||||
const row = list.createEl("button", { text: opt.textContent || "" });
|
||||
row.type = "button";
|
||||
row.style.cssText = "all:unset;display:block;width:100%;box-sizing:border-box;text-align:left;padding:5px 8px;cursor:pointer;border-radius:6px;";
|
||||
row.addEventListener("mouseenter", () => row.style.background = "var(--background-modifier-hover)");
|
||||
row.addEventListener("mouseleave", () => row.style.background = "transparent");
|
||||
row.addClass("sfv-sel-row");
|
||||
row.addEventListener("click", () => {
|
||||
selectEl.value = opt.value;
|
||||
selectEl.dispatchEvent(new Event("change"));
|
||||
|
|
@ -418,22 +414,23 @@ function makeSelectSearchable(selectEl, placeholder) {
|
|||
const refreshSummary = () => {
|
||||
const selected = selectEl.selectedOptions[0];
|
||||
summaryText.setText(selected?.textContent || "");
|
||||
summaryText.style.cssText = "display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;";
|
||||
summaryText.addClass("sfv-sel-summary-text");
|
||||
};
|
||||
input.addEventListener("input", () => {
|
||||
const q = input.value.trim().toLowerCase();
|
||||
for (const { text, row } of rows) {
|
||||
row.style.display = !q || text.includes(q) ? "block" : "none";
|
||||
if (!q || text.includes(q)) row.removeClass("sfv-hidden");
|
||||
else row.addClass("sfv-hidden");
|
||||
}
|
||||
});
|
||||
box.addEventListener("toggle", () => {
|
||||
if (box.open) {
|
||||
box.style.zIndex = "160";
|
||||
box.addClass("sfv-elevated");
|
||||
input.value = "";
|
||||
for (const { row } of rows) row.style.display = "block";
|
||||
for (const { row } of rows) row.removeClass("sfv-hidden");
|
||||
window.setTimeout(() => input.focus(), 0);
|
||||
} else {
|
||||
box.style.zIndex = "1";
|
||||
box.removeClass("sfv-elevated");
|
||||
}
|
||||
});
|
||||
selectEl.addEventListener("change", refreshSummary);
|
||||
|
|
@ -552,31 +549,35 @@ var SetupModal = class extends obsidian.Modal {
|
|||
});
|
||||
});
|
||||
const action = contentEl.createEl("div");
|
||||
action.style.cssText = "display:flex;justify-content:flex-end;gap:8px;margin-top:10px;";
|
||||
action.createEl("button", { text: this.plugin.t("preview") }).addEventListener("click", async () => {
|
||||
action.addClass("sfv-setup-action");
|
||||
action.createEl("button", { text: this.plugin.t("preview") }).addEventListener("click", () => {
|
||||
this.plugin.setDraftProfile(this.draft);
|
||||
await this.plugin.activateView();
|
||||
void this.plugin.activateView();
|
||||
this.hasPreviewedDraft = true;
|
||||
});
|
||||
const saveBtn = action.createEl("button", { text: this.plugin.t("saveAndOpen") });
|
||||
saveBtn.addClass("mod-cta");
|
||||
saveBtn.addEventListener("click", async () => {
|
||||
await this.plugin.saveProfile(this.draft, true, false);
|
||||
await this.plugin.activateView({ profile: this.draft });
|
||||
this.skipCloseSavePrompt = true;
|
||||
this.close();
|
||||
saveBtn.addEventListener("click", () => {
|
||||
void (async () => {
|
||||
await this.plugin.saveProfile(this.draft, true, false);
|
||||
await this.plugin.activateView({ profile: this.draft });
|
||||
this.skipCloseSavePrompt = true;
|
||||
this.close();
|
||||
})();
|
||||
});
|
||||
}
|
||||
onOpen() {
|
||||
this.render();
|
||||
}
|
||||
async onClose() {
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
if (this.skipCloseSavePrompt) return;
|
||||
if (!this.hasPreviewedDraft) return;
|
||||
if (!window.confirm(this.plugin.t("savePreviewOnClose"))) return;
|
||||
await this.plugin.saveProfile(this.draft, true, false);
|
||||
await this.plugin.activateView({ profile: this.draft });
|
||||
if (this.skipCloseSavePrompt || !this.hasPreviewedDraft) return;
|
||||
void askConfirm(this.app, this.plugin.t("savePreviewOnClose")).then((ok) => {
|
||||
if (!ok) return;
|
||||
void this.plugin.saveProfile(this.draft, true, false).then(
|
||||
() => void this.plugin.activateView({ profile: this.draft })
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
var TextInputModal = class extends obsidian.Modal {
|
||||
|
|
@ -592,14 +593,14 @@ var TextInputModal = class extends obsidian.Modal {
|
|||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h3", { text: this.titleText });
|
||||
if (this.hintText) contentEl.createEl("p", { text: this.hintText }).style.cssText = "margin:6px 0;color:var(--text-muted);font-size:0.9em;";
|
||||
if (this.hintText) contentEl.createEl("p", { text: this.hintText }).addClass("sfv-modal-hint");
|
||||
const input = contentEl.createEl("input");
|
||||
input.type = "text";
|
||||
input.placeholder = this.placeholderText;
|
||||
input.value = this.initialValue;
|
||||
input.style.cssText = "width:100%;margin-top:6px;";
|
||||
input.addClass("sfv-modal-input");
|
||||
const action = contentEl.createEl("div");
|
||||
action.style.cssText = "display:flex;justify-content:flex-end;gap:8px;margin-top:12px;";
|
||||
action.addClass("sfv-modal-action");
|
||||
action.createEl("button", { text: "Cancel" }).addEventListener("click", () => {
|
||||
this.onSubmitValue(null);
|
||||
this.close();
|
||||
|
|
@ -640,9 +641,9 @@ var ThreeChoiceModal = class extends obsidian.Modal {
|
|||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h3", { text: this.titleText });
|
||||
contentEl.createEl("p", { text: this.messageText }).style.cssText = "margin:6px 0 10px;color:var(--text-muted);font-size:0.9em;";
|
||||
contentEl.createEl("p", { text: this.messageText }).addClass("sfv-modal-hint");
|
||||
const action = contentEl.createEl("div");
|
||||
action.style.cssText = "display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;";
|
||||
action.addClass("sfv-modal-action");
|
||||
const addChoice = (key, label, isCta = false) => {
|
||||
if (!label) return;
|
||||
const btn = action.createEl("button", { text: label });
|
||||
|
|
@ -675,9 +676,9 @@ var StartActionModal = class extends obsidian.Modal {
|
|||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h3", { text: this.plugin.t("startActionTitle") });
|
||||
contentEl.createEl("p", { text: this.plugin.t("startActionDesc") }).style.cssText = "margin:6px 0 10px;color:var(--text-muted);font-size:0.9em;";
|
||||
contentEl.createEl("p", { text: this.plugin.t("startActionDesc") }).addClass("sfv-modal-hint");
|
||||
const action = contentEl.createEl("div");
|
||||
action.style.cssText = "display:flex;justify-content:flex-end;gap:8px;";
|
||||
action.addClass("sfv-modal-action");
|
||||
action.createEl("button", { text: this.plugin.t("startOpenPage") }).addEventListener("click", () => {
|
||||
this.onSubmitValue("open");
|
||||
this.close();
|
||||
|
|
@ -710,16 +711,16 @@ var FolderSelectModal = class extends obsidian.Modal {
|
|||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h3", { text: this.plugin.t("exportFolderTitle") });
|
||||
contentEl.createEl("p", { text: this.plugin.t("exportFolderDesc") }).style.cssText = "margin:6px 0 10px;color:var(--text-muted);font-size:0.9em;";
|
||||
contentEl.createEl("p", { text: this.plugin.t("exportFolderDesc") }).addClass("sfv-modal-hint");
|
||||
const select = contentEl.createEl("select");
|
||||
select.style.cssText = "width:100%;";
|
||||
select.addClass("sfv-modal-select");
|
||||
for (const f of this.folders) {
|
||||
select.createEl("option", { value: f, text: f || this.plugin.t("rootFolder") });
|
||||
}
|
||||
select.value = this.initialValue;
|
||||
makeSelectSearchable(select, this.plugin.t("search"));
|
||||
const action = contentEl.createEl("div");
|
||||
action.style.cssText = "display:flex;justify-content:flex-end;gap:8px;margin-top:12px;";
|
||||
action.addClass("sfv-modal-action");
|
||||
action.createEl("button", { text: "Cancel" }).addEventListener("click", () => {
|
||||
this.onSubmitValue(null);
|
||||
this.close();
|
||||
|
|
@ -740,6 +741,38 @@ function askFolderSelect(app, plugin, folders, initialValue) {
|
|||
new FolderSelectModal(app, plugin, folders, initialValue, resolve).open();
|
||||
});
|
||||
}
|
||||
var ConfirmModal = class extends obsidian.Modal {
|
||||
constructor(app, message, onResult) {
|
||||
super(app);
|
||||
this.message = message;
|
||||
this.onResultValue = onResult;
|
||||
}
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("p", { text: this.message });
|
||||
const action = contentEl.createEl("div");
|
||||
action.addClass("sfv-modal-action");
|
||||
action.createEl("button", { text: "Cancel" }).addEventListener("click", () => {
|
||||
this.onResultValue(false);
|
||||
this.close();
|
||||
});
|
||||
const ok = action.createEl("button", { text: "OK" });
|
||||
ok.addClass("mod-cta");
|
||||
ok.addEventListener("click", () => {
|
||||
this.onResultValue(true);
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
};
|
||||
function askConfirm(app, message) {
|
||||
return new Promise((resolve) => {
|
||||
new ConfirmModal(app, message, resolve).open();
|
||||
});
|
||||
}
|
||||
var ProfileManagerModal = class extends obsidian.Modal {
|
||||
constructor(app, plugin) {
|
||||
super(app);
|
||||
|
|
@ -752,21 +785,25 @@ var ProfileManagerModal = class extends obsidian.Modal {
|
|||
for (const profile of this.plugin.data.profiles) {
|
||||
const s = new obsidian.Setting(contentEl).setName(profile.name || profile.sourceFolder).setDesc(profile.sourceFolder);
|
||||
s.addButton((b) => b.setButtonText(this.plugin.t("open")).onClick(async () => {
|
||||
this.plugin.clearDraftProfile();
|
||||
this.plugin.data.lastActiveProfileId = profile.id;
|
||||
await this.plugin.persist();
|
||||
await this.plugin.activateView({ profile, newLeaf: true });
|
||||
this.close();
|
||||
void (async () => {
|
||||
this.plugin.clearDraftProfile();
|
||||
this.plugin.data.lastActiveProfileId = profile.id;
|
||||
await this.plugin.persist();
|
||||
await this.plugin.activateView({ profile, newLeaf: true });
|
||||
this.close();
|
||||
})();
|
||||
}));
|
||||
s.addButton((b) => b.setButtonText(this.plugin.t("edit")).onClick(() => {
|
||||
new SetupModal(this.app, this.plugin, profile).open();
|
||||
}));
|
||||
s.addButton((b) => b.setButtonText(this.plugin.t("delete")).onClick(async () => {
|
||||
if (!window.confirm(this.plugin.t("deleteConfirm", { name: profile.name || profile.sourceFolder }))) return;
|
||||
this.plugin.data.profiles = this.plugin.data.profiles.filter((p) => p.id !== profile.id);
|
||||
if (this.plugin.data.lastActiveProfileId === profile.id) this.plugin.data.lastActiveProfileId = "";
|
||||
await this.plugin.persist();
|
||||
this.onOpen();
|
||||
s.addButton((b) => b.setButtonText(this.plugin.t("delete")).onClick(() => {
|
||||
void askConfirm(this.app, this.plugin.t("deleteConfirm", { name: profile.name || profile.sourceFolder })).then(async (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
this.plugin.data.profiles = this.plugin.data.profiles.filter((p) => p.id !== profile.id);
|
||||
if (this.plugin.data.lastActiveProfileId === profile.id) this.plugin.data.lastActiveProfileId = "";
|
||||
await this.plugin.persist();
|
||||
this.onOpen();
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -825,7 +862,7 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
this.stateOwnerId = ownerId;
|
||||
this.profileDefaultsKey = nextDefaultsKey;
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(FILTER_STORAGE_PREFIX + ownerId) || "{}");
|
||||
const saved = this.app.loadLocalStorage(FILTER_STORAGE_PREFIX + ownerId) || {};
|
||||
this.isFreshFilterState = !saved || Object.keys(saved).length === 0;
|
||||
const savedDefaultsKey = typeof saved.profileDefaultsKey === "string" ? saved.profileDefaultsKey : "";
|
||||
const defaultsChanged = !!savedDefaultsKey && savedDefaultsKey !== this.profileDefaultsKey;
|
||||
|
|
@ -842,14 +879,14 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
}
|
||||
}
|
||||
saveState() {
|
||||
localStorage.setItem(FILTER_STORAGE_PREFIX + this.stateOwnerId, JSON.stringify({
|
||||
this.app.saveLocalStorage(FILTER_STORAGE_PREFIX + this.stateOwnerId, {
|
||||
...this.filterState,
|
||||
profileDefaultsKey: this.profileDefaultsKey
|
||||
}));
|
||||
});
|
||||
}
|
||||
loadPresets() {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(FILTER_PRESET_STORAGE_PREFIX + this.stateOwnerId) || "[]");
|
||||
const parsed = this.app.loadLocalStorage(FILTER_PRESET_STORAGE_PREFIX + this.stateOwnerId);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return clonePresetList(parsed);
|
||||
} catch {
|
||||
|
|
@ -857,7 +894,7 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
}
|
||||
}
|
||||
savePresets(presets) {
|
||||
localStorage.setItem(FILTER_PRESET_STORAGE_PREFIX + this.stateOwnerId, JSON.stringify(clonePresetList(presets)));
|
||||
this.app.saveLocalStorage(FILTER_PRESET_STORAGE_PREFIX + this.stateOwnerId, clonePresetList(presets));
|
||||
}
|
||||
contextKey(profile) {
|
||||
return JSON.stringify({ profileId: profile.id, selectedByAttr: this.filterState.selectedByAttr, enabled: this.filterState.enabledFilters, sort: this.filterState.sortOrder, viewMode: this.filterState.viewMode, boardField: profile.boardField });
|
||||
|
|
@ -867,14 +904,14 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
}
|
||||
loadOrderStore(ownerId) {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem(MANUAL_ORDER_STORAGE_PREFIX + ownerId) || "{}");
|
||||
return parsed && typeof parsed === "object" ? parsed : {};
|
||||
const parsed = this.app.loadLocalStorage(MANUAL_ORDER_STORAGE_PREFIX + ownerId);
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
saveOrderStore(ownerId, store) {
|
||||
localStorage.setItem(MANUAL_ORDER_STORAGE_PREFIX + ownerId, JSON.stringify(store));
|
||||
this.app.saveLocalStorage(MANUAL_ORDER_STORAGE_PREFIX + ownerId, store);
|
||||
}
|
||||
applyOrder(ownerId, key, items) {
|
||||
const store = this.loadOrderStore(ownerId);
|
||||
|
|
@ -912,8 +949,8 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
await this.rerenderPreserveScroll();
|
||||
new obsidian.Notice(this.plugin.t("undoDone"));
|
||||
}
|
||||
async onOpen() {
|
||||
await this.renderView();
|
||||
onOpen() {
|
||||
this.renderView();
|
||||
}
|
||||
async rerenderPreserveScroll() {
|
||||
const container = this.containerEl.children[1];
|
||||
|
|
@ -925,8 +962,8 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
}
|
||||
async askAndMaybeUpdateField(file, field, targetColumn, unsetLabel) {
|
||||
const suggested = targetColumn === unsetLabel ? "" : targetColumn;
|
||||
const useSuggested = window.confirm(this.plugin.t("moveAsk", { field, value: suggested || this.plugin.t("emptyValue") }));
|
||||
const finalValue = useSuggested ? suggested : window.prompt(this.plugin.t("moveManual", { field }), suggested);
|
||||
const useSuggested = await askConfirm(this.app, this.plugin.t("moveAsk", { field, value: suggested || this.plugin.t("emptyValue") }));
|
||||
const finalValue = useSuggested ? suggested : await askTextInput(this.app, this.plugin.t("moveManual", { field }), this.plugin.t("moveManual", { field }), suggested, "");
|
||||
if (finalValue == null) return void 0;
|
||||
const value = finalValue.trim();
|
||||
try {
|
||||
|
|
@ -961,21 +998,22 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
}
|
||||
renderCard(parent, file, cache, profile, color, showDot) {
|
||||
const card = parent.createEl("section");
|
||||
card.style.cssText = `position:relative;margin:0 0 12px;padding:12px 14px;border:1px solid var(--background-modifier-border);border-left:4px solid ${color};border-radius:12px;background:color-mix(in srgb, ${color} 9%, var(--background-primary) 91%);`;
|
||||
card.addClass("sfv-card");
|
||||
obsidian.setCssProps(card, { "--sfv-color": color });
|
||||
if (showDot) {
|
||||
card.createEl("span").style.cssText = `position:absolute;left:-26px;top:17px;width:12px;height:12px;border-radius:50%;background:${color};border:2px solid var(--background-primary);`;
|
||||
card.createEl("span").addClass("sfv-card-dot");
|
||||
}
|
||||
const top = card.createEl("div");
|
||||
top.style.cssText = "display:flex;align-items:center;gap:10px;overflow:hidden;margin-bottom:5px;";
|
||||
top.addClass("sfv-card-top");
|
||||
const meta = top.createEl("span");
|
||||
meta.setText(profile.displayFields.map((k) => `${k}: ${getFrontmatterValues(cache, k).join("\u3001") || "-"}`).join(" | "));
|
||||
meta.style.cssText = "font-size:0.85em;color:var(--text-muted);white-space:nowrap;overflow-x:auto;flex:1 1 auto;min-width:100px;";
|
||||
meta.addClass("sfv-card-meta");
|
||||
const link = top.createEl("a");
|
||||
link.setText(file.basename);
|
||||
link.className = "internal-link";
|
||||
link.setAttribute("data-href", file.path);
|
||||
link.setAttribute("href", file.path);
|
||||
link.style.cssText = "font-size:1em;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:0 1 220px;min-width:80px;";
|
||||
link.addClass("sfv-card-link");
|
||||
const triggerHoverPreview = (e) => {
|
||||
this.app.workspace?.trigger?.("hover-link", {
|
||||
event: e,
|
||||
|
|
@ -1012,7 +1050,6 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
};
|
||||
const bodyEl = card.createEl("div");
|
||||
bodyEl.addClass("sfv-card-body");
|
||||
bodyEl.style.cssText = "max-height:calc(var(--line-height-normal) * 1em * 3);overflow-y:auto;overflow-x:hidden;padding-right:4px;";
|
||||
bodyEl.addEventListener("mouseover", (e) => {
|
||||
const el = getInternalLinkElement(e.target);
|
||||
if (!el) return;
|
||||
|
|
@ -1048,16 +1085,16 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
}
|
||||
buildDropdown(container, label, options, selectedValues, onChange, onCommit) {
|
||||
const wrap = container.createEl("div");
|
||||
wrap.style.cssText = "display:flex;align-items:flex-start;gap:8px;flex:1 1 420px;min-width:320px;max-width:420px;position:relative;z-index:1;";
|
||||
wrap.createEl("span", { text: label }).style.cssText = "font-size:1em;color:var(--text-normal);min-width:72px;padding-top:8px;white-space:nowrap;";
|
||||
wrap.addClass("sfv-dd-wrap");
|
||||
wrap.createEl("span", { text: label }).addClass("sfv-dd-label");
|
||||
const box = wrap.createEl("details");
|
||||
box.style.cssText = "flex:1;position:relative;z-index:1;min-width:0;";
|
||||
box.addClass("sfv-dd-box");
|
||||
const summary = box.createEl("summary");
|
||||
summary.style.cssText = "list-style:none;cursor:pointer;height:36px;padding:7px 34px 7px 12px;border-radius:10px;border:1px solid var(--background-modifier-border);font-size:1em;box-sizing:border-box;overflow:hidden;";
|
||||
summary.addClass("sfv-dd-summary");
|
||||
const initialValues = [...selectedValues];
|
||||
let currentValues = [...initialValues];
|
||||
const summarySpan = summary.createEl("span");
|
||||
summarySpan.style.cssText = "color:var(--text-normal);display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
|
||||
summarySpan.addClass("sfv-dd-summary-text");
|
||||
const toDisplayText = (v) => v === EMPTY_OPTION_VALUE ? this.plugin.t("emptyValue") : v;
|
||||
const refreshSummary = () => {
|
||||
const isAll = currentValues.length === options.length && options.length > 0;
|
||||
|
|
@ -1068,11 +1105,11 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
};
|
||||
refreshSummary();
|
||||
const panel = box.createEl("div");
|
||||
panel.style.cssText = "position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:30;max-height:260px;overflow:auto;padding:8px;border-radius:10px;border:1px solid var(--background-modifier-border);background:var(--background-primary);";
|
||||
panel.addClass("sfv-dd-panel");
|
||||
const actionBar = panel.createEl("div");
|
||||
actionBar.style.cssText = "display:flex;gap:10px;align-items:center;margin-bottom:4px;";
|
||||
actionBar.addClass("sfv-dd-action-bar");
|
||||
const allBtn = actionBar.createEl("button", { text: this.plugin.t("selectAll") });
|
||||
allBtn.style.cssText = "border:none;background:transparent;color:var(--interactive-accent);cursor:pointer;";
|
||||
allBtn.addClass("sfv-dd-action-btn");
|
||||
allBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -1085,7 +1122,7 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
refreshSummary();
|
||||
});
|
||||
const clearBtn = actionBar.createEl("button", { text: this.plugin.t("selectNone") });
|
||||
clearBtn.style.cssText = "border:none;background:transparent;color:var(--interactive-accent);cursor:pointer;";
|
||||
clearBtn.addClass("sfv-dd-action-btn");
|
||||
clearBtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -1100,12 +1137,12 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
const search = panel.createEl("input");
|
||||
search.type = "search";
|
||||
search.placeholder = this.plugin.t("search");
|
||||
search.style.cssText = "width:100%;height:32px;padding:4px 8px;border-radius:8px;border:1px solid var(--background-modifier-border);margin:6px 0;";
|
||||
search.addClass("sfv-dd-search");
|
||||
const list = panel.createEl("div");
|
||||
const rows = [];
|
||||
for (const option of options) {
|
||||
const row = list.createEl("label");
|
||||
row.style.cssText = "display:flex;align-items:center;gap:6px;padding:4px;font-size:1em;";
|
||||
row.addClass("sfv-dd-row");
|
||||
const cb = row.createEl("input");
|
||||
cb.type = "checkbox";
|
||||
cb.checked = currentValues.includes(option);
|
||||
|
|
@ -1120,34 +1157,35 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
}
|
||||
search.addEventListener("input", () => {
|
||||
const q = search.value.toLowerCase();
|
||||
for (const { option, row } of rows) row.style.display = !q || option.toLowerCase().includes(q) ? "flex" : "none";
|
||||
for (const { option, row } of rows) {
|
||||
if (!q || option.toLowerCase().includes(q)) row.removeClass("sfv-hidden");
|
||||
else row.addClass("sfv-hidden");
|
||||
}
|
||||
});
|
||||
box.addEventListener("toggle", () => {
|
||||
if (box.open) {
|
||||
wrap.style.zIndex = "170";
|
||||
box.style.zIndex = "170";
|
||||
wrap.addClass("sfv-elevated");
|
||||
box.addClass("sfv-elevated");
|
||||
} else {
|
||||
wrap.style.zIndex = "1";
|
||||
box.style.zIndex = "1";
|
||||
wrap.removeClass("sfv-elevated");
|
||||
box.removeClass("sfv-elevated");
|
||||
if (onCommit) onCommit();
|
||||
}
|
||||
});
|
||||
}
|
||||
async renderView() {
|
||||
renderView() {
|
||||
const profile = this.getRenderProfile();
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
container.classList.add("smart-folder-view-root");
|
||||
this.updateLeafTitle();
|
||||
container.style.padding = "16px 18px";
|
||||
container.style.overflowY = "auto";
|
||||
if (!profile) {
|
||||
container.createEl("p", { text: this.plugin.t("noProfile") });
|
||||
return;
|
||||
}
|
||||
this.ensureState(profile.id, profile.sortOrder, profile.viewMode);
|
||||
const topBar = container.createEl("div");
|
||||
topBar.style.cssText = "display:flex;justify-content:flex-start;gap:8px;margin-bottom:10px;";
|
||||
topBar.addClass("sfv-top-bar");
|
||||
const saveBtn = topBar.createEl("button", { text: this.plugin.isDraft(profile.id) ? this.plugin.t("savePage") : this.plugin.t("updatePage") });
|
||||
saveBtn.addClass("mod-cta");
|
||||
saveBtn.addEventListener("click", async () => {
|
||||
|
|
@ -1160,11 +1198,11 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
});
|
||||
topBar.createEl("button", { text: this.plugin.t("openBuilderBtn") }).addEventListener("click", () => new SetupModal(this.app, this.plugin).open());
|
||||
topBar.createEl("button", { text: this.plugin.t("openManagerBtn") }).addEventListener("click", () => new ProfileManagerModal(this.app, this.plugin).open());
|
||||
topBar.createEl("button", { text: this.plugin.t("exportPage") }).addEventListener("click", async () => {
|
||||
await this.plugin.exportProfileToMarkdown(profile);
|
||||
topBar.createEl("button", { text: this.plugin.t("exportPage") }).addEventListener("click", () => {
|
||||
void this.plugin.exportProfileToMarkdown(profile);
|
||||
});
|
||||
topBar.createEl("button", { text: this.plugin.t("refreshPreview") }).addEventListener("click", async () => {
|
||||
await this.renderView();
|
||||
topBar.createEl("button", { text: this.plugin.t("refreshPreview") }).addEventListener("click", () => {
|
||||
this.renderView();
|
||||
new obsidian.Notice(this.plugin.t("refreshPreviewDone"));
|
||||
});
|
||||
const files = this.plugin.getFilesByFolder(profile.sourceFolder);
|
||||
|
|
@ -1193,11 +1231,11 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
}
|
||||
if (defaultSelectionInitialized) this.saveState();
|
||||
const switchesEl = container.createEl("div");
|
||||
switchesEl.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;";
|
||||
switchesEl.addClass("sfv-switches");
|
||||
for (const key of attrKeys) {
|
||||
const enabled = this.filterState.enabledFilters.includes(key);
|
||||
const chip = switchesEl.createEl("label");
|
||||
chip.style.cssText = "display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;border:1px solid var(--background-modifier-border);font-size:1em;color:var(--text-normal);";
|
||||
chip.addClass("sfv-chip");
|
||||
const cb = chip.createEl("input");
|
||||
cb.type = "checkbox";
|
||||
cb.checked = enabled;
|
||||
|
|
@ -1208,16 +1246,16 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
else s.delete(key);
|
||||
this.filterState.enabledFilters = attrKeys.filter((k) => s.has(k));
|
||||
this.saveState();
|
||||
this.renderView();
|
||||
void this.renderView();
|
||||
});
|
||||
}
|
||||
const controls = container.createEl("div");
|
||||
controls.style.cssText = "display:flex;flex-direction:column;gap:8px;margin-bottom:12px;position:relative;z-index:120;overflow:visible;";
|
||||
controls.createEl("p", { text: this.plugin.t("presetScopeDesc") }).style.cssText = "margin:0;color:var(--text-muted);font-size:0.85em;";
|
||||
controls.addClass("sfv-controls");
|
||||
controls.createEl("p", { text: this.plugin.t("presetScopeDesc") }).addClass("sfv-controls-hint");
|
||||
const presets = this.loadPresets();
|
||||
const presetWrap = controls.createEl("div");
|
||||
presetWrap.style.cssText = "display:flex;gap:8px;align-items:center;";
|
||||
presetWrap.createEl("span", { text: this.plugin.t("preset") }).style.cssText = "min-width:72px;color:var(--text-normal);";
|
||||
presetWrap.addClass("sfv-preset-wrap");
|
||||
presetWrap.createEl("span", { text: this.plugin.t("preset") }).addClass("sfv-dd-label");
|
||||
const presetSel = presetWrap.createEl("select");
|
||||
presetSel.createEl("option", { text: this.plugin.t("noPreset"), value: "" });
|
||||
for (const p of presets) {
|
||||
|
|
@ -1237,22 +1275,24 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
this.filterState.presetName = p.name;
|
||||
}
|
||||
this.saveState();
|
||||
this.renderView();
|
||||
void this.renderView();
|
||||
});
|
||||
makeSelectSearchable(presetSel, this.plugin.t("search"));
|
||||
presetWrap.createEl("button", { text: this.plugin.t("savePreset") }).addEventListener("click", async () => {
|
||||
const name = (await askTextInput(this.app, this.plugin.t("savePreset"), this.plugin.t("presetPrompt"), this.filterState.presetName || "", this.plugin.t("presetSaveHint")) || "").trim();
|
||||
if (!name) {
|
||||
new obsidian.Notice(this.plugin.t("presetNameRequired"));
|
||||
return;
|
||||
}
|
||||
const currentPresets = this.loadPresets();
|
||||
const next = currentPresets.filter((p) => p.name !== name);
|
||||
next.push(clonePreset({ name, enabledFilters: [...this.filterState.enabledFilters], selectedByAttr: cloneSelectedByAttr(this.filterState.selectedByAttr), sortOrder: this.filterState.sortOrder, viewMode: this.filterState.viewMode }));
|
||||
this.savePresets(next);
|
||||
this.filterState.presetName = name;
|
||||
this.saveState();
|
||||
await this.renderView();
|
||||
presetWrap.createEl("button", { text: this.plugin.t("savePreset") }).addEventListener("click", () => {
|
||||
void (async () => {
|
||||
const name = (await askTextInput(this.app, this.plugin.t("savePreset"), this.plugin.t("presetPrompt"), this.filterState.presetName || "", this.plugin.t("presetSaveHint")) || "").trim();
|
||||
if (!name) {
|
||||
new obsidian.Notice(this.plugin.t("presetNameRequired"));
|
||||
return;
|
||||
}
|
||||
const currentPresets = this.loadPresets();
|
||||
const next = currentPresets.filter((p) => p.name !== name);
|
||||
next.push(clonePreset({ name, enabledFilters: [...this.filterState.enabledFilters], selectedByAttr: cloneSelectedByAttr(this.filterState.selectedByAttr), sortOrder: this.filterState.sortOrder, viewMode: this.filterState.viewMode }));
|
||||
this.savePresets(next);
|
||||
this.filterState.presetName = name;
|
||||
this.saveState();
|
||||
this.renderView();
|
||||
})();
|
||||
});
|
||||
presetWrap.createEl("button", { text: this.plugin.t("delPreset") }).addEventListener("click", () => {
|
||||
if (!this.filterState.presetName) return;
|
||||
|
|
@ -1260,10 +1300,10 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
this.savePresets(currentPresets.filter((p) => p.name !== this.filterState.presetName));
|
||||
this.filterState.presetName = "";
|
||||
this.saveState();
|
||||
this.renderView();
|
||||
void this.renderView();
|
||||
});
|
||||
const filterGrid = controls.createEl("div");
|
||||
filterGrid.style.cssText = "display:flex;flex-wrap:wrap;gap:8px 10px;align-items:flex-start;";
|
||||
filterGrid.addClass("sfv-filter-grid");
|
||||
for (const key of this.filterState.enabledFilters) {
|
||||
this.buildDropdown(filterGrid, key, optionsByAttr[key] || [], this.filterState.selectedByAttr[key] || [], (vals) => {
|
||||
this.filterState.selectedByAttr[key] = [...vals];
|
||||
|
|
@ -1274,8 +1314,8 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
}
|
||||
this.isFreshFilterState = false;
|
||||
const sortWrap = controls.createEl("label");
|
||||
sortWrap.style.cssText = "display:flex;gap:8px;align-items:center;";
|
||||
sortWrap.createEl("span", { text: this.plugin.t("sortOrder") }).style.cssText = "min-width:72px;color:var(--text-normal);";
|
||||
sortWrap.addClass("sfv-sort-wrap");
|
||||
sortWrap.createEl("span", { text: this.plugin.t("sortOrder") }).addClass("sfv-dd-label");
|
||||
const sortSel = sortWrap.createEl("select");
|
||||
sortSel.createEl("option", { value: "desc", text: this.plugin.t("desc") });
|
||||
sortSel.createEl("option", { value: "asc", text: this.plugin.t("asc") });
|
||||
|
|
@ -1285,39 +1325,41 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
sortSel.addEventListener("change", () => {
|
||||
this.filterState.sortOrder = sortSel.value;
|
||||
this.saveState();
|
||||
this.renderView();
|
||||
void this.renderView();
|
||||
});
|
||||
const modeWrap = controls.createEl("label");
|
||||
modeWrap.style.cssText = "display:flex;gap:8px;align-items:center;";
|
||||
modeWrap.createEl("span", { text: this.plugin.t("viewMode") }).style.cssText = "min-width:72px;color:var(--text-normal);";
|
||||
modeWrap.addClass("sfv-sort-wrap");
|
||||
modeWrap.createEl("span", { text: this.plugin.t("viewMode") }).addClass("sfv-dd-label");
|
||||
const modeSel = modeWrap.createEl("select");
|
||||
modeSel.createEl("option", { value: "timeline", text: this.plugin.t("timeline") });
|
||||
modeSel.createEl("option", { value: "board", text: this.plugin.t("board") });
|
||||
modeSel.value = this.filterState.viewMode;
|
||||
makeSelectSearchable(modeSel, this.plugin.t("search"));
|
||||
modeSel.dispatchEvent(new Event("change"));
|
||||
modeSel.addEventListener("change", async () => {
|
||||
modeSel.addEventListener("change", () => {
|
||||
this.filterState.viewMode = modeSel.value === "board" ? "board" : "timeline";
|
||||
this.saveState();
|
||||
this.renderView();
|
||||
void this.renderView();
|
||||
});
|
||||
if (this.filterState.viewMode === "board") {
|
||||
const boardFieldWrap = controls.createEl("label");
|
||||
boardFieldWrap.style.cssText = "display:flex;gap:8px;align-items:center;";
|
||||
boardFieldWrap.createEl("span", { text: this.plugin.t("boardField") }).style.cssText = "min-width:72px;color:var(--text-normal);";
|
||||
boardFieldWrap.addClass("sfv-sort-wrap");
|
||||
boardFieldWrap.createEl("span", { text: this.plugin.t("boardField") }).addClass("sfv-dd-label");
|
||||
const boardFieldSel = boardFieldWrap.createEl("select");
|
||||
boardFieldSel.createEl("option", { value: "", text: this.plugin.t("choose") });
|
||||
for (const k of attrKeys) boardFieldSel.createEl("option", { value: k, text: k });
|
||||
boardFieldSel.value = profile.boardField || "";
|
||||
makeSelectSearchable(boardFieldSel, this.plugin.t("search"));
|
||||
boardFieldSel.addEventListener("change", async () => {
|
||||
const next = boardFieldSel.value;
|
||||
profile.boardField = next;
|
||||
const idx = this.plugin.data.profiles.findIndex((p) => p.id === profile.id);
|
||||
if (idx >= 0) this.plugin.data.profiles[idx].boardField = next;
|
||||
if (this.plugin.draftProfile?.id === profile.id) this.plugin.draftProfile.boardField = next;
|
||||
await this.plugin.persist();
|
||||
await this.renderView();
|
||||
boardFieldSel.addEventListener("change", () => {
|
||||
void (async () => {
|
||||
const next = boardFieldSel.value;
|
||||
profile.boardField = next;
|
||||
const idx = this.plugin.data.profiles.findIndex((p) => p.id === profile.id);
|
||||
if (idx >= 0) this.plugin.data.profiles[idx].boardField = next;
|
||||
if (this.plugin.draftProfile?.id === profile.id) this.plugin.draftProfile.boardField = next;
|
||||
await this.plugin.persist();
|
||||
this.renderView();
|
||||
})();
|
||||
});
|
||||
}
|
||||
const filtered = entries.filter(({ file, cache }) => {
|
||||
|
|
@ -1340,7 +1382,7 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
const cmp = pa.kind === "number" && pb.kind === "number" ? Number(pa.value) - Number(pb.value) : String(pa.raw).localeCompare(String(pb.raw), "zh-Hans-CN", { numeric: true, sensitivity: "base" });
|
||||
return this.filterState.sortOrder === "asc" ? cmp : -cmp;
|
||||
});
|
||||
container.createEl("p", { text: this.plugin.t("matched", { count: filtered.length }) }).style.cssText = "color:var(--text-muted);margin:0 0 10px;";
|
||||
container.createEl("p", { text: this.plugin.t("matched", { count: filtered.length }) }).addClass("sfv-matched");
|
||||
if (!filtered.length) {
|
||||
container.createEl("p", { text: this.plugin.t("noMatch") });
|
||||
return;
|
||||
|
|
@ -1348,7 +1390,7 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
if (this.filterState.viewMode === "timeline") {
|
||||
const ordered = this.applyOrder(profile.id, this.orderKey(profile), filtered);
|
||||
const orderActions2 = container.createEl("div");
|
||||
orderActions2.style.cssText = "display:flex;gap:8px;align-items:center;margin:0 0 10px;";
|
||||
orderActions2.addClass("sfv-order-actions");
|
||||
orderActions2.createEl("button", { text: this.plugin.t("saveOrder") }).addEventListener("click", () => {
|
||||
this.saveOrder(profile.id, this.orderKey(profile), ordered.map((e) => e.file));
|
||||
new obsidian.Notice(this.plugin.t("saveOrderDone"));
|
||||
|
|
@ -1357,27 +1399,27 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
this.recordUndoSnapshot(profile);
|
||||
this.clearOrder(profile.id, this.orderKey(profile));
|
||||
new obsidian.Notice(this.plugin.t("resetOrderDone"));
|
||||
await this.renderView();
|
||||
await this.rerenderPreserveScroll();
|
||||
});
|
||||
orderActions2.createEl("button", { text: this.plugin.t("undoLastAction") }).addEventListener("click", async () => {
|
||||
await this.undoLastAction(profile);
|
||||
});
|
||||
const timelineEl = container.createEl("div");
|
||||
timelineEl.style.cssText = "position:relative;padding-left:34px;margin-top:8px;";
|
||||
timelineEl.addClass("sfv-timeline");
|
||||
const lineEl = timelineEl.createEl("div");
|
||||
lineEl.style.cssText = "position:absolute;left:16px;top:0;bottom:0;width:2px;background:var(--background-modifier-border);";
|
||||
lineEl.addClass("sfv-timeline-line");
|
||||
const indicator = timelineEl.createEl("div");
|
||||
indicator.style.cssText = "position:absolute;left:22px;right:0;height:4px;border-radius:6px;background:var(--interactive-accent);opacity:0;pointer-events:none;z-index:40;transition:opacity 80ms ease;";
|
||||
indicator.addClass("sfv-timeline-indicator");
|
||||
let dropBeforePath = null;
|
||||
const hideIndicator = () => {
|
||||
indicator.style.opacity = "0";
|
||||
indicator.removeClass("sfv-visible");
|
||||
};
|
||||
const updateDropIndicator = (clientY) => {
|
||||
const cards = Array.from(timelineEl.querySelectorAll("section[data-sfv-path]")).filter((el) => !el.classList.contains("sfv-dragging"));
|
||||
if (!cards.length) {
|
||||
dropBeforePath = null;
|
||||
indicator.style.top = "0px";
|
||||
indicator.style.opacity = "1";
|
||||
obsidian.setCssProps(indicator, { "--sfv-top": "0px" });
|
||||
indicator.addClass("sfv-visible");
|
||||
return;
|
||||
}
|
||||
let beforeCard = null;
|
||||
|
|
@ -1396,8 +1438,8 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
const last = cards[cards.length - 1];
|
||||
top = last.offsetTop + last.offsetHeight - 2;
|
||||
}
|
||||
indicator.style.top = `${Math.max(0, top)}px`;
|
||||
indicator.style.opacity = "1";
|
||||
obsidian.setCssProps(indicator, { "--sfv-top": `${Math.max(0, top)}px` });
|
||||
indicator.addClass("sfv-visible");
|
||||
};
|
||||
timelineEl.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -1462,7 +1504,7 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
this.timelineDropBusy = false;
|
||||
}
|
||||
};
|
||||
timelineEl.addEventListener("drop", async (e) => {
|
||||
timelineEl.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
hideIndicator();
|
||||
const payload = e.dataTransfer?.getData("text/plain") || "";
|
||||
|
|
@ -1471,7 +1513,7 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
if (obj.token && this.timelineDragToken && obj.token !== this.timelineDragToken) return;
|
||||
if (!obj.path || obj.from !== "timeline") return;
|
||||
const beforePath = dropBeforePath;
|
||||
await handleTimelineDrop(obj.path, beforePath);
|
||||
void handleTimelineDrop(obj.path, beforePath);
|
||||
});
|
||||
for (const { file, cache } of ordered) {
|
||||
const color = colorForValue(getFrontmatterValues(cache, profile.colorField)[0] || null, this.plugin.data.colorConfig) || this.plugin.data.colorConfig.fallbackColor;
|
||||
|
|
@ -1509,7 +1551,7 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
}
|
||||
const columns = uniqueSorted([...grouped.keys()]);
|
||||
const orderActions = container.createEl("div");
|
||||
orderActions.style.cssText = "display:flex;gap:8px;align-items:center;margin:0 0 10px;";
|
||||
orderActions.addClass("sfv-order-actions");
|
||||
orderActions.createEl("button", { text: this.plugin.t("saveOrder") }).addEventListener("click", () => {
|
||||
for (const col of columns) {
|
||||
const arr = grouped.get(col) || [];
|
||||
|
|
@ -1527,13 +1569,12 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
await this.undoLastAction(profile);
|
||||
});
|
||||
const boardEl = container.createEl("div");
|
||||
boardEl.style.cssText = "display:flex;gap:12px;align-items:flex-start;overflow-x:auto;margin-top:2px;padding-top:2px;";
|
||||
boardEl.addClass("sfv-board");
|
||||
const handleDrop = async (payload, to, beforePath) => {
|
||||
const fromArr = grouped.get(payload.from) || [];
|
||||
const toArr = grouped.get(to) || [];
|
||||
const idx = fromArr.findIndex((e) => e.file.path === payload.path);
|
||||
if (idx < 0) return;
|
||||
const moved = fromArr[idx];
|
||||
let action = "order";
|
||||
let nextBoardValue = void 0;
|
||||
if (payload.from !== to && profile.boardField) {
|
||||
|
|
@ -1574,15 +1615,15 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
const ordered = this.applyOrder(profile.id, this.orderKey(profile, col), grouped.get(col) || []);
|
||||
grouped.set(col, ordered);
|
||||
const colWrap = boardEl.createEl("section");
|
||||
colWrap.style.cssText = "min-width:280px;max-width:320px;flex:0 0 300px;border:1px solid var(--background-modifier-border);border-radius:12px;background:var(--background-primary-alt);padding:8px;";
|
||||
colWrap.createEl("strong", { text: `${col} (${ordered.length})` });
|
||||
colWrap.addClass("sfv-board-col");
|
||||
colWrap.createEl("strong", { text: `${String(col)} (${ordered.length})` });
|
||||
const cardsEl = colWrap.createEl("div");
|
||||
cardsEl.style.cssText = "margin-top:8px;min-height:40px;position:relative;";
|
||||
cardsEl.addClass("sfv-board-cards");
|
||||
const dropIndicator = cardsEl.createEl("div");
|
||||
dropIndicator.style.cssText = "height:10px;border:2px dashed var(--interactive-accent);border-radius:8px;background:color-mix(in srgb, var(--interactive-accent) 12%, transparent);margin:4px 0;opacity:0;pointer-events:none;transition:opacity 80ms ease;";
|
||||
dropIndicator.addClass("sfv-drop-indicator");
|
||||
let dropBeforePath = null;
|
||||
const hideDropIndicator = () => {
|
||||
dropIndicator.style.opacity = "0";
|
||||
dropIndicator.removeClass("sfv-visible");
|
||||
dropBeforePath = null;
|
||||
};
|
||||
const getNextCardPath = (cardEl) => {
|
||||
|
|
@ -1603,7 +1644,7 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
if (!cards.length) {
|
||||
dropBeforePath = null;
|
||||
cardsEl.appendChild(dropIndicator);
|
||||
dropIndicator.style.opacity = "1";
|
||||
dropIndicator.addClass("sfv-visible");
|
||||
return;
|
||||
}
|
||||
let beforeCard = null;
|
||||
|
|
@ -1617,7 +1658,7 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
dropBeforePath = beforeCard?.getAttribute("data-sfv-path") || null;
|
||||
if (beforeCard) cardsEl.insertBefore(dropIndicator, beforeCard);
|
||||
else cardsEl.appendChild(dropIndicator);
|
||||
dropIndicator.style.opacity = "1";
|
||||
dropIndicator.addClass("sfv-visible");
|
||||
};
|
||||
cardsEl.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -1629,13 +1670,13 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
if (related instanceof Node && cardsEl.contains(related)) return;
|
||||
hideDropIndicator();
|
||||
});
|
||||
cardsEl.addEventListener("drop", async (e) => {
|
||||
cardsEl.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
const beforePath = dropBeforePath;
|
||||
hideDropIndicator();
|
||||
const payload = e.dataTransfer?.getData("text/plain") || "";
|
||||
if (!payload) return;
|
||||
await handleDrop(JSON.parse(payload), col, beforePath);
|
||||
void handleDrop(JSON.parse(payload), col, beforePath);
|
||||
});
|
||||
for (const { file, cache } of ordered) {
|
||||
const color = colorForValue(getFrontmatterValues(cache, profile.colorField)[0] || null, this.plugin.data.colorConfig) || this.plugin.data.colorConfig.fallbackColor;
|
||||
|
|
@ -1655,19 +1696,19 @@ var SmartFolderView = class extends obsidian.ItemView {
|
|||
e.preventDefault();
|
||||
updateDropIndicator(e.clientY);
|
||||
});
|
||||
card.addEventListener("drop", async (e) => {
|
||||
card.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const beforePath = getCardDropBeforePath(card, e.clientY);
|
||||
hideDropIndicator();
|
||||
const payload = e.dataTransfer?.getData("text/plain") || "";
|
||||
if (!payload) return;
|
||||
await handleDrop(JSON.parse(payload), col, beforePath);
|
||||
void handleDrop(JSON.parse(payload), col, beforePath);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
async onClose() {
|
||||
onClose() {
|
||||
}
|
||||
};
|
||||
var SmartFolderSettingTab = class extends obsidian.PluginSettingTab {
|
||||
|
|
@ -1710,7 +1751,7 @@ var SmartFolderSettingTab = class extends obsidian.PluginSettingTab {
|
|||
this.plugin.data.colorConfig.lightnessRange = v;
|
||||
await this.plugin.persist();
|
||||
}));
|
||||
new obsidian.Setting(containerEl).setName(this.plugin.t("fallbackColor")).setDesc("CSS color value, e.g. #3b82f6 or var(--interactive-accent)").addText((t) => t.setPlaceholder("var(--interactive-accent)").setValue(this.plugin.data.colorConfig.fallbackColor).onChange(async (v) => {
|
||||
new obsidian.Setting(containerEl).setName(this.plugin.t("fallbackColor")).setDesc("CSS color value, e.g. #3b82f6 or var(--interactive-accent)").addText((t) => t.setPlaceholder("e.g. var(--interactive-accent)").setValue(this.plugin.data.colorConfig.fallbackColor).onChange(async (v) => {
|
||||
this.plugin.data.colorConfig.fallbackColor = v.trim() || DEFAULT_COLOR_CONFIG.fallbackColor;
|
||||
await this.plugin.persist();
|
||||
})).addButton((b) => b.setButtonText(this.plugin.t("resetDefault")).onClick(async () => {
|
||||
|
|
@ -1766,7 +1807,7 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
|
|||
}
|
||||
async saveProfile(profile, setActive, askNameIfEmpty) {
|
||||
const next = { ...profile, displayFields: [...profile.displayFields || []], updatedAt: Date.now() };
|
||||
if (!next.name && askNameIfEmpty) next.name = (window.prompt(this.t("pageNamePrompt"), next.sourceFolder || "") || "").trim();
|
||||
if (!next.name && askNameIfEmpty) next.name = (await askTextInput(this.app, this.t("pageNamePrompt"), this.t("pageNamePrompt"), next.sourceFolder || "", "") || "").trim();
|
||||
if (!next.name) next.name = next.sourceFolder || `Page-${(/* @__PURE__ */ new Date()).toISOString()}`;
|
||||
const idx = this.data.profiles.findIndex((p) => p.id === next.id);
|
||||
if (idx >= 0) {
|
||||
|
|
@ -1828,7 +1869,7 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
|
|||
const chosenFolder = await askFolderSelect(this.app, this, folders, defaultFolder);
|
||||
if (chosenFolder == null) return;
|
||||
const safeBase = (profile.name || profile.sourceFolder || "smart-folder-page").replace(/[\\/:*?"<>|]/g, "-").trim() || "smart-folder-page";
|
||||
const folderPrefix = chosenFolder ? `${chosenFolder}/` : "";
|
||||
const folderPrefix = chosenFolder ? `${String(chosenFolder)}/` : "";
|
||||
let path = `${folderPrefix}${safeBase}.smart-folder-page.md`;
|
||||
let i = 1;
|
||||
while (this.app.vault.getAbstractFileByPath(path)) {
|
||||
|
|
@ -1867,62 +1908,6 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
|
|||
}
|
||||
async onload() {
|
||||
await this.loadPluginData();
|
||||
this.styleEl = document.createElement("style");
|
||||
this.styleEl.textContent = `
|
||||
.smart-folder-view-root {
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: rgba(252, 253, 255, 0.8);
|
||||
backdrop-filter: blur(8px) saturate(1.03);
|
||||
box-shadow: inset 0 0 0 9999px rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
.theme-dark .smart-folder-view-root {
|
||||
background: rgba(5, 8, 12, 0.8);
|
||||
backdrop-filter: blur(8px) saturate(0.95);
|
||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
.smart-folder-view-root details {
|
||||
position: relative;
|
||||
}
|
||||
.smart-folder-view-root details[open] {
|
||||
z-index: 200;
|
||||
}
|
||||
.smart-folder-view-root .sfv-dragging {
|
||||
opacity: 0.72;
|
||||
}
|
||||
.smart-folder-view-root .sfv-card-body,
|
||||
.smart-folder-view-root .sfv-card-body p,
|
||||
.smart-folder-view-root .sfv-card-body li,
|
||||
.smart-folder-view-root .sfv-card-body ul,
|
||||
.smart-folder-view-root .sfv-card-body ol,
|
||||
.smart-folder-view-root .sfv-card-body a,
|
||||
.smart-folder-view-root .sfv-card-body strong,
|
||||
.smart-folder-view-root .sfv-card-body em,
|
||||
.smart-folder-view-root .sfv-card-body code,
|
||||
.smart-folder-view-root .sfv-card-body span,
|
||||
.smart-folder-view-root .sfv-card-body h1,
|
||||
.smart-folder-view-root .sfv-card-body h2,
|
||||
.smart-folder-view-root .sfv-card-body h3,
|
||||
.smart-folder-view-root .sfv-card-body h4,
|
||||
.smart-folder-view-root .sfv-card-body h5,
|
||||
.smart-folder-view-root .sfv-card-body h6 {
|
||||
font-size: calc(var(--font-text-size) - 1px) !important;
|
||||
line-height: var(--line-height-normal) !important;
|
||||
}
|
||||
.smart-folder-view-root button,
|
||||
.smart-folder-view-root input,
|
||||
.smart-folder-view-root select,
|
||||
.smart-folder-view-root summary,
|
||||
.smart-folder-view-root label,
|
||||
.smart-folder-view-root span,
|
||||
.smart-folder-view-root p {
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(this.styleEl);
|
||||
this.registerView(VIEW_TYPE, (leaf) => new SmartFolderView(leaf, this));
|
||||
this.registerMarkdownCodeBlockProcessor("smart-folder-page", async (source, el) => {
|
||||
el.empty();
|
||||
|
|
@ -1940,26 +1925,28 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
|
|||
}
|
||||
if (!el.dataset.sfvAutoOpened) {
|
||||
el.dataset.sfvAutoOpened = "1";
|
||||
window.setTimeout(async () => {
|
||||
await this.openProfileFromExport(raw);
|
||||
window.setTimeout(() => {
|
||||
void this.openProfileFromExport(raw);
|
||||
}, 0);
|
||||
}
|
||||
const wrap = el.createEl("div");
|
||||
wrap.style.cssText = "border:1px solid var(--background-modifier-border);border-radius:10px;padding:10px;background:var(--background-primary-alt);";
|
||||
wrap.addClass("sfv-export-block");
|
||||
wrap.createEl("strong", { text: this.t("exportBlockTitle") });
|
||||
wrap.createEl("p", { text: `${this.t("exportBlockDesc")} ${profile.name || profile.sourceFolder}` }).style.cssText = "margin:6px 0;color:var(--text-muted);";
|
||||
wrap.createEl("p", { text: `${this.t("exportBlockDesc")} ${profile.name || profile.sourceFolder}` }).addClass("sfv-export-desc");
|
||||
const row = wrap.createEl("div");
|
||||
row.style.cssText = "display:flex;gap:8px;flex-wrap:wrap;";
|
||||
row.addClass("sfv-export-row");
|
||||
const openBtn = row.createEl("button", { text: this.t("exportBlockOpen") });
|
||||
openBtn.addClass("mod-cta");
|
||||
openBtn.addEventListener("click", async () => {
|
||||
const ok = await this.openProfileFromExport(raw);
|
||||
if (!ok) new obsidian.Notice(this.t("exportBlockInvalid"));
|
||||
openBtn.addEventListener("click", () => {
|
||||
void this.openProfileFromExport(raw).then((ok) => {
|
||||
if (!ok) new obsidian.Notice(this.t("exportBlockInvalid"));
|
||||
});
|
||||
});
|
||||
const importBtn = row.createEl("button", { text: this.t("exportBlockImport") });
|
||||
importBtn.addEventListener("click", async () => {
|
||||
const ok = await this.importProfileFromExport(raw);
|
||||
if (!ok) new obsidian.Notice(this.t("exportBlockInvalid"));
|
||||
importBtn.addEventListener("click", () => {
|
||||
void this.importProfileFromExport(raw).then((ok) => {
|
||||
if (!ok) new obsidian.Notice(this.t("exportBlockInvalid"));
|
||||
});
|
||||
});
|
||||
});
|
||||
this.registerDomEvent(document, "pointerdown", (ev) => {
|
||||
|
|
@ -1976,29 +1963,34 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
|
|||
const openBoxes = document.querySelectorAll(".smart-folder-view-root details[open]");
|
||||
for (const box of openBoxes) box.removeAttribute("open");
|
||||
});
|
||||
this.addRibbonIcon("layout-grid", this.t("title"), async () => {
|
||||
const action = await askStartAction(this.app, this);
|
||||
if (action === "open") new ProfileManagerModal(this.app, this).open();
|
||||
else if (action === "create") new SetupModal(this.app, this).open();
|
||||
this.addRibbonIcon("layout-grid", this.t("title"), () => {
|
||||
void askStartAction(this.app, this).then((action) => {
|
||||
if (action === "open") new ProfileManagerModal(this.app, this).open();
|
||||
else if (action === "create") new SetupModal(this.app, this).open();
|
||||
});
|
||||
});
|
||||
this.addCommand({ id: "open-smart-folder-view", name: this.t("openBuilder"), callback: () => new SetupModal(this.app, this).open() });
|
||||
this.addCommand({ id: "open-smart-folder-view-builder", name: this.t("openBuilder"), callback: () => new SetupModal(this.app, this).open() });
|
||||
this.addCommand({ id: "open-smart-folder-view-manager", name: this.t("openManager"), callback: () => new ProfileManagerModal(this.app, this).open() });
|
||||
this.addCommand({ id: "open-smart-folder-view-last", name: this.t("openLast"), callback: async () => {
|
||||
if (!this.getCurrentProfile()) {
|
||||
new obsidian.Notice(this.t("noProfile"));
|
||||
new SetupModal(this.app, this).open();
|
||||
return;
|
||||
}
|
||||
await this.activateView({ profile: this.getCurrentProfile(), newLeaf: true });
|
||||
this.addCommand({ id: "create-page", name: this.t("openBuilder"), callback: () => new SetupModal(this.app, this).open() });
|
||||
this.addCommand({ id: "create-page-2", name: this.t("openBuilder"), callback: () => new SetupModal(this.app, this).open() });
|
||||
this.addCommand({ id: "open-manager", name: this.t("openManager"), callback: () => new ProfileManagerModal(this.app, this).open() });
|
||||
this.addCommand({ id: "open-last", name: this.t("openLast"), callback: () => {
|
||||
void (async () => {
|
||||
if (!this.getCurrentProfile()) {
|
||||
new obsidian.Notice(this.t("noProfile"));
|
||||
new SetupModal(this.app, this).open();
|
||||
return;
|
||||
}
|
||||
await this.activateView({ profile: this.getCurrentProfile(), newLeaf: true });
|
||||
})();
|
||||
} });
|
||||
this.addCommand({ id: "export-smart-folder-page", name: this.t("exportPage"), callback: async () => {
|
||||
const profile = this.getCurrentProfile();
|
||||
if (!profile) {
|
||||
new obsidian.Notice(this.t("noProfile"));
|
||||
return;
|
||||
}
|
||||
await this.exportProfileToMarkdown(profile);
|
||||
this.addCommand({ id: "export-page", name: this.t("exportPage"), callback: () => {
|
||||
void (async () => {
|
||||
const profile = this.getCurrentProfile();
|
||||
if (!profile) {
|
||||
new obsidian.Notice(this.t("noProfile"));
|
||||
return;
|
||||
}
|
||||
await this.exportProfileToMarkdown(profile);
|
||||
})();
|
||||
} });
|
||||
this.addSettingTab(new SmartFolderSettingTab(this.app, this));
|
||||
}
|
||||
|
|
@ -2023,8 +2015,6 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
|
|||
}
|
||||
}
|
||||
onunload() {
|
||||
if (this.styleEl) this.styleEl.remove();
|
||||
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
|
||||
}
|
||||
async loadPluginData() {
|
||||
const raw = await this.loadData();
|
||||
|
|
|
|||
477
src/main.ts
477
src/main.ts
File diff suppressed because it is too large
Load diff
194
styles.css
194
styles.css
|
|
@ -1,5 +1,189 @@
|
|||
/*
|
||||
* Smart Folder View currently ships its view styling from main.js.
|
||||
* This file is intentionally kept as a release asset placeholder so
|
||||
* GitHub releases can include the standard Obsidian plugin file set.
|
||||
*/
|
||||
/* Smart Folder View — plugin styles */
|
||||
|
||||
/* ── Root container ─────────────────────────────────────────────── */
|
||||
.smart-folder-view-root {
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: rgba(252, 253, 255, 0.8);
|
||||
backdrop-filter: blur(8px) saturate(1.03);
|
||||
box-shadow: inset 0 0 0 9999px rgba(255, 255, 255, 0.28);
|
||||
padding: 16px 18px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.theme-dark .smart-folder-view-root {
|
||||
background: rgba(5, 8, 12, 0.8);
|
||||
backdrop-filter: blur(8px) saturate(0.95);
|
||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
.smart-folder-view-root details { position: relative; }
|
||||
.smart-folder-view-root details[open] { z-index: 200; }
|
||||
|
||||
/* ── Utility states ──────────────────────────────────────────────── */
|
||||
.sfv-dragging { opacity: 0.72; }
|
||||
.sfv-hidden { display: none !important; }
|
||||
.sfv-visible { opacity: 1 !important; }
|
||||
.sfv-elevated { z-index: 170 !important; }
|
||||
|
||||
/* ── Font-size normalization ─────────────────────────────────────── */
|
||||
.smart-folder-view-root .sfv-card-body,
|
||||
.smart-folder-view-root .sfv-card-body p,
|
||||
.smart-folder-view-root .sfv-card-body li,
|
||||
.smart-folder-view-root .sfv-card-body ul,
|
||||
.smart-folder-view-root .sfv-card-body ol,
|
||||
.smart-folder-view-root .sfv-card-body a,
|
||||
.smart-folder-view-root .sfv-card-body strong,
|
||||
.smart-folder-view-root .sfv-card-body em,
|
||||
.smart-folder-view-root .sfv-card-body code,
|
||||
.smart-folder-view-root .sfv-card-body span,
|
||||
.smart-folder-view-root .sfv-card-body h1,
|
||||
.smart-folder-view-root .sfv-card-body h2,
|
||||
.smart-folder-view-root .sfv-card-body h3,
|
||||
.smart-folder-view-root .sfv-card-body h4,
|
||||
.smart-folder-view-root .sfv-card-body h5,
|
||||
.smart-folder-view-root .sfv-card-body h6 {
|
||||
font-size: calc(var(--font-text-size) - 1px) !important;
|
||||
line-height: var(--line-height-normal) !important;
|
||||
}
|
||||
.smart-folder-view-root button,
|
||||
.smart-folder-view-root input,
|
||||
.smart-folder-view-root select,
|
||||
.smart-folder-view-root summary,
|
||||
.smart-folder-view-root label,
|
||||
.smart-folder-view-root span,
|
||||
.smart-folder-view-root p { font-size: 12px; }
|
||||
|
||||
/* ── Searchable single-select ────────────────────────────────────── */
|
||||
.sfv-sel-host { position: relative; display: block; z-index: 1; width: 100%; }
|
||||
.sfv-sel-host.sfv-elevated { z-index: 160; }
|
||||
.sfv-sel-summary {
|
||||
list-style: none; cursor: pointer; min-height: 30px;
|
||||
padding: 5px 10px; border-radius: 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.sfv-sel-panel {
|
||||
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
|
||||
z-index: 60; max-height: 220px; overflow: auto; padding: 8px;
|
||||
border-radius: 10px; border: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-primary);
|
||||
min-width: var(--sfv-sel-min-width, 0px);
|
||||
max-width: var(--sfv-sel-max-width, none);
|
||||
}
|
||||
.sfv-sel-input {
|
||||
width: 100%; height: 28px; padding: 2px 8px; border-radius: 8px;
|
||||
border: 1px solid var(--background-modifier-border); margin-bottom: 6px;
|
||||
}
|
||||
.sfv-sel-meter { position: fixed; visibility: hidden; white-space: nowrap; font: var(--font-ui-medium); }
|
||||
.sfv-sel-row {
|
||||
all: unset; display: block; width: 100%; box-sizing: border-box;
|
||||
text-align: left; padding: 5px 8px; cursor: pointer; border-radius: 6px;
|
||||
}
|
||||
.sfv-sel-row:hover { background: var(--background-modifier-hover); }
|
||||
.sfv-sel-summary-text { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
|
||||
/* ── Multi-select dropdown filter ────────────────────────────────── */
|
||||
.sfv-dd-wrap {
|
||||
display: flex; align-items: flex-start; gap: 8px;
|
||||
flex: 1 1 420px; min-width: 320px; max-width: 420px;
|
||||
position: relative; z-index: 1;
|
||||
}
|
||||
.sfv-dd-wrap.sfv-elevated { z-index: 170; }
|
||||
.sfv-dd-label { font-size: 1em; color: var(--text-normal); min-width: 72px; padding-top: 8px; white-space: nowrap; }
|
||||
.sfv-dd-box { flex: 1; position: relative; z-index: 1; min-width: 0; }
|
||||
.sfv-dd-box.sfv-elevated { z-index: 170; }
|
||||
.sfv-dd-summary {
|
||||
list-style: none; cursor: pointer; height: 36px;
|
||||
padding: 7px 34px 7px 12px; border-radius: 10px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
font-size: 1em; box-sizing: border-box; overflow: hidden;
|
||||
}
|
||||
.sfv-dd-summary-text { color: var(--text-normal); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.sfv-dd-panel {
|
||||
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
|
||||
z-index: 30; max-height: 260px; overflow: auto; padding: 8px;
|
||||
border-radius: 10px; border: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-primary);
|
||||
}
|
||||
.sfv-dd-action-bar { display: flex; gap: 10px; align-items: center; margin-bottom: 4px; }
|
||||
.sfv-dd-action-btn { border: none; background: transparent; color: var(--interactive-accent); cursor: pointer; }
|
||||
.sfv-dd-search {
|
||||
width: 100%; height: 32px; padding: 4px 8px; border-radius: 8px;
|
||||
border: 1px solid var(--background-modifier-border); margin: 6px 0;
|
||||
}
|
||||
.sfv-dd-row { display: flex; align-items: center; gap: 6px; padding: 4px; font-size: 1em; }
|
||||
|
||||
/* ── Cards ───────────────────────────────────────────────────────── */
|
||||
.sfv-card {
|
||||
position: relative; margin: 0 0 12px; padding: 12px 14px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-left: 4px solid var(--sfv-color, var(--interactive-accent));
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--sfv-color, var(--interactive-accent)) 9%, var(--background-primary) 91%);
|
||||
}
|
||||
.sfv-card-dot {
|
||||
position: absolute; left: -26px; top: 17px; width: 12px; height: 12px;
|
||||
border-radius: 50%; background: var(--sfv-color, var(--interactive-accent));
|
||||
border: 2px solid var(--background-primary);
|
||||
}
|
||||
.sfv-card-top { display: flex; align-items: center; gap: 10px; overflow: hidden; margin-bottom: 5px; }
|
||||
.sfv-card-meta { font-size: 0.85em; color: var(--text-muted); white-space: nowrap; overflow-x: auto; flex: 1 1 auto; min-width: 100px; }
|
||||
.sfv-card-link { font-size: 1em; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 1 220px; min-width: 80px; }
|
||||
.sfv-card-body { max-height: calc(var(--line-height-normal) * 1em * 3); overflow-y: auto; overflow-x: hidden; padding-right: 4px; }
|
||||
|
||||
/* ── Modal shared ────────────────────────────────────────────────── */
|
||||
.sfv-modal-action { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
||||
.sfv-setup-action { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
|
||||
.sfv-modal-hint { margin: 6px 0 10px; color: var(--text-muted); font-size: 0.9em; }
|
||||
.sfv-modal-input { width: 100%; margin-top: 6px; }
|
||||
.sfv-modal-select { width: 100%; }
|
||||
|
||||
/* ── View controls ───────────────────────────────────────────────── */
|
||||
.sfv-top-bar { display: flex; justify-content: flex-start; gap: 8px; margin-bottom: 10px; }
|
||||
.sfv-switches { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
|
||||
.sfv-chip {
|
||||
display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px;
|
||||
border-radius: 999px; border: 1px solid var(--background-modifier-border);
|
||||
font-size: 1em; color: var(--text-normal);
|
||||
}
|
||||
.sfv-controls {
|
||||
display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px;
|
||||
position: relative; z-index: 120; overflow: visible;
|
||||
}
|
||||
.sfv-controls-hint { margin: 0; color: var(--text-muted); font-size: 0.85em; }
|
||||
.sfv-preset-wrap { display: flex; gap: 8px; align-items: center; }
|
||||
.sfv-filter-grid { display: flex; flex-wrap: wrap; gap: 8px 10px; align-items: flex-start; }
|
||||
.sfv-sort-wrap { display: flex; gap: 8px; align-items: center; }
|
||||
.sfv-matched { color: var(--text-muted); margin: 0 0 10px; }
|
||||
.sfv-order-actions { display: flex; gap: 8px; align-items: center; margin: 0 0 10px; }
|
||||
|
||||
/* ── Timeline ────────────────────────────────────────────────────── */
|
||||
.sfv-timeline { position: relative; padding-left: 34px; margin-top: 8px; }
|
||||
.sfv-timeline-line { position: absolute; left: 16px; top: 0; bottom: 0; width: 2px; background: var(--background-modifier-border); }
|
||||
.sfv-timeline-indicator {
|
||||
position: absolute; left: 22px; right: 0; height: 4px; border-radius: 6px;
|
||||
background: var(--interactive-accent); opacity: 0; pointer-events: none;
|
||||
z-index: 40; transition: opacity 80ms ease; top: var(--sfv-top, 0px);
|
||||
}
|
||||
.sfv-timeline-indicator.sfv-visible { opacity: 1; }
|
||||
|
||||
/* ── Board ───────────────────────────────────────────────────────── */
|
||||
.sfv-board { display: flex; gap: 12px; align-items: flex-start; overflow-x: auto; margin-top: 2px; padding-top: 2px; }
|
||||
.sfv-board-col {
|
||||
min-width: 280px; max-width: 320px; flex: 0 0 300px;
|
||||
border: 1px solid var(--background-modifier-border); border-radius: 12px;
|
||||
background: var(--background-primary-alt); padding: 8px;
|
||||
}
|
||||
.sfv-board-cards { margin-top: 8px; min-height: 40px; position: relative; }
|
||||
.sfv-drop-indicator {
|
||||
height: 10px; border: 2px dashed var(--interactive-accent); border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--interactive-accent) 12%, transparent);
|
||||
margin: 4px 0; opacity: 0; pointer-events: none; transition: opacity 80ms ease;
|
||||
}
|
||||
.sfv-drop-indicator.sfv-visible { opacity: 1; }
|
||||
|
||||
/* ── Export block ────────────────────────────────────────────────── */
|
||||
.sfv-export-block { border: 1px solid var(--background-modifier-border); border-radius: 10px; padding: 10px; background: var(--background-primary-alt); }
|
||||
.sfv-export-desc { margin: 6px 0; color: var(--text-muted); }
|
||||
.sfv-export-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue