mirror of
https://github.com/grub-basket/SP.git
synced 2026-07-22 07:46:27 +00:00
0.122.9: UI/UX polish — View menu icons/docs, details panel, sync-aware rendering, frameless tiny-window drag
This commit is contained in:
parent
727b78891e
commit
434896fd66
13 changed files with 470 additions and 69 deletions
75
docs/view-menu-guide.md
Normal file
75
docs/view-menu-guide.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# The View menu — modes & filters
|
||||
|
||||
The **View** dropdown (the `list-tree` / `list` / `layout-grid` button in the
|
||||
filter bar) controls how the current folder's notes are listed, plus a set of
|
||||
filters. As of 0.122.1 the inline descriptions are hidden in the menu to keep it
|
||||
compact (it was clipping when the header collapses into the combined ⋯ menu at
|
||||
narrow widths) — this page is the canonical, expanded reference. Each setting is
|
||||
**per-folder** unless noted.
|
||||
|
||||
## View modes
|
||||
|
||||
- **Nested** *(default)* — a tree of the focused note's **immediate children**;
|
||||
drill in to see the next level. Drag-to-reorder works here (the manual order is
|
||||
stored per parent). This is the only mode where **Sort** applies.
|
||||
- **Flat** — **every descendant** of the current focus in one flat list, ordered
|
||||
by sort (created time when sort is non-manual). Drag-reorder is disabled
|
||||
(positions are synthesized, not stored). Good for "show me everything under
|
||||
here at once."
|
||||
- **Everything** — like Flat, **plus** the non-Stashpad files that live in the
|
||||
same folder (interleaved by created / file ctime). Lets the Stashpad view double
|
||||
as a file browser for that folder.
|
||||
|
||||
## Encryption filter *(only when vault encryption is set up)*
|
||||
|
||||
- **Show all** — both locked 🔒 stubs and decrypted notes.
|
||||
- **Locked only** — only the locked 🔒 stubs.
|
||||
- **Decrypted only** — hide the locked 🔒 stubs.
|
||||
|
||||
## Filters (apply in every mode unless noted)
|
||||
|
||||
- **Hide childless notes** — in **Nested**, show only notes that have children
|
||||
(applied at the current level). In **Flat/Everything**, hide top-level notes
|
||||
without children but keep every parent's full subtree, so nothing nested is
|
||||
overlooked.
|
||||
- **Hide completed notes** — hide notes marked complete. A completed **parent**
|
||||
stays visible while any descendant is still incomplete (so you don't lose the
|
||||
path to open work).
|
||||
- **Hide notes without attachments** — show only notes that have an attachment; a
|
||||
parent stays visible while any descendant has one.
|
||||
- **Include attachments** *(Everything mode only)* — show attachments referenced
|
||||
by notes as their own rows in the file list. Off by default — they already
|
||||
appear inline on the notes that embed them.
|
||||
- **Imported notes only** — show only notes that came in via import (`.stash` /
|
||||
dropped files).
|
||||
- **By author** — filter to notes stamped with a chosen author (the dropdown
|
||||
lists the distinct authors present in this folder). Most useful in
|
||||
Flat/Everything, which flatten descendants.
|
||||
|
||||
## Notes
|
||||
|
||||
- **Sort** is per-parent and **Nested-only**; in Flat/Everything the list is a
|
||||
synthesized flat order, so the Sort control is disabled there.
|
||||
- Filters compose — e.g. "Flat + Hide completed + By author" shows one author's
|
||||
outstanding work across the whole subtree.
|
||||
|
||||
## Verbatim in-menu descriptions (archived)
|
||||
|
||||
These are the exact one-line descriptions that used to render under each item in
|
||||
the View menu, preserved here verbatim now that they're hidden in the UI (the
|
||||
strings still live in `src/view.ts` ~3880–4029, just `display:none`):
|
||||
|
||||
| Item | Description |
|
||||
| --- | --- |
|
||||
| Nested | Tree of immediate children (default). |
|
||||
| Flat | All descendants of the current focus, flat by sort. |
|
||||
| Everything | All descendants PLUS non-Stashpad files in the folder. |
|
||||
| Encryption: show all | Both locked 🔒 and decrypted notes. |
|
||||
| Encryption: locked only | Show only locked 🔒 stubs. |
|
||||
| Encryption: decrypted only | Hide locked 🔒 stubs. |
|
||||
| Hide childless notes | Show only notes that have children. Applied at this level. |
|
||||
| Hide completed notes | Hide notes marked complete. A completed parent stays visible while any descendant is still incomplete. |
|
||||
| Hide notes without attachments | Show only notes that have an attachment. A parent stays visible while any descendant has one. |
|
||||
| Include attachments | Only applies in Everything mode. |
|
||||
| Imported notes only | Show only notes that came in via import. |
|
||||
| By author | Show only notes by the chosen author. |
|
||||
86
main.js
86
main.js
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "stashpad",
|
||||
"name": "Stashpad",
|
||||
"version": "0.121.13",
|
||||
"version": "0.122.9",
|
||||
"minAppVersion": "1.13.0",
|
||||
"description": "A chat-style, nested-notes workspace: rapid capture, outliner navigation, fast search, tasks, and per-folder templates, with one-click Open Knowledge Format (OKF) export for LLMs and agents.",
|
||||
"author": "Human",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "stashpad-obsidian",
|
||||
"version": "0.121.13",
|
||||
"version": "0.122.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
|
|
|
|||
95
release-notes/0.122.9.md
Normal file
95
release-notes/0.122.9.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# 0.122.9
|
||||
|
||||
A consistency fix for mobile settings, followed by a UI/UX polish pass focused
|
||||
on window width and the Obsidian window frame, the note context menu, the
|
||||
details panel, and the View menu — plus two bug hunts (sticky stale renders and
|
||||
sync-driven re-render storms).
|
||||
|
||||
## 0.121.14 — mobile settings gutter
|
||||
|
||||
- Settings now match Obsidian's own 20px left/right gutter on mobile, so the
|
||||
Stashpad settings page lines up with the rest of the app instead of sitting at
|
||||
a different inset.
|
||||
|
||||
## 0.122.0 — rebootstrap legibility + icon browse cap
|
||||
|
||||
- The "Rebootstrapping…" progress notice's sub-text inherits the notice color so
|
||||
it's legible instead of rendering low-contrast grey.
|
||||
- The icon picker browses up to 300 icons with an empty query (was ~50, which
|
||||
stopped at the "A…" names); typed search is unaffected.
|
||||
|
||||
## 0.122.1 — declutter the View menu
|
||||
|
||||
- The View menu's inline one-line descriptions are hidden to keep the menu
|
||||
compact (they were clipping when the header collapses into the combined
|
||||
overflow menu at narrow widths).
|
||||
- The full descriptions are preserved in a new guide, `docs/view-menu-guide.md`,
|
||||
which documents every mode and filter and archives the exact in-menu strings
|
||||
verbatim.
|
||||
|
||||
## 0.122.2 — richer note context menu
|
||||
|
||||
- The note context menu gains **Copy text**, and headers (the focused-note and
|
||||
home rows) now get the same context menu as any other note.
|
||||
- Non-task notes can be marked complete from the menu.
|
||||
- (A "Cut note" item was added here and later removed in 0.122.7 — see below.)
|
||||
|
||||
## 0.122.3 — popout-window popovers
|
||||
|
||||
- The due-date time picker and the search time/calendar popovers now build into
|
||||
the anchor element's own document, so they appear in the correct secondary
|
||||
window instead of the main one when Stashpad is open in a popout.
|
||||
|
||||
## 0.122.4 — narrow settings + frameless tiny header
|
||||
|
||||
- First pass at keeping a settings row's controls from collapsing to ~1
|
||||
character wide at narrow window widths (reworked in 0.122.7).
|
||||
- In frameless windows, the tiny-mode header reserves space so its controls
|
||||
don't sit under the OS window buttons — top-right on Windows/Linux, top-left
|
||||
on macOS.
|
||||
|
||||
## 0.122.5 — details panel fixes
|
||||
|
||||
- Fixed multiple composer input boxes stacking on top of each other in the
|
||||
details panel, caused by async render re-entrancy (guarded with a render
|
||||
token).
|
||||
- When viewing a note level with no child selected, the details panel now falls
|
||||
back to previewing the current parent note.
|
||||
|
||||
## 0.122.6 — sticky stale render fix
|
||||
|
||||
- On a file modify, the note's render-cache entry is evicted so the row
|
||||
recomputes from fresh content. This fixes a previously-open note occasionally
|
||||
rendering an earlier version (truncated text, missing attachments) until a
|
||||
manual reload — seen on network drives and with external/synced edits.
|
||||
|
||||
## 0.122.7 — View menu icons, context-menu trim, time entry, nav
|
||||
|
||||
- **Cut note removed** from the context menu: cut/paste has known bugs and
|
||||
cutting a home/parent note is a footgun. It remains available via the
|
||||
cut-notes hotkey.
|
||||
- **View menu** gains leading icons (Nested, Flat, Everything, By author) and
|
||||
dividers between every mode and above By author; By author is no longer
|
||||
indented like a toggle.
|
||||
- **Time picker** keeps exactly what was typed, including leading zeros like
|
||||
`00` and `09`, and only rewrites the value when it's out of range — so a
|
||||
zeroed hour or minute renders correctly.
|
||||
- **Details panel** re-resolves on navigate-up and on back/forward, so exiting a
|
||||
parent note updates the panel instead of leaving it pinned to the child you
|
||||
were on.
|
||||
- Re-attempt at the narrow-settings wrap: a control now wraps onto its own
|
||||
full-width line instead of squeezing its description down to a sliver.
|
||||
|
||||
## 0.122.8 — defer list rendering during sync
|
||||
|
||||
- Obsidian Sync and other bulk writes fire a stream of file events that would
|
||||
each repaint the list, making the view (especially on mobile) unusable for the
|
||||
duration of a sync. Stashpad now detects a burst of file events, pauses list
|
||||
re-rendering, shows a "syncing — list updates paused" notice, and repaints once
|
||||
when activity settles.
|
||||
|
||||
## 0.122.9 — drag frameless tiny windows
|
||||
|
||||
- Restored the ability to move a frameless window while in tiny mode: the
|
||||
tiny-mode header is now a draggable region, with its buttons, inputs, and
|
||||
toggles exempted so they stay clickable. Native-frame windows are unaffected.
|
||||
|
|
@ -27,6 +27,11 @@ export class StashpadDetailView extends ItemView {
|
|||
private unsubscribeSelection: (() => void) | null = null;
|
||||
/** Coalesces a burst of selection/modify events into one render. */
|
||||
private renderTimer: number | null = null;
|
||||
/** 0.122.5 (#12): re-entrancy guard. render() is async (awaits cachedRead +
|
||||
* MarkdownRenderer); without this, two overlapping renders (fast level/tab
|
||||
* switches) both append a composer to the live root → stacked input boxes.
|
||||
* Each render captures a token; after an await it aborts if superseded. */
|
||||
private renderToken = 0;
|
||||
/** 0.74.2: ids of children-list rows currently expanded into their
|
||||
* own subtrees — same pattern as StashpadPanelsView.expanded. Key
|
||||
* is "<folder>|<id>" so expansion state is scoped per folder
|
||||
|
|
@ -129,11 +134,21 @@ export class StashpadDetailView extends ItemView {
|
|||
this.displayedId = null;
|
||||
}
|
||||
const sel = this.plugin.getActiveStashpadSelection();
|
||||
if (sel) this.displayedId = sel.id;
|
||||
return sel;
|
||||
if (sel) { this.displayedId = sel.id; return sel; }
|
||||
// 0.122.5 (#12b): nothing selected — fall back to the focused PARENT note so
|
||||
// the panel previews the current level instead of "no note selected". Not
|
||||
// pinned (no displayedId): re-derived each render, so selecting a child
|
||||
// immediately takes over. Home (ROOT, no file) falls through to empty.
|
||||
const view = this.plugin.lastActiveStashpadLeaf?.view as any;
|
||||
if (view?.getViewType?.() === STASHPAD_VIEW_TYPE && view.tree?.get && view.focusId) {
|
||||
const node = view.tree.get(view.focusId);
|
||||
if (node?.file) return { folder: view.noteFolder as string, id: view.focusId as StashpadId, file: node.file as TFile };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async render(): Promise<void> {
|
||||
const token = ++this.renderToken;
|
||||
const root = this.contentEl;
|
||||
// 0.74.4: preserve composer focus + caret across the rebuild so a
|
||||
// re-render mid-typing doesn't drop the user out of the textarea.
|
||||
|
|
@ -205,6 +220,10 @@ export class StashpadDetailView extends ItemView {
|
|||
bodyWrap.createDiv({ cls: "stashpad-detail-error", text: `Couldn't read \`${file.path}\`: ${(e as Error).message}` });
|
||||
}
|
||||
|
||||
// 0.122.5 (#12): a newer render started while we awaited — abort so we don't
|
||||
// append a second composer (etc.) onto the root it already rebuilt.
|
||||
if (token !== this.renderToken) return;
|
||||
|
||||
// Footer metadata — author, contributors, modified, children count.
|
||||
this.renderFooterMeta(scroll, file, fm, sel);
|
||||
|
||||
|
|
|
|||
|
|
@ -37,9 +37,13 @@ export class IconSuggest extends AbstractInputSuggest<string> {
|
|||
}
|
||||
const match = (id: string): boolean => tokens.every((t) => id.toLowerCase().includes(t));
|
||||
const out = tokens.length ? ids.filter(match) : ids;
|
||||
// Cap the list — the popover only renders a window anyway, and an unfiltered
|
||||
// getIconIds() is ~1.5k entries.
|
||||
return out.sort().slice(0, 50);
|
||||
out.sort();
|
||||
// 0.122.0 (#8): a narrowing search only needs a small window, but with NO
|
||||
// query the user is BROWSING — cap of 50 stranded them in the "a…" icons.
|
||||
// Render a much larger window so scrolling reaches the rest of the alphabet.
|
||||
// (AbstractInputSuggest has no scroll hook for true infinite-load; this is
|
||||
// the pragmatic middle ground — still bounded so it stays snappy.)
|
||||
return out.slice(0, tokens.length ? 50 : 300);
|
||||
}
|
||||
|
||||
renderSuggestion(id: string, el: HTMLElement): void {
|
||||
|
|
|
|||
|
|
@ -1737,6 +1737,11 @@ export class DueDatePickerModal extends Modal {
|
|||
private tinyClosePopover: (() => void) | null = null;
|
||||
private openTimeNumpad(anchor: HTMLElement, timeInput: HTMLInputElement): void {
|
||||
this.tinyClosePopover?.();
|
||||
// 0.122.3 (#4): build the popover + bind its listeners in the ANCHOR's
|
||||
// document so it lands in the same (possibly popout) window as the modal —
|
||||
// not always the main window.
|
||||
const doc = anchor.ownerDocument ?? document;
|
||||
const win = doc.defaultView ?? window;
|
||||
// Seed from the current time value, else the current clock time.
|
||||
let h24 = 9, mm = 0;
|
||||
if (timeInput.value) {
|
||||
|
|
@ -1751,15 +1756,15 @@ export class DueDatePickerModal extends Modal {
|
|||
const period: "am" | "pm" = h24 >= 12 ? "pm" : "am";
|
||||
const seedH = h24 === 0 ? 12 : (h24 > 12 ? h24 - 12 : h24);
|
||||
|
||||
const pop = document.body.createDiv({ cls: "stashpad-when-popover stashpad-due-time-pop" });
|
||||
const pop = doc.body.createDiv({ cls: "stashpad-when-popover stashpad-due-time-pop" });
|
||||
// Above the modal (Obsidian modals sit ~var(--layer-modal)).
|
||||
pop.setCssStyles({ position: "fixed", zIndex: "9999" });
|
||||
|
||||
let onEnter: (() => void) | null = null;
|
||||
const close = (): void => {
|
||||
pop.remove();
|
||||
document.removeEventListener("mousedown", outside, true);
|
||||
document.removeEventListener("keydown", onKey, true);
|
||||
doc.removeEventListener("mousedown", outside, true);
|
||||
doc.removeEventListener("keydown", onKey, true);
|
||||
if (this.tinyClosePopover === close) this.tinyClosePopover = null;
|
||||
};
|
||||
const outside = (e: MouseEvent): void => {
|
||||
|
|
@ -1782,12 +1787,12 @@ export class DueDatePickerModal extends Modal {
|
|||
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
pop.setCssStyles({
|
||||
left: `${Math.max(8, Math.min(rect.left, window.innerWidth - 220))}px`,
|
||||
left: `${Math.max(8, Math.min(rect.left, win.innerWidth - 220))}px`,
|
||||
top: `${rect.bottom + 4}px`,
|
||||
});
|
||||
setTimeout(() => {
|
||||
document.addEventListener("mousedown", outside, true);
|
||||
document.addEventListener("keydown", onKey, true);
|
||||
doc.addEventListener("mousedown", outside, true);
|
||||
doc.addEventListener("keydown", onKey, true);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,19 @@ export class NoteBodyRenderer {
|
|||
return !!c && c.mtime === file.stat.mtime;
|
||||
}
|
||||
|
||||
/** 0.122.6 (#13): drop a file's cached render so the next render recomputes
|
||||
* from fresh content. Wired to the modify event. The mtime-keyed cache can be
|
||||
* poisoned: a render that runs while `cachedRead` is momentarily stale — seen
|
||||
* on a network drive or after an external/coworker edit — stamps the NEW
|
||||
* mtime onto OLD content, so it then serves a truncated / attachment-less body
|
||||
* until reload. Evicting on modify forces a recompute after the render
|
||||
* debounce (by when cachedRead is fresh), breaking that sticky-stale state. */
|
||||
evict(file: TFile): void {
|
||||
const c = this.renderCache as { evict?: (p: string) => void; delete?: (p: string) => void };
|
||||
if (c.evict) c.evict(file.path);
|
||||
else if (c.delete) c.delete(file.path);
|
||||
}
|
||||
|
||||
/** Register a deferred render for a cold row: run `fn` once the container
|
||||
* nears the viewport. */
|
||||
defer(container: HTMLElement, fn: () => void): void {
|
||||
|
|
|
|||
|
|
@ -1345,6 +1345,10 @@ export class StashpadSuggest extends SuggestModal<PickerItem> {
|
|||
): void => {
|
||||
if (currentPopoverClose) currentPopoverClose();
|
||||
const pop = anchor.createDiv({ cls: "stashpad-when-popover" });
|
||||
// 0.122.3 (#4): bind the outside-click listener on the ANCHOR's document
|
||||
// so it works when the search modal is in a popout window (the popover
|
||||
// itself is already a child of `anchor`, so it renders in that window).
|
||||
const doc = anchor.ownerDocument ?? document;
|
||||
// 0.69.34: parent = chipScope's parent (this.scope) so unhandled
|
||||
// keys (arrows etc) cascade to SuggestModal's handlers, same
|
||||
// reasoning as chipScope above.
|
||||
|
|
@ -1359,7 +1363,7 @@ export class StashpadSuggest extends SuggestModal<PickerItem> {
|
|||
if (closed) return;
|
||||
closed = true;
|
||||
pop.remove();
|
||||
document.removeEventListener("mousedown", outside, true);
|
||||
doc.removeEventListener("mousedown", outside, true);
|
||||
try { (this.app as any).keymap?.popScope(popScope); } catch { /* ignore */ }
|
||||
if (currentPopoverClose === close) currentPopoverClose = null;
|
||||
};
|
||||
|
|
@ -1393,7 +1397,7 @@ export class StashpadSuggest extends SuggestModal<PickerItem> {
|
|||
// Belt-and-suspenders: if the modal closes with this popover
|
||||
// still open, ensure the scope is popped on close.
|
||||
this.pendingCleanups.push(close);
|
||||
setTimeout(() => document.addEventListener("mousedown", outside, true), 0);
|
||||
setTimeout(() => doc.addEventListener("mousedown", outside, true), 0);
|
||||
};
|
||||
|
||||
tfBtn.addEventListener("click", (e) => {
|
||||
|
|
|
|||
|
|
@ -85,12 +85,16 @@ export function buildTimePickerInto(pop: HTMLElement, opts: BuildTimePickerOpts)
|
|||
};
|
||||
|
||||
const clamp = (el: HTMLInputElement): void => {
|
||||
// 0.122.7 (#11): keep EXACTLY what was typed (incl. leading zeros like
|
||||
// "00" / "09") so the field renders it — only rewrite when it's out of
|
||||
// range (hours > 24 → 24, minutes > 59 → 59). Previously `String(n)`
|
||||
// collapsed "00" → "0", so a zero could never be seen/entered.
|
||||
const v = el.value.replace(/\D/g, "").slice(0, 2);
|
||||
if (v === "") { el.value = ""; if (el === hField) syncAmpmEnabled(); return; }
|
||||
let n = parseInt(v, 10);
|
||||
if (el === hField) { if (n > 24) n = 24; }
|
||||
else { if (n > 59) n = 59; }
|
||||
el.value = String(n);
|
||||
el.value = v;
|
||||
if (v === "") { if (el === hField) syncAmpmEnabled(); return; }
|
||||
const n = parseInt(v, 10);
|
||||
const max = el === hField ? 24 : 59;
|
||||
if (n > max) el.value = String(max);
|
||||
if (el === hField) syncAmpmEnabled();
|
||||
};
|
||||
for (const el of [hField, mField]) el.addEventListener("input", () => clamp(el));
|
||||
|
|
|
|||
96
src/view.ts
96
src/view.ts
|
|
@ -705,6 +705,7 @@ export class StashpadView extends ItemView {
|
|||
private renderSuppressed(): boolean {
|
||||
return this.bulkRenderDepth > 0
|
||||
|| this.bulkSettleTimer != null
|
||||
|| this.autoSyncDeferActive
|
||||
|| this.plugin.rebootstrapInProgress
|
||||
|| this.plugin.okfRebuildingFolders.has(this.noteFolder);
|
||||
}
|
||||
|
|
@ -731,6 +732,48 @@ export class StashpadView extends ItemView {
|
|||
}, settleMs);
|
||||
}
|
||||
|
||||
// --- Auto sync-burst deferral (0.122.8, F7) -----------------------------
|
||||
// Obsidian Sync (and any external bulk write) fires a stream of `modify`/
|
||||
// `create` events that none of the explicit bulk flags above cover — each
|
||||
// one would otherwise repaint the list, which on mobile makes it unusable
|
||||
// for the duration of a sync. Detect a burst of file events, engage the
|
||||
// same render suppression, surface a notice, and repaint ONCE when it
|
||||
// settles.
|
||||
private syncBurstTimes: number[] = [];
|
||||
private autoSyncDeferActive = false;
|
||||
private autoSyncSettleTimer: number | null = null;
|
||||
private autoSyncNotice: Notice | null = null;
|
||||
|
||||
/** Record an external file event; returns true when the render should be
|
||||
* deferred (a burst is in progress). Resets the settle timer on every
|
||||
* event so the notice stays up for the whole sync, then clears. */
|
||||
private deferDuringSyncBurst(): boolean {
|
||||
// Rebootstrap has its own suppression + one-shot repaint flow — don't
|
||||
// double up a notice on top of it.
|
||||
if (this.plugin.rebootstrapInProgress) return false;
|
||||
const now = Date.now();
|
||||
this.syncBurstTimes.push(now);
|
||||
const cutoff = now - 2000;
|
||||
while (this.syncBurstTimes.length && this.syncBurstTimes[0] < cutoff) this.syncBurstTimes.shift();
|
||||
if (!this.autoSyncDeferActive && this.syncBurstTimes.length >= 6) {
|
||||
this.autoSyncDeferActive = true;
|
||||
this.autoSyncNotice = new Notice("Stashpad: syncing — list updates paused until it settles…", 0);
|
||||
}
|
||||
if (!this.autoSyncDeferActive) return false;
|
||||
if (this.autoSyncSettleTimer != null) window.clearTimeout(this.autoSyncSettleTimer);
|
||||
this.autoSyncSettleTimer = window.setTimeout(() => this.endAutoSyncDefer(), 1500);
|
||||
return true;
|
||||
}
|
||||
|
||||
private endAutoSyncDefer(): void {
|
||||
this.autoSyncSettleTimer = null;
|
||||
this.autoSyncDeferActive = false;
|
||||
this.syncBurstTimes = [];
|
||||
this.autoSyncNotice?.hide();
|
||||
this.autoSyncNotice = null;
|
||||
this.forceReconcileRender();
|
||||
}
|
||||
|
||||
/** Public: a one-shot rebuild + render. Used by the plugin after rebootstrap
|
||||
* clears `rebootstrapInProgress` to repaint the (suppressed) view once. */
|
||||
forceReconcileRender(): void {
|
||||
|
|
@ -830,6 +873,9 @@ export class StashpadView extends ItemView {
|
|||
this.focusedMiniObserver?.disconnect();
|
||||
this.focusedMiniObserver = null;
|
||||
if (this.treeReconcileTimer != null) { window.clearTimeout(this.treeReconcileTimer); this.treeReconcileTimer = null; }
|
||||
if (this.autoSyncSettleTimer != null) { window.clearTimeout(this.autoSyncSettleTimer); this.autoSyncSettleTimer = null; }
|
||||
this.autoSyncNotice?.hide();
|
||||
this.autoSyncNotice = null;
|
||||
this.composerAutocomplete?.detach();
|
||||
this.composerAutocomplete = null;
|
||||
for (const d of this.slugDebouncers.values()) d.cancel();
|
||||
|
|
@ -3813,10 +3859,12 @@ export class StashpadView extends ItemView {
|
|||
* caller can close the wrapping popover/accordion. */
|
||||
private populateViewMenuBody(container: HTMLElement, onPicked: () => void): void {
|
||||
const current = this.currentViewMode();
|
||||
const addRow = (mode: ViewMode, desc: string): void => {
|
||||
const addRow = (mode: ViewMode, desc: string, icon: string): void => {
|
||||
const row = container.createDiv({ cls: "stashpad-view-popover-row" });
|
||||
if (mode === current) row.addClass("is-active");
|
||||
const main = row.createDiv({ cls: "stashpad-view-popover-main" });
|
||||
// 0.122.7: leading icon (matches the View dropdown button's icons).
|
||||
setIcon(main.createSpan({ cls: "stashpad-view-popover-icon" }), icon);
|
||||
main.createSpan({ cls: "stashpad-view-popover-label", text: VIEW_MODE_LABELS[mode] });
|
||||
row.createDiv({ cls: "stashpad-view-popover-desc", text: desc });
|
||||
row.onclick = async (e) => {
|
||||
|
|
@ -3828,9 +3876,12 @@ export class StashpadView extends ItemView {
|
|||
this.render();
|
||||
};
|
||||
};
|
||||
addRow("nested", "Tree of immediate children (default).");
|
||||
addRow("flat", "All descendants of the current focus, flat by sort.");
|
||||
addRow("everything", "All descendants PLUS non-Stashpad files in the folder.");
|
||||
// 0.122.7: dividers between each mode for clearer separation.
|
||||
addRow("nested", "Tree of immediate children (default).", "list-tree");
|
||||
container.createDiv({ cls: "stashpad-view-popover-divider" });
|
||||
addRow("flat", "All descendants of the current focus, flat by sort.", "list");
|
||||
container.createDiv({ cls: "stashpad-view-popover-divider" });
|
||||
addRow("everything", "All descendants PLUS non-Stashpad files in the folder.", "layout-grid");
|
||||
|
||||
container.createDiv({ cls: "stashpad-view-popover-divider" });
|
||||
|
||||
|
|
@ -3955,8 +4006,12 @@ export class StashpadView extends ItemView {
|
|||
const a = parseAuthorRef(this.app.metadataCache.getFileCache(f)?.frontmatter?.author);
|
||||
if (a) authors.set(a.id, a.name);
|
||||
}
|
||||
const authRow = container.createDiv({ cls: "stashpad-view-popover-row stashpad-view-popover-toggle" });
|
||||
// 0.122.7: divider above "By author" + a leading icon; dropped the
|
||||
// -toggle class so it isn't indented like the checkbox rows.
|
||||
container.createDiv({ cls: "stashpad-view-popover-divider" });
|
||||
const authRow = container.createDiv({ cls: "stashpad-view-popover-row" });
|
||||
const authMain = authRow.createDiv({ cls: "stashpad-view-popover-main" });
|
||||
setIcon(authMain.createSpan({ cls: "stashpad-view-popover-icon" }), "user");
|
||||
authMain.createSpan({ cls: "stashpad-view-popover-label", text: "By author" });
|
||||
const authSel = authMain.createEl("select", { cls: "stashpad-view-author-select" });
|
||||
const allO = authSel.createEl("option", { text: "All authors", value: "" });
|
||||
|
|
@ -4729,6 +4784,9 @@ export class StashpadView extends ItemView {
|
|||
if (!node.file) return;
|
||||
const file = node.file;
|
||||
const wrap = parent.createDiv({ cls: "stashpad-focused" });
|
||||
// 0.122.2 (#9): the focused-note header gets the same right-click menu as a
|
||||
// list row (it IS a note — Copy/Cut/Move/Task/Delete all apply to it).
|
||||
wrap.oncontextmenu = (evt) => { evt.preventDefault(); this.openNoteMenu(evt, node); };
|
||||
|
||||
// meta column: timestamp + a transparent grip-shaped spacer so the
|
||||
// body's left edge column-aligns with each list row's body.
|
||||
|
|
@ -8624,6 +8682,9 @@ export class StashpadView extends ItemView {
|
|||
this.render(policy);
|
||||
this.refreshHeaderTitle();
|
||||
this.viewRoot.focus({ preventScroll: true });
|
||||
// 0.122.7 (F2): back/forward changed the cursor — notify so the detail panel
|
||||
// re-resolves to the new selection instead of staying pinned to the old one.
|
||||
this.plugin.notifyStashpadSelectionChanged();
|
||||
}
|
||||
|
||||
private navigateUp(): void {
|
||||
|
|
@ -8676,6 +8737,10 @@ export class StashpadView extends ItemView {
|
|||
this.refreshHeaderTitle();
|
||||
// Belt-and-suspenders reveal in the fallback case.
|
||||
if (idx < 0) this.revealCursorRow();
|
||||
// 0.122.7 (F2): navigating OUT moves the cursor to the note we exited;
|
||||
// notify so the detail panel (pinned to the child we were on) re-resolves to
|
||||
// this new selection (the parent) instead of staying on the stale child.
|
||||
this.plugin.notifyStashpadSelectionChanged();
|
||||
}
|
||||
private openBookmarks(): void {
|
||||
const bookmarks = (this.app as any).internalPlugins?.plugins?.bookmarks?.instance?.items ?? [];
|
||||
|
|
@ -10917,6 +10982,11 @@ export class StashpadView extends ItemView {
|
|||
private onFileModify = (file: TFile): void => {
|
||||
if (!(file instanceof TFile) || file.extension !== "md") return;
|
||||
if (!file.path.startsWith(this.noteFolder + "/")) return;
|
||||
// 0.122.6 (#13): drop this file's (possibly stale-content-but-fresh-mtime)
|
||||
// render-cache entry so the debounced re-render below recomputes from fresh
|
||||
// content — fixes the truncated/attachment-less "earlier version" render
|
||||
// that stuck until reload (network drive / external edits).
|
||||
this.bodyRenderer.evict(file);
|
||||
this.scheduleSlugRename(file);
|
||||
this.scheduleAttachmentSync(file);
|
||||
// 0.72.4: classify self vs external and queue the contributor stamp.
|
||||
|
|
@ -10926,11 +10996,15 @@ export class StashpadView extends ItemView {
|
|||
// metadataCache hook only fires for metadata-affecting edits — pure
|
||||
// body changes (e.g. pasting a long block of plain text) wouldn't
|
||||
// otherwise trigger a re-render, leaving stale clamp state.
|
||||
// 0.122.8 (F7): during a sync/bulk-write burst, hold the repaint and let
|
||||
// deferDuringSyncBurst do one render when it settles.
|
||||
if (this.deferDuringSyncBurst()) return;
|
||||
this.debouncedRender();
|
||||
};
|
||||
private onFileCreate = (file: TFile): void => {
|
||||
if (!(file instanceof TFile) || file.extension !== "md") return;
|
||||
if (!file.path.startsWith(this.noteFolder + "/")) return;
|
||||
if (this.deferDuringSyncBurst()) return;
|
||||
this.debouncedRender();
|
||||
};
|
||||
|
||||
|
|
@ -11150,11 +11224,18 @@ export class StashpadView extends ItemView {
|
|||
if (!this.selection.has(node.id)) { this.selection.clear(); this.selection.add(node.id); this.lastSelected = node.id; }
|
||||
void this.cmdClone();
|
||||
}));
|
||||
// 0.122.2 (#9): copy the note's text, or cut the whole note (for paste/move).
|
||||
// `focusClicked` (defined below) normalises selection to the right-clicked row.
|
||||
menu.addItem((it: any) => it.setTitle("Copy text").setIcon("copy").onClick(() => { focusClicked(); void this.cmdCopy(); }));
|
||||
// 0.122.7: "Cut note" pulled from the menu for now — cut/paste has known bugs
|
||||
// and cutting a parent/home note from the context menu is too easy a footgun.
|
||||
// Still available via the cutNotes hotkey. (See ui-polish todos.)
|
||||
menu.addItem((it: any) => it.setTitle("Fork into a separate note…").setIcon("git-branch").onClick(() => {
|
||||
if (!this.selection.has(node.id)) { this.selection.clear(); this.selection.add(node.id); this.lastSelected = node.id; }
|
||||
this.cmdForkNote();
|
||||
}));
|
||||
menu.addItem((it: any) => it.setTitle("Insert template…").setIcon("file-plus-2").onClick(() => this.cmdInsertTemplate()));
|
||||
// 0.122.2 (#9): "Insert template…" removed from the right-click menu to keep
|
||||
// it compact — still available via command palette + its hotkey.
|
||||
menu.addItem((it: any) => it.setTitle("Export to .stash").setIcon("package").onClick(() => {
|
||||
// Multi-select normalisation (matches Clone / Delete / Set color):
|
||||
// if the right-clicked row isn't in the selection, treat the
|
||||
|
|
@ -11235,6 +11316,9 @@ export class StashpadView extends ItemView {
|
|||
const isDone = this.isCompleted(node);
|
||||
target.addItem((it: any) => it.setTitle(isDone ? "Mark incomplete" : "Mark complete").setIcon(isDone ? "circle" : "check-circle").onClick(() => { focusClicked(); void this.cmdToggleComplete(); }));
|
||||
} else {
|
||||
// 0.122.2 (#10): let non-tasks be marked complete too (sets `completed`;
|
||||
// the note then counts as a task via the bare-completed field).
|
||||
target.addItem((it: any) => it.setTitle("Mark complete").setIcon("check-circle").onClick(() => { focusClicked(); void this.toggleCompletedForNode(node); }));
|
||||
target.addItem((it: any) => it.setTitle("Turn into task").setIcon("check-square").onClick(() => { focusClicked(); void this.cmdToggleTask(); }));
|
||||
}
|
||||
target.addItem((it: any) => it.setTitle("Assign / schedule…").setIcon("user-plus").onClick(() => { focusClicked(); this.cmdAssign(); }));
|
||||
|
|
|
|||
98
styles.css
98
styles.css
|
|
@ -471,8 +471,18 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
.stashpad-view-popover-main { display: flex; align-items: center; gap: 8px; }
|
||||
/* 0.122.7: leading icon on the view-menu mode rows + "By author". */
|
||||
.stashpad-view-popover-icon { display: inline-flex; align-items: center; flex: 0 0 auto; color: var(--text-muted); }
|
||||
.stashpad-view-popover-icon .svg-icon { width: 15px; height: 15px; }
|
||||
.stashpad-view-popover-row.is-active .stashpad-view-popover-icon { color: var(--interactive-accent); }
|
||||
.stashpad-view-popover-label { white-space: nowrap; }
|
||||
.stashpad-view-popover-desc {
|
||||
/* 0.122.1: descriptions are hidden everywhere now (were mobile-only) to keep
|
||||
the view/filter menu compact — it was getting cut off when the header
|
||||
collapses into the combined ⋯ menu at narrow widths. The full, expanded
|
||||
explanations live in docs/public/view-menu-guide.md; the View dropdown
|
||||
button still carries per-mode tooltips. */
|
||||
display: none;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
white-space: normal;
|
||||
|
|
@ -950,6 +960,10 @@
|
|||
dark background in light mode. Inherit the notice's text color instead, which
|
||||
is guaranteed to contrast its background in any theme. */
|
||||
.stashpad-progress-done .stashpad-progress-sub { color: inherit; opacity: 0.9; }
|
||||
/* 0.122.0 (#5): the IN-PROGRESS sub line was --text-muted (grey/dim) — same
|
||||
legibility problem on the notice's dark background. Inherit the notice color
|
||||
so the initial "Counting notes… / <label> · N%" reads bright like the rest. */
|
||||
.stashpad-progress-notice .stashpad-progress-sub { color: inherit; opacity: 0.85; }
|
||||
.stashpad-progress-done .stashpad-progress-title { color: var(--text-success, var(--interactive-accent)); }
|
||||
.stashpad-progress-error .stashpad-progress-title { color: var(--text-error); }
|
||||
/* 0.117.0: "jump to level" button pinned at the right end of the breadcrumb
|
||||
|
|
@ -2885,6 +2899,54 @@ body.is-mobile .stashpad-jd-actions .setting-item-control {
|
|||
.stashpad-settings-section:last-child {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
/* 0.122.4 (#7): at narrow widths a setting's control (especially multi-input
|
||||
ones like the folder-icon picker: dropdown + text + preview) squeezed the
|
||||
description down to ~1 character. Let the control WRAP below the info instead,
|
||||
and keep the info from collapsing. Scoped to Stashpad's settings sections. */
|
||||
.stashpad-settings-section .setting-item {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 8px;
|
||||
}
|
||||
.stashpad-settings-section .setting-item .setting-item-info {
|
||||
/* don't let the name+description collapse (it was wrapping to ~1ch). */
|
||||
min-width: 50%;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.stashpad-settings-section .setting-item .setting-item-control {
|
||||
/* 0.122.7: keep the control at its natural size so it WRAPS to its own
|
||||
full-width line when the row is too narrow — instead of shrinking its
|
||||
inputs and squeezing the description. */
|
||||
flex-shrink: 0;
|
||||
max-width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
/* 0.121.14: Obsidian 1.13's MOBILE settings use a 20px horizontal gutter
|
||||
(measured: .setting-item padding-left = 20px). Several stashpad insets were
|
||||
still at the older 14px, so they didn't line up with Obsidian's own rows —
|
||||
the "nothing is consistent" problem. Bump EVERY settings inset to 20px on
|
||||
mobile so it all aligns. Desktop keeps its existing gutter. */
|
||||
body.is-mobile .stashpad-settings-section > p,
|
||||
body.is-mobile .stashpad-settings-section > h2,
|
||||
body.is-mobile .stashpad-settings-section > h3,
|
||||
body.is-mobile .stashpad-settings-section > h4,
|
||||
body.is-mobile .stashpad-settings-section > ol,
|
||||
body.is-mobile .stashpad-settings-section > ul,
|
||||
body.is-mobile .stashpad-settings-section > .setting-item-description,
|
||||
body.is-mobile .stashpad-folder-list,
|
||||
body.is-mobile .stashpad-authored-folders-list,
|
||||
body.is-mobile .stashpad-folder-placement-group,
|
||||
body.is-mobile .stashpad-known-authors-list,
|
||||
body.is-mobile .stashpad-encryption-section > .setting-item-description,
|
||||
body.is-mobile .stashpad-okf-howto,
|
||||
body.is-mobile .stashpad-beta-row {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
body.is-mobile .stashpad-ai-disclaimer,
|
||||
body.is-mobile .stashpad-okf-cta {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.stashpad-ai-disclaimer {
|
||||
margin: 4px 14px 12px; padding: 10px 12px; border-radius: var(--radius-m, 6px);
|
||||
border: 1px solid var(--color-red, #e24b4a);
|
||||
|
|
@ -3393,6 +3455,42 @@ body.is-mobile .stashpad-jd-actions .setting-item-control {
|
|||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
/* 0.122.4 (#1): when the window is FRAMELESS (Obsidian draws min/max/close
|
||||
inside the content area), the tiny-header controls sit under those window
|
||||
buttons. Reserve space on the controls' side: top-RIGHT on Windows/Linux,
|
||||
top-LEFT on macOS. Native-frame windows (OS draws the frame) have neither
|
||||
class, so they're unaffected. (Unverified on Windows — reasoned from
|
||||
Obsidian's mod-*/is-frameless body classes.) */
|
||||
body.is-frameless.mod-windows .stashpad-tiny-header,
|
||||
body.is-hidden-frameless.mod-windows .stashpad-tiny-header,
|
||||
body.is-frameless.mod-linux .stashpad-tiny-header,
|
||||
body.is-hidden-frameless.mod-linux .stashpad-tiny-header {
|
||||
padding-right: 140px;
|
||||
}
|
||||
body.is-frameless.mod-macos .stashpad-tiny-header,
|
||||
body.is-hidden-frameless.mod-macos .stashpad-tiny-header {
|
||||
padding-left: 80px;
|
||||
}
|
||||
/* 0.122.9 (F3): in frameless mode the tiny-header bar sits where the OS would
|
||||
otherwise expose a draggable titlebar — and the reserved padding above
|
||||
extends the (non-draggable) header box over the macOS traffic-light strip,
|
||||
so the window could no longer be moved. Make the header itself a drag region
|
||||
and exempt its interactive children so they stay clickable. Scoped to
|
||||
frameless only; native-frame windows drag via the OS frame. */
|
||||
body.is-frameless .stashpad-tiny-header,
|
||||
body.is-hidden-frameless .stashpad-tiny-header {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
body.is-frameless .stashpad-tiny-header button,
|
||||
body.is-frameless .stashpad-tiny-header input,
|
||||
body.is-frameless .stashpad-tiny-header .stashpad-tiny-sticky,
|
||||
body.is-frameless .stashpad-tiny-header .stashpad-folder-btn,
|
||||
body.is-hidden-frameless .stashpad-tiny-header button,
|
||||
body.is-hidden-frameless .stashpad-tiny-header input,
|
||||
body.is-hidden-frameless .stashpad-tiny-header .stashpad-tiny-sticky,
|
||||
body.is-hidden-frameless .stashpad-tiny-header .stashpad-folder-btn {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
/* 0.77.0-feat: tiny-window transparency button + slider popover. */
|
||||
.stashpad-tiny-opacity-btn.is-active { color: var(--text-accent); }
|
||||
.stashpad-tiny-opacity-popover {
|
||||
|
|
|
|||
Loading…
Reference in a new issue