0.121.1: mobile layout overhaul, collapsed-on-load, store-review fix

This commit is contained in:
Human 2026-06-23 00:02:42 -07:00
parent d5250fe6e4
commit 0510541589
9 changed files with 3105 additions and 107 deletions

38
eslint.config.mjs Normal file
View file

@ -0,0 +1,38 @@
// ESLint flat config — surfaces the rules the Obsidian community-plugin review
// bot actually acts on (eslint-plugin-obsidianmd). Treat a clean
// `pnpm run lint` as a pre-publish gate so a store rejection is caught here.
//
// IMPORTANT: the `obsidianmd/recommended` preset also pulls in
// typescript-eslint's *type-checked* layer (no-unsafe-*, no-explicit-any, …),
// which fires thousands of times on this codebase's pre-existing `as any`
// usage. The store does NOT gate on those — historically only the obsidianmd/*
// rules block publishing. So we turn off the type-aware @typescript-eslint
// noise, leaving the obsidianmd/* rules (the real gate) front and centre.
//
// Order matters: `disableTypeChecked` also clears parserOptions.project, so the
// parser+project block is re-asserted AFTER it — several obsidianmd rules
// (no-plugin-as-component, …) require type information to run.
import tseslint from "typescript-eslint";
import obsidianmd from "eslint-plugin-obsidianmd";
export default [
{
ignores: ["main.js", "node_modules/**", "dist/**", "*.mjs"],
},
...obsidianmd.configs.recommended,
tseslint.configs.disableTypeChecked,
{
files: ["src/**/*.ts"],
languageOptions: {
parser: tseslint.parser,
parserOptions: { project: "./tsconfig.json" },
},
rules: {
// Stylistic base rules the store ignores; off so obsidianmd findings
// aren't buried.
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"no-unused-vars": "off",
},
},
];

127
main.js

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
{
"id": "stashpad",
"name": "Stashpad",
"version": "0.118.14",
"version": "0.121.1",
"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",

View file

@ -1,19 +1,24 @@
{
"name": "stashpad-obsidian",
"version": "0.118.14",
"version": "0.121.1",
"private": true,
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "node esbuild.config.mjs production",
"deploy:files": "node scripts/deploy.mjs",
"deploy": "pnpm run build && node scripts/deploy.mjs",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"lint": "eslint src",
"lint:store": "eslint src --max-warnings 0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"esbuild": "^0.21.0",
"eslint": "^10.5.0",
"eslint-plugin-obsidianmd": "^0.3.0",
"obsidian": "latest",
"typescript": "^5.4.0"
"typescript": "^5.4.0",
"typescript-eslint": "^8.61.1"
},
"dependencies": {
"fflate": "^0.8.3",

File diff suppressed because it is too large Load diff

66
release-notes/0.121.1.md Normal file
View file

@ -0,0 +1,66 @@
# 0.121.1 — Mobile layout overhaul, collapsed-on-load, and a store-review fix
A mobile-focused release: the toolbars and navigation controls are rearranged
for one-handed use, note rows get a tighter layout, compact mode is reworked,
and the list now starts fully collapsed on every load. Also includes a fix for
the plugin-store review and an internal lint gate.
## Mobile: toolbars to the bottom, nav into the composer (0.119.00.119.6)
On mobile the chrome is reorganized so the controls sit near your thumb:
- **Toolbar moved below the list**, just above the composer, instead of at the
top (0.119.0). The breadcrumb trail stays at the top.
- **Folder picker + search** move into the composer's bottom-left (0.119.0), and
are anchored to the left of the composer so Send stays on the right (0.119.3).
The folder button shows the folder **name** (capped, with ellipsis) so you can
see which folder you're posting to — not just an icon (0.119.4, 0.119.5).
- **Back / forward / select / ⚡ actions** move from the breadcrumb into the
bottom toolbar, alongside the ⋯ filters and compact buttons (0.119.1).
- **Jump-to-level (route)** sits right after the forward button in the bottom
toolbar (0.119.6); on Home it simply lists Home. Its picker opens as a
**bottom sheet** that slides up from the bottom of the screen (0.119.6).
- The ⋯ filters popover now opens **upward** (with a max-height and scroll) when
there isn't room below, since its button now lives at the bottom (0.119.0).
- Balanced/compact spacing for the bottom toolbar row (0.119.2).
## Mobile: tighter, fixed-position note rows (0.119.7, 0.119.8, 0.120.00.120.2)
- **3-section note rows** (0.120.0): each row stacks into three parts — a top
line with the timestamp (spread out), the expand/collapse toggle, and the ⋯
menu; the note text in the middle; metadata at the bottom — for better
vertical flow.
- The top-row buttons are **sized to match the timestamp** so they read at the
same weight (0.120.1).
- The composer's **expand-toggle chevron stays in a fixed spot** whether the
secondary button group is collapsed or expanded — it no longer jumps as the
group opens (0.119.8).
- **Compact mode cells** no longer clip or sit low: the single-line text now
centers cleanly in the row (0.119.7).
- **List/composer animations are disabled on mobile** to cut repaint flicker on
re-render and scroll (0.120.2). The composer's keyboard-reflow timing is
preserved.
## Mobile compact mode reworked (0.121.0, 0.121.1)
- In compact mode the **bottom toolbar is now shown** (carrying the actions
cluster and the compact/exit toggle) and the **breadcrumb is hidden**, so the
un-compact button is reachable and the list reads as a tight, single-line view
(0.121.0).
- The **focused-note header is hidden in mobile compact** too (matching desktop
compact), removing the empty block that used to sit at the top — the compact
list now starts at the first row (0.121.1).
## Notes start collapsed on load (0.118.16)
- On every load / reload / refresh, **all rows render collapsed**. The
"expand the cursor row automatically" behaviour now only kicks in once you
actively move the cursor (arrow keys or tapping a row) — a fresh view no
longer opens with one row already expanded. Applies on desktop and mobile.
## Store-review fix (0.118.15)
- Removed a static inline-style assignment flagged by the Obsidian
community-plugin review (it was dead code — the element was cleared on the
next line before it could render).
## Internal
- Added an `eslint-plugin-obsidianmd` lint gate run before publishing, so
store-review issues are caught locally. No effect on the shipped plugin.
- A short-lived change that hid Obsidian's own view-header on mobile was trialed
and **reverted** before this release — the header is unchanged.

View file

@ -2425,7 +2425,8 @@ export default class StashpadPlugin extends Plugin {
}
if (r.authors > 0) parts.push(`${r.authors} author${r.authors === 1 ? "" : "s"} in registry`);
// Replace the progress notice in place with a persistent success notice.
fill.style.width = "100%";
// (No need to animate the bar to 100% — el.empty() below removes it
// synchronously before the browser can paint, so it would never show.)
el.empty();
el.removeClass("stashpad-progress-notice");
el.addClass("stashpad-progress-done");

View file

@ -294,6 +294,12 @@ export class StashpadView extends ItemView {
* `.is-cursor-expanded` so an explicit collapse sticks; cleared when the
* cursor leaves the row (then it auto-expands again next time). */
private cursorExpandOverride = new Set<StashpadId>();
/** Has the user moved the cursor (arrow nav / row click) since this view
* instance loaded? Gates the cursor auto-expand (autoExpandCursorRow) so a
* fresh load/reload/refresh renders EVERY row collapsed the cursor row
* only auto-expands once the user actively navigates. Per-instance, so an
* app/view reload (new instance) starts collapsed again. */
private cursorHasMoved = false;
/** Sheet versions: which version (note id) of a `sheet:` group is currently
* shown as the row. View-state only (not persisted) falls back to the
* final pick / first-by-order when unset. */
@ -1642,7 +1648,11 @@ export class StashpadView extends ItemView {
this.pendingCursorId = null;
}
}
if (focused.file && Platform.isMobile) {
// 0.121.1 (item 5 follow-up): mobile appends the focused-note header into
// the list. In COMPACT mode skip it (matching desktop compact, which omits
// the focused header) — with the breadcrumb also hidden, leaving the header
// in left an empty-looking block at the top. Compact = tight list, no header.
if (focused.file && Platform.isMobile && !this.compactMode) {
this.renderFocusedHeaderMini(list, focused);
this.renderFocusedHeader(list, focused);
}
@ -1760,7 +1770,7 @@ export class StashpadView extends ItemView {
else this.renderLockedPlaceholder(list, it.lk);
}
}
if (focused.file && Platform.isMobile) this.installFocusedMiniObserver(list);
if (focused.file && Platform.isMobile && !this.compactMode) this.installFocusedMiniObserver(list);
}
/** 0.98.1: a locked-subtree placeholder row. Click unlock (prompts for the
@ -2349,13 +2359,19 @@ export class StashpadView extends ItemView {
if (this.tinyMode) {
this.renderTinyHeader(root);
} else {
// 0.61.2: compact mode skips the time-filter row (folder switcher,
// tag/color/sort/view dropdowns, time-window buttons, the three
// view-mode buttons). Breadcrumb stays — it's the smallest signal
// of "where am I" worth keeping, and the breadcrumb is where the
// actions cluster (select-mode toggle + ⚡ actions menu) lives.
if (!this.compactMode) this.renderTimeFilterBar(root);
this.renderBreadcrumb(root);
// 0.61.2: compact mode skips the time-filter row on DESKTOP (folder
// switcher, tag/color/sort/view dropdowns, time-window buttons, the three
// view-mode buttons); the breadcrumb stays there and hosts the
// exit-compact button + the desktop actions cluster.
// 0.121.0 (item 5): on MOBILE compact mode, do the opposite — surface the
// bottom toolbar (it already carries the actions cluster + the
// compact/exit toggle on mobile) and HIDE the breadcrumb, so the
// un-compact button is reachable and compact reads as a tight list. On
// desktop compact, behavior is unchanged (toolbar hidden, breadcrumb +
// its exit button shown).
const mobileCompact = this.compactMode && Platform.isMobile;
if (!this.compactMode || Platform.isMobile) this.renderTimeFilterBar(root);
if (!mobileCompact) this.renderBreadcrumb(root);
}
const focused = this.tree.get(this.focusId) ?? this.tree.getRoot();
@ -2654,28 +2670,37 @@ export class StashpadView extends ItemView {
private renderTimeFilterBar(parent: HTMLElement): void {
const bar = parent.createDiv({ cls: "stashpad-time-filter-bar" });
// Folder switcher
const folderBtn = bar.createEl("button", { cls: "stashpad-folder-btn" });
const isOverride = !!this.folderOverride;
const displayName = (this.noteFolder.split("/").pop() || this.noteFolder) || "Stashpad";
// 0.118.0: per-folder icon (settings) on the switcher too, falling back to
// the generic folder glyph.
setIcon(folderBtn.createSpan({ cls: "stashpad-btn-icon" }), this.plugin.getFolderIcon(this.noteFolder) ?? "folder");
folderBtn.createSpan({ text: displayName, cls: "stashpad-btn-text" });
folderBtn.title = isOverride
? `Folder (override): ${this.noteFolder}\nClick to change or revert to default.`
: `Folder: ${this.noteFolder}\nClick to override for this tab.`;
if (isOverride) folderBtn.addClass("is-override");
folderBtn.onclick = (e) => { e.preventDefault(); this.openFolderPicker(); };
// 0.119.0 (mobile-ui-changes-2): on mobile the folder switcher + search move
// into the composer's bottom-left nav cluster (renderComposerNavCluster), so
// skip them here. Desktop keeps them in the toolbar as before.
if (!Platform.isMobile) {
// Folder switcher
const folderBtn = bar.createEl("button", { cls: "stashpad-folder-btn" });
const isOverride = !!this.folderOverride;
const displayName = (this.noteFolder.split("/").pop() || this.noteFolder) || "Stashpad";
// 0.118.0: per-folder icon (settings) on the switcher too, falling back to
// the generic folder glyph.
setIcon(folderBtn.createSpan({ cls: "stashpad-btn-icon" }), this.plugin.getFolderIcon(this.noteFolder) ?? "folder");
folderBtn.createSpan({ text: displayName, cls: "stashpad-btn-text" });
folderBtn.title = isOverride
? `Folder (override): ${this.noteFolder}\nClick to change or revert to default.`
: `Folder: ${this.noteFolder}\nClick to override for this tab.`;
if (isOverride) folderBtn.addClass("is-override");
folderBtn.onclick = (e) => { e.preventDefault(); this.openFolderPicker(); };
// 0.68.4: icon-only Search button between the folder switcher and
// the tags dropdown. Mirrors the Mod+F binding for mouse users.
const searchBtn = bar.createEl("button", { cls: "stashpad-search-btn" });
setIconSafe(searchBtn, "search", "🔍");
searchBtn.title = "Search notes (Mod+F)";
searchBtn.onclick = (e) => { e.preventDefault(); this.openSearchModal(); };
// 0.68.4: icon-only Search button between the folder switcher and
// the tags dropdown. Mirrors the Mod+F binding for mouse users.
const searchBtn = bar.createEl("button", { cls: "stashpad-search-btn" });
setIconSafe(searchBtn, "search", "🔍");
searchBtn.title = "Search notes (Mod+F)";
searchBtn.onclick = (e) => { e.preventDefault(); this.openSearchModal(); };
}
if (Platform.isMobile) {
// 0.119.1 (mobile-ui-changes-2): the actions cluster (back / forward /
// select / ⚡) lives here at the start of the bottom toolbar, next to the
// ⋯ filters + compact buttons (moved out of the breadcrumb).
this.renderActionsCluster(bar);
// Mobile: collapse the four filter/view buttons into a single
// entry-point button. Tapping it opens a vertical accordion with
// one section per former button — keeps the header bar uncluttered
@ -3094,6 +3119,16 @@ export class StashpadView extends ItemView {
if (!canGoFwd) fwdBtn.addClass("is-disabled");
fwdBtn.onclick = (e) => { e.preventDefault(); this.navigateForward(); };
// 0.119.6 (mobile-ui-changes-2): jump-to-level (route) sits right after the
// forward button on mobile (the actions cluster lives in the bottom toolbar
// there). Always shown — at Home the picker just lists Home.
if (Platform.isMobile) {
const routeBtn = actions.createEl("button", { cls: "stashpad-mobile-action-btn" });
setIconSafe(routeBtn, "route", "⋔");
routeBtn.title = "Jump to a level in the path";
routeBtn.onclick = (e) => { e.preventDefault(); this.openBreadcrumbLevelsModal(); };
}
const selectBtn = actions.createEl("button", { cls: "stashpad-mobile-action-btn" });
const inSelect = this.mobileSelectMode;
setIconSafe(selectBtn, inSelect ? "check-square" : "square", inSelect ? "☑" : "☐");
@ -3589,13 +3624,21 @@ export class StashpadView extends ItemView {
// instead of off the right side of the screen. Min 8px gutter
// from the viewport right edge as a safety margin if the button
// is itself off-screen for any reason.
// 0.119.0 (mobile-ui-changes-2): the filters button now sits at the BOTTOM
// of the screen (toolbar moved above the composer), so opening downward
// would run off-screen. Open UPWARD when there isn't room below.
const openUp = r.bottom + 280 > win.innerHeight;
pop.setCssStyles({
right: `${Math.max(8, win.innerWidth - r.right)}px`,
left: "auto",
top: `${r.bottom + 4}px`,
...(openUp
? { bottom: `${Math.max(8, win.innerHeight - r.top + 4)}px`, top: "auto" }
: { top: `${r.bottom + 4}px`, bottom: "auto" }),
// Wider than the per-button popovers so accordion section headers +
// option rows have room to breathe. Capped to viewport width.
maxWidth: "min(360px, calc(100vw - 16px))",
maxHeight: "min(60vh, 420px)",
overflowY: "auto",
width: "max-content",
minWidth: "260px",
});
@ -4424,7 +4467,10 @@ export class StashpadView extends ItemView {
// gives the time-filter row more horizontal real estate.
// 0.117.0: the "jump to level" button now lives inside the actions
// cluster (rendered above), grouped with back/forward/select/⚡.
this.renderActionsCluster(bar);
// 0.119.1 (mobile-ui-changes-2): on mobile the actions cluster
// (back/forward/select/⚡) moves into the bottom toolbar (time-filter-bar),
// next to the ⋯ filters + compact buttons; the breadcrumb keeps just the trail.
if (!Platform.isMobile) this.renderActionsCluster(bar);
const homeBtn = bar.createSpan({ cls: "stashpad-crumb stashpad-crumb-home" });
if (Platform.isMobile) {
// Mobile: render as a house icon to save horizontal space.
@ -4542,7 +4588,9 @@ export class StashpadView extends ItemView {
// (by the crumbs, not the nav cluster). CSS absolutely-positions it at the
// right edge with reserved padding, so it stays visible even when the
// inline crumbs clip — which is exactly when it's needed.
this.renderBreadcrumbLevelsButton(bar);
// 0.119.0 (mobile-ui-changes-2): on mobile it moves into the composer's
// bottom-left nav cluster instead.
if (!Platform.isMobile) this.renderBreadcrumbLevelsButton(bar);
}
/** 0.117.0: the breadcrumb "all levels" button. Pinned at the right end of
@ -4840,7 +4888,7 @@ export class StashpadView extends ItemView {
if (isCursor) row.addClass("is-cursor");
// 0.73.14: auto-expand the cursor row on initial render too (not
// just on arrow-key repaints). Settings-gated.
if (isCursor && this.plugin.settings.autoExpandCursorRow && !this.cursorExpandOverride.has(node.id)) row.addClass("is-cursor-expanded");
if (isCursor && this.cursorHasMoved && this.plugin.settings.autoExpandCursorRow && !this.cursorExpandOverride.has(node.id)) row.addClass("is-cursor-expanded");
if (isPickTarget) row.addClass("is-pick-target");
if (this.isCompleted(node)) row.addClass("is-completed");
if (this.isListPinned(node.id)) row.addClass("is-list-pinned");
@ -5290,6 +5338,34 @@ export class StashpadView extends ItemView {
}
}
/** 0.119.0 (mobile-ui-changes-2): bottom-left nav cluster in the composer
* folder picker + search + jump-to-level (route). Mobile only; these moved
* out of the top toolbar / breadcrumb. Always visible (no collapse, per the
* request that always-visible is fine). */
private renderComposerNavCluster(rail: HTMLElement): void {
const nav = rail.createDiv({ cls: "stashpad-composer-nav" });
// Folder picker (shows the per-folder icon if set, else the folder glyph).
const folderBtn = nav.createEl("button", { cls: "stashpad-composer-btn stashpad-composer-nav-folder" });
setIcon(folderBtn, this.plugin.getFolderIcon(this.noteFolder) ?? "folder");
// 0.119.4: show the folder NAME (capped via CSS) so you know which folder
// you're in — not just the icon.
const fname = (this.noteFolder.split("/").pop() || this.noteFolder) || "Stashpad";
folderBtn.createSpan({ cls: "stashpad-btn-text", text: fname });
folderBtn.title = `Folder: ${this.noteFolder}\nTap to switch / create.`;
if (this.folderOverride) folderBtn.addClass("is-active");
folderBtn.onmousedown = (e) => e.preventDefault();
folderBtn.onclick = (e) => { e.preventDefault(); this.openFolderPicker(); };
// Search.
const searchBtn = nav.createEl("button", { cls: "stashpad-composer-btn" });
setIconSafe(searchBtn, "search", "🔍");
searchBtn.title = "Search notes (Mod+F)";
searchBtn.onmousedown = (e) => e.preventDefault();
searchBtn.onclick = (e) => { e.preventDefault(); this.openSearchModal(); };
// 0.119.6: route (jump-to-level) moved to the actions cluster (after the
// forward button) — see renderActionsCluster. The composer nav is now just
// folder + search.
}
private renderComposer(parent: HTMLElement): void {
const settings = getSettings();
const enterSubmits = this.modeEnterSubmits;
@ -5480,6 +5556,10 @@ export class StashpadView extends ItemView {
fileInput.setCssStyles({ display: "none" });
const btnRail = composer.createDiv({ cls: "stashpad-composer-btn-rail" });
// 0.119.0 (mobile-ui-changes-2): on mobile, the folder picker + search +
// jump-to-level (route) controls live here at the bottom-left of the
// composer (moved out of the top toolbar / breadcrumb).
if (Platform.isMobile) this.renderComposerNavCluster(btnRail);
// Mobile: secondary buttons (split/dest/enter/clip) live inside a
// collapsible group. A chevron-left button at the head of the rail
// toggles their visibility — collapsed at rest to keep the composer
@ -5830,6 +5910,9 @@ export class StashpadView extends ItemView {
// Normal (settled) tap — drop any stale aimed target so it can't leak into
// a later unrelated double-tap.
this.aimedTapTargetId = null;
// A real tap on a row counts as a cursor move — arm the cursor auto-expand
// (kept off through the initial post-load render).
this.cursorHasMoved = true;
const targetEl = e.target as HTMLElement | null;
// Tag click → open global search filtered by that tag.
const tag = targetEl?.closest?.(".tag") as HTMLElement | null;
@ -6330,6 +6413,9 @@ export class StashpadView extends ItemView {
private selectCursor(shift: boolean): void {
const node = this.currentChildren[this.cursorIdx];
if (!node) return;
// First real cursor move since load — arm the cursor auto-expand (so the
// initial render stayed fully collapsed).
this.cursorHasMoved = true;
if (!shift) {
this.selection.clear();
this.selection.add(node.id);
@ -6388,7 +6474,7 @@ export class StashpadView extends ItemView {
// 0.118.10: respect a manual-collapse override; clear it once the cursor
// leaves the row so the auto-expand resumes on the next visit.
if (!isCursor) this.cursorExpandOverride.delete(id);
row.classList.toggle("is-cursor-expanded", autoExpand && isCursor && !this.cursorExpandOverride.has(id));
row.classList.toggle("is-cursor-expanded", autoExpand && isCursor && this.cursorHasMoved && !this.cursorExpandOverride.has(id));
// 0.73.15: pick-target class. Used by the in-list parent picker
// so its arrow-key nav also avoids the full-render rebuild.
row.classList.toggle("is-pick-target", idx === pickIdx);

View file

@ -953,6 +953,24 @@
min-width: 260px;
max-width: min(440px, 94vw) !important;
}
/* 0.119.6: on mobile the jump-to-level picker is a full-width bottom SHEET
(slides up from the bottom, like Obsidian's mobile pickers) instead of a
centered box anchor the modal container to the bottom and stretch the modal. */
body.is-mobile .modal-container:has(.stashpad-breadcrumb-modal),
body.is-phone .modal-container:has(.stashpad-breadcrumb-modal) {
align-items: flex-end;
}
body.is-mobile .modal.stashpad-compact-modal.stashpad-breadcrumb-modal,
body.is-phone .modal.stashpad-compact-modal.stashpad-breadcrumb-modal {
width: 100% !important;
max-width: 100% !important;
min-width: 0;
margin: 0;
border-radius: 14px 14px 0 0;
max-height: 75vh;
animation: stashpad-sheet-up 0.18s ease;
}
@keyframes stashpad-sheet-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
.stashpad-breadcrumb-modal .stashpad-bc-levels { display: flex; flex-direction: column; gap: 2px; }
.stashpad-bc-level-row {
display: flex;
@ -1692,6 +1710,61 @@
grid-template-columns: auto 1fr auto;
}
/* 0.120.0 (trial): 3-section mobile note row. Instead of [meta | body |
actions] side-by-side columns, stack three rows for vertical tightness +
better reading flow:
row 1: timestamp (spread left) · expand toggle · menu
row 2: the text body (full width)
row 3: metadata (children-count, pin, task checkbox)
Pure CSS the DOM already has these pieces; we just regroup them with
grid-template-areas. `display: contents` on the meta/actions wrappers
promotes their children to direct grid items so they can be placed into
areas. Scoped to NON-compact mobile (compact has its own dense layout). */
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note,
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note.is-task {
grid-template-columns: 1fr auto auto;
grid-template-areas:
"meta toggle more"
"body body body"
"foot foot foot";
align-items: center;
row-gap: 2px;
column-gap: 8px;
}
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note-meta,
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note-actions {
display: contents;
}
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note-meta-top { grid-area: meta; }
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note-body { grid-area: body; }
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note-meta-bottom { grid-area: foot; }
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note .stashpad-expand-toggle { grid-area: toggle; }
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note .stashpad-note-more { grid-area: more; }
/* Timestamp reads horizontally (one line) on mobile so it "spreads out"
across the top row instead of stacking date over time. */
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note-meta-top .stashpad-note-time {
white-space: normal;
}
/* The top-row buttons sit flush right, snug together and are sized down so
they read at the same visual weight as the timestamp text (was 28px box /
14px icon, which looked chunkier than the time). */
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note .stashpad-expand-toggle,
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note .stashpad-note-more {
align-self: center;
width: 22px;
height: 22px;
}
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note .stashpad-expand-toggle .svg-icon,
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note .stashpad-note-more .svg-icon {
width: 15px;
height: 15px;
}
/* Timestamp nudged up a step (smaller small) so it matches the now-smaller
top-row buttons instead of looking tiny beside them. */
.stashpad-view.is-mobile:not(.is-compact) .stashpad-note-meta-top .stashpad-note-time {
font-size: var(--font-ui-small);
}
/* Suggest modal (pick/search) */
.stashpad-suggest-item { padding: 6px 8px; }
.stashpad-suggest-title { font-weight: 500; }
@ -1993,13 +2066,30 @@
transition-delay: 0s;
}
/* 0.120.2 (item 3): kill list + composer transitions/animations on mobile to
cut the repaint flicker on re-render / scroll / selection change. Scoped to
the note rows (+ their children) and the COMPOSER'S DESCENDANTS only the
composer's own `margin-bottom` transition above is the keyboard-reflow timing
and lives on `.stashpad-composer` itself, so the `*` descendant selector
leaves it untouched. (Cheap; may only partly reduce the flicker since some of
it comes from the deferred body-render reflow, not CSS animation.) */
.stashpad-view.is-mobile .stashpad-note,
.stashpad-view.is-mobile .stashpad-note *,
.stashpad-view.is-mobile .stashpad-composer * {
transition: none !important;
animation: none !important;
}
/* Hide the composer's helper text on mobile it wraps and eats
another row of vertical space we can't afford. */
.stashpad-view.is-mobile .stashpad-composer-help { display: none; }
/* Tighten the top filter row: pills match the action-button height
(28px) so the whole row reads as a single chip strip. */
.stashpad-view.is-mobile .stashpad-time-filter-bar { padding: 1px 0 2px; gap: 4px; align-items: center; }
/* 0.119.2 (mobile-ui-changes-2): balanced, compact vertical spacing a little
room above (between the divider and the button row) and below, instead of the
buttons hugging the top border with a big gap beneath. */
.stashpad-view.is-mobile .stashpad-time-filter-bar { padding: 5px 0; gap: 4px; align-items: center; }
.stashpad-view.is-mobile .stashpad-folder-btn,
.stashpad-view.is-mobile .stashpad-time-filter-btn {
height: 28px;
@ -2035,25 +2125,69 @@
doesn't run under the rail. */
.stashpad-view.is-mobile .stashpad-composer {
position: relative;
padding-top: 6px;
/* 0.119.2: was 6px; trimmed so the toolbar sits closer above the composer
(less dead space between the button row and the input). */
padding-top: 2px;
}
.stashpad-view.is-mobile .stashpad-composer-input-wrap {
width: 100%;
flex: 1 1 100%;
}
.stashpad-view.is-mobile .stashpad-composer-input {
padding-right: 96px;
/* 0.119.3: the button rail is now a full-width strip along the bottom
(folder/search left, send right), so reserve bottom space instead of a
right gutter. */
padding-right: 12px;
padding-bottom: 40px;
min-height: 72px;
max-height: 220px;
}
.stashpad-view.is-mobile .stashpad-composer-btn-rail {
position: absolute;
right: 6px;
bottom: 8px;
/* 0.119.3: span the full width so the nav cluster can sit at the LEFT
(easily reachable) while send stays at the right. */
left: 8px;
right: 8px;
bottom: 6px;
gap: 0;
pointer-events: none;
display: inline-flex;
display: flex;
align-items: center;
/* 0.119.8 (item 4): right-anchor the toggle+send cluster so the collapse
chevron sits in a FIXED spot whether the secondary group is collapsed or
expanded. nav's margin-right:auto already pushes the cluster right when
present; flex-end keeps it anchored even if it isn't. */
justify-content: flex-end;
}
/* 0.119.8 (item 4): order the collapsible group BEFORE the toggle so it
expands to the LEFT of the chevron. With the cluster right-anchored, the
toggle + send then stay put (toggle = rightEdge send toggle) regardless
of whether the group is showing no more chevron jump on expand/collapse. */
.stashpad-view.is-mobile .stashpad-composer-btn-group { order: 1; }
.stashpad-view.is-mobile .stashpad-composer-rail-toggle { order: 2; }
.stashpad-view.is-mobile .stashpad-composer-send { order: 3; }
/* nav cluster pushed to the far left; everything after it (collapse chevron,
secondary group, send) stays on the right. */
.stashpad-view.is-mobile .stashpad-composer-nav { margin-right: auto; gap: 6px; }
/* 0.119.4 / 0.119.5: the composer folder button shows the folder NAME so you know
which folder you're in. The button must GROW to fit the name use the compound
`.stashpad-composer-btn.stashpad-composer-nav-folder` (both classes, no space)
so width:auto out-specifies the generic mobile `width: 32px` for composer-btn. */
.stashpad-view.is-mobile .stashpad-composer-btn.stashpad-composer-nav-folder {
width: auto;
min-width: 0;
padding: 0 8px;
gap: 4px;
}
.stashpad-view.is-mobile .stashpad-composer-btn.stashpad-composer-nav-folder .stashpad-btn-text {
display: inline-block;
/* ≥15 chars of the folder name before ellipsis. */
max-width: 16ch;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
font-size: var(--font-ui-smaller);
}
.stashpad-view.is-mobile .stashpad-composer-btn-rail > *,
.stashpad-view.is-mobile .stashpad-composer-btn-group > * { pointer-events: auto; }
@ -2078,6 +2212,26 @@
color: var(--text-on-accent);
}
.stashpad-view.is-mobile .stashpad-composer-send:hover { color: var(--text-on-accent); }
/* 0.119.0 (mobile-ui-changes-2): mobile layout move the filter/view/time
toolbar to the bottom (above the composer); the breadcrumb trail stays at top.
The root is a flex column whose direct children are time-filter-bar,
breadcrumb, list, composer reorder them with flex `order`. */
/* 0.119.2: tighter row spacing on mobile (the root column's flex gap was 8px,
which read as too much dead space between the bottom toolbar and composer). */
.stashpad-view.is-mobile { gap: 5px; }
.stashpad-view.is-mobile .stashpad-breadcrumb { order: 1; }
.stashpad-view.is-mobile .stashpad-list { order: 2; }
.stashpad-view.is-mobile .stashpad-time-filter-bar {
order: 3;
/* It now sits above the composer, so border reads better on top. */
border-bottom: none;
border-top: 1px solid var(--background-modifier-border);
}
.stashpad-view.is-mobile .stashpad-composer { order: 4; }
/* Bottom-left nav cluster inside the composer: folder · search · route. */
.stashpad-composer-nav { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; }
.stashpad-composer-nav .stashpad-composer-btn { flex: 0 0 auto; }
/* Collapsible secondary-options group. Default state: width 0,
contents clipped only the toggle chevron + send are visible.
Expanded: width grows to fit children, contents fade in. */
@ -3386,6 +3540,12 @@
min-height: 32px;
align-items: center;
overflow: hidden;
/* 0.119.7: the base row keeps 8px top/bottom padding, which left only
~16px of content area inside the 32px row too short for the ~22px
text line, so it clipped and sat low ("top gap + sunk text"). Drop the
vertical padding so the single clamped line centers cleanly in 32px. */
padding-top: 0;
padding-bottom: 0;
}
/* Make sure the tiny-header buttons never get shrunk out of view. */
.stashpad-tiny-header .stashpad-tiny-expand,