joplin-plugin-minimap 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,11 +10,12 @@ A hover-to-expand table of contents minimap for the [Joplin](https://joplinapp.o
10
10
  - **Desktop and Mobile** — Hover on desktop, tap on touch devices: the collapsed strip is a single tap target, rows and buttons grow to finger size when open, and tapping outside or picking a heading closes it again
11
11
  - **Hover to Expand** — Move the mouse over the bars and they expand into a full table of contents overlay, indented by level, with long titles ellipsized
12
12
  - **Depth Stepper** — `+` / `-` buttons at the top of the expanded panel raise or lower how deep the outline goes, so a long nested note collapses to its top-level structure in one click
13
+ - **Pin as a sidebar** — the pin button at the top of the expanded panel keeps the outline open as a permanent sidebar (Obsidian-style) and moves the note text aside so it is never covered; the active heading stays in view as you read. Also available as a setting
13
14
  - **Heading Tiers** — Top-level headings render larger and heavier than nested ones, so the shape of the document is readable at a glance
14
15
  - **Click to Jump** — Click any entry to smooth-scroll to that heading
15
16
  - **Reading Position** — The section currently in view is highlighted automatically
16
17
  - **Live Rebuild** — Rebuilds itself when you switch notes or edit content (listens to `joplin-noteDidUpdate` with a MutationObserver fallback)
17
- - **RTL Aware** — Headings in right-to-left scripts (Persian, Arabic, Hebrew) are right-aligned in the expanded panel and indent inward from the right; direction is detected per heading, so mixed notes stay readable
18
+ - **RTL Aware** — Headings in right-to-left scripts (Persian, Arabic, Hebrew) are right-aligned in the expanded panel and indent inward from the right; direction is detected per heading, so mixed notes stay readable
18
19
  - **Theme Aware** — Follows light and dark themes automatically (colors derive from `currentColor`)
19
20
  - **Unobtrusive** — Hidden for notes with fewer than 2 headings, hidden when printing/exporting, never steals focus or keyboard input
20
21
 
@@ -56,6 +57,7 @@ Configurable in **Tools → Options → Joplin Minimap**:
56
57
  - **Expanded panel width (px)** — maximum width of the hover-expanded table of contents (default 240)
57
58
  - **Minimap side** — which edge of the viewer the minimap docks to, right or left (default right)
58
59
  - **Default depth** — deepest heading level shown when a note opens; the `+`/`-` buttons change it on the fly (default 6, every level)
60
+ - **Keep outline open (pinned)** — show the outline as a permanent sidebar instead of on hover; the pin button in the panel toggles the same setting (default off)
59
61
  - **Edge distance (px)** — gap between the minimap and the viewer edge it sits on (default 6)
60
62
  - **Text size (%)** — scales the text in the expanded panel; raise it on high-DPI screens (default 100, under *Advanced*)
61
63
  - **High contrast panel** — brighter text and a more solid panel background, using no fixed colours so it still follows your theme (default off, under *Advanced*)
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
- {
2
- "name": "joplin-plugin-minimap",
3
- "version": "1.4.0",
4
- "description": "A table of contents minimap for the Joplin note viewer, on desktop and mobile",
5
- "author": "lim0513",
6
- "homepage": "https://github.com/lim0513/joplin-minimap",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/lim0513/joplin-minimap.git"
10
- },
11
- "scripts": {
12
- "dist": "node build.js && node scripts/pack-jpl.js"
13
- },
14
- "keywords": ["joplin-plugin", "joplin", "toc", "outline", "minimap", "table-of-contents", "viewer"],
15
- "license": "MIT",
16
- "files": ["publish"]
17
- }
1
+ {
2
+ "name": "joplin-plugin-minimap",
3
+ "version": "1.5.0",
4
+ "description": "A table of contents minimap for the Joplin note viewer, on desktop and mobile",
5
+ "author": "lim0513",
6
+ "homepage": "https://github.com/lim0513/joplin-minimap",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/lim0513/joplin-minimap.git"
10
+ },
11
+ "scripts": {
12
+ "dist": "node build.js && node scripts/pack-jpl.js"
13
+ },
14
+ "keywords": ["joplin-plugin", "joplin", "toc", "outline", "minimap", "table-of-contents", "viewer"],
15
+ "license": "MIT",
16
+ "files": ["publish"]
17
+ }
package/publish/index.js CHANGED
@@ -31,6 +31,10 @@ const SETTINGS_I18N = {
31
31
  maxLevelDesc: 'Deepest heading level shown when a note opens. The +/- buttons at the top of the expanded panel change it on the fly. Default: 6 (every level).',
32
32
  showTodos: 'Show to-do markers',
33
33
  showTodosDesc: 'Show a small red dot before a section\'s tick bar when it contains open to-dos. Default: on.',
34
+ pinned: 'Keep outline open (pinned)',
35
+ pinnedDesc: 'Show the expanded outline as a permanent sidebar instead of opening it on hover, and move the note text aside so the outline never covers it. The pin button at the top of the panel toggles this too. Default: off.',
36
+ pinTip: 'Pin the outline open',
37
+ unpinTip: 'Unpin (open on hover)',
34
38
  },
35
39
  zh_CN: {
36
40
  minHeadings: '最少标题数',
@@ -51,6 +55,10 @@ const SETTINGS_I18N = {
51
55
  maxLevelDesc: '打开笔记时显示到第几级标题。展开面板顶部的 +/- 按钮可随时调整。默认 6(全部层级)。',
52
56
  showTodos: '显示待办标记',
53
57
  showTodosDesc: '当章节内含未完成待办时,在该章节横线前显示一个小红点提醒。默认开启。',
58
+ pinned: '常驻展开(钉住)',
59
+ pinnedDesc: '把展开的目录作为常驻侧栏显示,而不是悬停时才展开,并把正文让开,避免目录遮住文字。面板顶部的钉子按钮也可以切换。默认关闭。',
60
+ pinTip: '钉住目录',
61
+ unpinTip: '取消钉住(悬停展开)',
54
62
  },
55
63
  zh_TW: {
56
64
  minHeadings: '最少標題數',
@@ -71,6 +79,10 @@ const SETTINGS_I18N = {
71
79
  maxLevelDesc: '開啟筆記時顯示到第幾級標題。展開面板頂部的 +/- 按鈕可隨時調整。預設 6(全部層級)。',
72
80
  showTodos: '顯示待辦標記',
73
81
  showTodosDesc: '當章節內含未完成待辦時,在該章節橫線前顯示一個小紅點提醒。預設開啟。',
82
+ pinned: '常駐展開(釘住)',
83
+ pinnedDesc: '把展開的目錄作為常駐側欄顯示,而不是懸停時才展開,並把內文讓開,避免目錄遮住文字。面板頂部的釘子按鈕也可以切換。預設關閉。',
84
+ pinTip: '釘住目錄',
85
+ unpinTip: '取消釘住(懸停展開)',
74
86
  },
75
87
  ru: {
76
88
  minHeadings: 'Минимум заголовков',
@@ -91,6 +103,10 @@ const SETTINGS_I18N = {
91
103
  maxLevelDesc: 'До какого уровня заголовков показывать при открытии заметки. Кнопки +/- вверху развёрнутой панели меняют её на лету. По умолчанию: 6 (все уровни).',
92
104
  showTodos: 'Показывать метки задач',
93
105
  showTodosDesc: 'Показывать маленькую красную точку перед линией раздела, если в нём есть открытые задачи. По умолчанию: вкл.',
106
+ pinned: 'Закрепить оглавление',
107
+ pinnedDesc: 'Показывать развёрнутое оглавление как постоянную боковую панель вместо раскрытия при наведении и сдвигать текст заметки, чтобы панель его не закрывала. Кнопка-булавка вверху панели тоже переключает режим. По умолчанию: выкл.',
108
+ pinTip: 'Закрепить оглавление',
109
+ unpinTip: 'Открепить (раскрывать при наведении)',
94
110
  },
95
111
  ja_JP: {
96
112
  minHeadings: '最小見出し数',
@@ -111,6 +127,10 @@ const SETTINGS_I18N = {
111
127
  maxLevelDesc: 'ノートを開いたときに表示する見出しの深さ。展開パネル上部の +/- ボタンでいつでも変更できます。既定値:6(すべての階層)。',
112
128
  showTodos: 'ToDoマーカーを表示',
113
129
  showTodosDesc: '未完了のToDoを含むセクションの線の前に小さな赤い点を表示します。既定値:オン。',
130
+ pinned: '目次を常時表示(ピン留め)',
131
+ pinnedDesc: '展開した目次をホバー時だけでなく常設のサイドバーとして表示し、本文を横にずらして目次が文字に重ならないようにします。パネル上部のピンボタンでも切り替えられます。既定値:オフ。',
132
+ pinTip: '目次をピン留め',
133
+ unpinTip: 'ピン留めを解除(ホバーで展開)',
114
134
  },
115
135
  };
116
136
 
@@ -213,6 +233,16 @@ joplin.plugins.register({
213
233
  label: t.highContrast,
214
234
  description: t.highContrastDesc,
215
235
  },
236
+ // Also written by the pin button in the panel (see onMessage below), so
237
+ // the choice survives note switches and restarts.
238
+ 'minimapPinned': {
239
+ value: false,
240
+ type: TYPE_BOOL,
241
+ section: 'minimap',
242
+ public: true,
243
+ label: t.pinned,
244
+ description: t.pinnedDesc,
245
+ },
216
246
  'minimapShowTodos': {
217
247
  value: true,
218
248
  type: TYPE_BOOL,
@@ -243,8 +273,17 @@ joplin.plugins.register({
243
273
  fontScale: await joplin.settings.value('minimapFontScale'),
244
274
  highContrast: await joplin.settings.value('minimapHighContrast'),
245
275
  showTodos: await joplin.settings.value('minimapShowTodos'),
276
+ pinned: await joplin.settings.value('minimapPinned'),
277
+ pinTip: t.pinTip,
278
+ unpinTip: t.unpinTip,
246
279
  };
247
280
  }
281
+ // The pin button in the viewer. Written back so every note - and the
282
+ // next session - opens in the same mode.
283
+ if (message && message.type === 'setPinned') {
284
+ await joplin.settings.setValue('minimapPinned', message.value === true);
285
+ return true;
286
+ }
248
287
  return null;
249
288
  });
250
289
  },
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "manifest_version": 1,
3
3
  "id": "com.github.joplin-minimap",
4
- "app_min_version": "2.8",
5
- "app_min_version_mobile": "3.2",
6
- "platforms": [
7
- "desktop",
8
- "mobile"
4
+ "app_min_version": "2.8",
5
+ "app_min_version_mobile": "3.2",
6
+ "platforms": [
7
+ "desktop",
8
+ "mobile"
9
9
  ],
10
- "version": "1.4.0",
10
+ "version": "1.5.0",
11
11
  "name": "Joplin Minimap",
12
12
  "description": "A table of contents minimap for the note viewer. Shows heading tick marks along one edge; hover (or tap on mobile) to expand into a full outline, click to jump.",
13
13
  "author": "lim0513",
@@ -1,6 +1,7 @@
1
1
  /* Joplin Minimap - runs inside the rendered note viewer.
2
2
  * Collects h1-h6 from the rendered DOM, draws collapsed tick bars on the
3
- * right edge, expands into a full ToC on hover, and jumps on click.
3
+ * right edge, expands into a full ToC on hover (or stays open as a pinned
4
+ * sidebar), and jumps on click.
4
5
  * Rebuilds itself whenever the note content changes (note switch, edit).
5
6
  */
6
7
  (function () {
@@ -8,14 +9,24 @@
8
9
 
9
10
  // Defaults; overridden from Joplin's plugin settings (Tools > Options > Minimap)
10
11
  // when webviewApi is available in this webview.
11
- var settings = { minHeadings: 2, panelWidth: 240, rightOffset: 6, side: 'right', maxLevel: 6, fontScale: 100, highContrast: false, showTodos: true };
12
+ var settings = { minHeadings: 2, panelWidth: 240, rightOffset: 6, side: 'right', maxLevel: 6, fontScale: 100, highContrast: false, showTodos: true, pinned: false, pinTip: 'Pin the outline open', unpinTip: 'Unpin (open on hover)' };
13
+
14
+ // Pin state set from the panel's own button. The write-back to the plugin
15
+ // setting is async, so a settings fetch already in flight can still return
16
+ // the OLD value and flip the panel back. Hold the local choice until a fetch
17
+ // agrees with it; after that the setting is authoritative again, so a change
18
+ // made on the options screen still takes effect.
19
+ var pinOverride = null;
12
20
 
13
21
  function loadSettings() {
14
22
  if (typeof webviewApi === 'undefined' || !webviewApi.postMessage) {
15
23
  return Promise.resolve();
16
24
  }
17
25
  return webviewApi.postMessage('joplin-minimap', 'getSettings').then(function (s) {
18
- if (s && typeof s.minHeadings === 'number') settings = s;
26
+ if (s && typeof s.minHeadings === 'number') {
27
+ settings = s;
28
+ if (pinOverride !== null && (s.pinned === true) === pinOverride) pinOverride = null;
29
+ }
19
30
  }).catch(function () { /* keep defaults (e.g. print/export context) */ });
20
31
  }
21
32
 
@@ -47,7 +58,7 @@
47
58
  // then renders UNSTYLED as flow content below the note (looks like a
48
59
  // duplicated outline under the document). Keeping the CSS inline and
49
60
  // re-injecting guarantees the nav and its styling live and die together.
50
- var MINIMAP_CSS = "/* Joplin Minimap \u2014 collapsed tick bars, hover-expanded ToC panel.\n * Colors use currentColor / rgba so it follows both light and dark themes.\n */\n\n#jp-minimap {\n\tuser-select: none;\n\t-webkit-user-select: none;\n\tcaret-color: transparent;\n\tcursor: default;\n\tposition: fixed;\n\ttop: 50%;\n\tright: 6px;\n\ttransform: translateY(-50%);\n\tz-index: 9999;\n\tfont-size: var(--jp-mm-font, 12.5px);\n\tline-height: 1.35;\n\tcolor: var(--jp-mm-color, inherit);\n\t/* Collapsed geometry is direction-independent: the tick bars always\n\t * hug the docked edge, whichever side that is, even in an RTL note. Per-item direction applies\n\t * to the expanded panel only (see the dir=rtl rule below). */\n\tdirection: ltr;\n}\n\n.jp-mm-list {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: flex-end;\n\tpadding: 8px 6px;\n\tmax-height: 84vh;\n\toverflow: hidden;\n\tborder-radius: 10px;\n\ttransition: background 0.15s ease, box-shadow 0.15s ease;\n}\n\n.jp-mm-item {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: flex-end;\n\tpadding: 3px 4px;\n\tborder-radius: 6px;\n\ttext-decoration: none;\n\tcolor: inherit;\n\topacity: var(--jp-mm-dim, 0.5);\n\tcursor: pointer;\n\toutline: none;\n\tdirection: ltr;\n}\n\n/* ---- collapsed state: tick bars, width by heading level ---- */\n\n.jp-mm-bar {\n\tdisplay: block;\n\theight: 2px;\n\tborder-radius: 1px;\n\tbackground: currentColor;\n}\n\n.jp-mm-l1 .jp-mm-bar { width: 18px; }\n.jp-mm-l2 .jp-mm-bar { width: 13px; }\n.jp-mm-l3 .jp-mm-bar { width: 9px; }\n.jp-mm-l4 .jp-mm-bar { width: 7px; }\n.jp-mm-l5 .jp-mm-bar { width: 5px; }\n.jp-mm-l6 .jp-mm-bar { width: 5px; }\n\n.jp-mm-label { display: none; }\n\n/* ---- depth stepper (issue #2) ---- */\n\n/* Rows deeper than the current depth are HIDDEN, not removed: keeping the\n * nav element alive is what lets the panel stay open across a depth change,\n * since the whole expansion rides on :hover over #jp-minimap. */\n#jp-minimap[data-depth=\"1\"] .jp-mm-l2,\n#jp-minimap[data-depth=\"1\"] .jp-mm-l3,\n#jp-minimap[data-depth=\"1\"] .jp-mm-l4,\n#jp-minimap[data-depth=\"1\"] .jp-mm-l5,\n#jp-minimap[data-depth=\"1\"] .jp-mm-l6,\n#jp-minimap[data-depth=\"2\"] .jp-mm-l3,\n#jp-minimap[data-depth=\"2\"] .jp-mm-l4,\n#jp-minimap[data-depth=\"2\"] .jp-mm-l5,\n#jp-minimap[data-depth=\"2\"] .jp-mm-l6,\n#jp-minimap[data-depth=\"3\"] .jp-mm-l4,\n#jp-minimap[data-depth=\"3\"] .jp-mm-l5,\n#jp-minimap[data-depth=\"3\"] .jp-mm-l6,\n#jp-minimap[data-depth=\"4\"] .jp-mm-l5,\n#jp-minimap[data-depth=\"4\"] .jp-mm-l6,\n#jp-minimap[data-depth=\"5\"] .jp-mm-l6 { display: none; }\n\n/* The stepper is part of the expanded panel only - collapsed, the minimap\n * stays the same narrow strip of tick marks it has always been. */\n.jp-mm-head { display: none; }\n\n\n/* ---- docked on the left edge ---- */\n\n/* Mirror the collapsed alignment so the tick bars hug the left border.\n * direction is pinned to ltr above, so flex-start is unambiguously the left\n * side whatever language the note is in. Placed BEFORE the :hover rules on\n * purpose: they carry the same specificity, so source order decides. */\n#jp-minimap.jp-mm-left .jp-mm-list { align-items: flex-start; }\n#jp-minimap.jp-mm-left .jp-mm-item { justify-content: flex-start; }\n\n/* The to-do dot mirrors too: docked right it hangs left of the tick bar,\n * docked left it must hang RIGHT of it - otherwise a dotted row pushes its\n * bar 9px inward (4px dot + 5px gap) and the flush bar column goes ragged.\n * order= reorders the flex row without touching the DOM, so the expanded\n * panel can put the dot back in front of the label. */\n#jp-minimap.jp-mm-left .jp-mm-dot {\n\torder: 1;\n\tmargin-inline-start: 5px;\n\tmargin-inline-end: 0;\n}\n\n/* ---- expanded state (hover) ---- */\n\n#jp-minimap.jp-mm-open .jp-mm-list {\n\talign-items: stretch;\n\toverflow-y: auto;\n\toverscroll-behavior: contain;\n\tbackground: var(--jp-mm-bg, rgba(127, 127, 127, 0.16));\n\tbackdrop-filter: blur(10px);\n\t-webkit-backdrop-filter: blur(10px);\n\tbox-shadow: 0 6px 28px rgba(0, 0, 0, 0.28);\n}\n\n#jp-minimap.jp-mm-open .jp-mm-bar { display: none; }\n\n#jp-minimap.jp-mm-open .jp-mm-item { justify-content: flex-start; }\n\n#jp-minimap.jp-mm-open .jp-mm-label {\n\tdisplay: block;\n\tmax-width: var(--jp-mm-width, 240px);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\ttext-align: start;\n}\n\n/* RTL headings (Persian/Arabic/Hebrew) read right-to-left in the expanded\n * panel: justify-content, padding-inline-start and text-align:start all flip\n * with the row direction, so the label hugs the right edge and nested levels\n * indent inward from the right. build() sets dir per row (first strong char). */\n#jp-minimap.jp-mm-open .jp-mm-item[dir=\"rtl\"] { direction: rtl; }\n\n/* indent by heading level when expanded (logical: left in LTR, right in RTL) */\n#jp-minimap.jp-mm-open .jp-mm-l2 { padding-inline-start: 16px; }\n#jp-minimap.jp-mm-open .jp-mm-l3 { padding-inline-start: 28px; }\n#jp-minimap.jp-mm-open .jp-mm-l4 { padding-inline-start: 40px; }\n#jp-minimap.jp-mm-open .jp-mm-l5 { padding-inline-start: 52px; }\n#jp-minimap.jp-mm-open .jp-mm-l6 { padding-inline-start: 52px; }\n\n#jp-minimap.jp-mm-open .jp-mm-head {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tgap: 7px;\n\tpadding: 0 4px 5px;\n\tmargin-bottom: 4px;\n\tborder-bottom: 1px solid rgba(127, 127, 127, 0.3);\n\tfont-size: 0.88em;\n\topacity: 0.8;\n\t/* The control reads the same either way round, and the panel is pinned\n\t * to ltr anyway - keep it out of the per-row bidi logic entirely. */\n\tdirection: ltr;\n}\n\n.jp-mm-step {\n\tmin-width: 15px;\n\ttext-align: center;\n\tborder-radius: 4px;\n\tfont-weight: 700;\n\tcursor: pointer;\n\tbackground: rgba(127, 127, 127, 0.22);\n}\n\n#jp-minimap.jp-mm-open .jp-mm-step:hover { background: rgba(127, 127, 127, 0.45); }\n\n/* Already at the shallowest / deepest level this note has. */\n.jp-mm-step-off { opacity: 0.3; cursor: default; }\n#jp-minimap.jp-mm-open .jp-mm-step-off:hover { background: rgba(127, 127, 127, 0.22); }\n\n.jp-mm-depth { font-variant-numeric: tabular-nums; }\n\n/* ---- heading tiers (issue #2): indentation alone is a weak cue when\n * scanning a long outline. Size and weight only - NEVER a colour. The\n * panel has to stay legible on every Joplin theme, so the palette is\n * limited to currentColor and neutral rgba (see CLAUDE.md). ---- */\n#jp-minimap.jp-mm-open .jp-mm-r0 .jp-mm-label { font-size: 1.08em; font-weight: 600; }\n#jp-minimap.jp-mm-open .jp-mm-r1 .jp-mm-label { font-weight: 500; }\n#jp-minimap.jp-mm-open .jp-mm-r2 .jp-mm-label { font-size: 0.92em; opacity: 0.8; }\n\n\n/* Expanded, the dot reads as a marker BEFORE the title on either edge. */\n#jp-minimap.jp-mm-open.jp-mm-left .jp-mm-dot {\n\torder: 0;\n\tmargin-inline-start: 0;\n\tmargin-inline-end: 5px;\n}\n\n/* ---- high contrast (issue #5) ---- */\n\n/* Still routed through the same variables, so a userstyle.css override wins\n * over the mode rather than fighting it. */\n#jp-minimap.jp-mm-hc .jp-mm-item { opacity: var(--jp-mm-dim, 0.82); }\n#jp-minimap.jp-mm-hc.jp-mm-open .jp-mm-r2 .jp-mm-label { opacity: 0.95; }\n\n#jp-minimap.jp-mm-hc.jp-mm-open .jp-mm-list {\n\tbackground: var(--jp-mm-bg, rgba(127, 127, 127, 0.38));\n\tbox-shadow: 0 6px 28px rgba(0, 0, 0, 0.45);\n}\n\n#jp-minimap.jp-mm-hc.jp-mm-open .jp-mm-head { opacity: 1; }\n#jp-minimap.jp-mm-hc .jp-mm-step { background: rgba(127, 127, 127, 0.4); }\n\n/* ---- shared states ---- */\n\n.jp-mm-item:hover {\n\topacity: 1;\n\tbackground: rgba(127, 127, 127, 0.22);\n}\n\n.jp-mm-active { opacity: 1; }\n\n#jp-minimap.jp-mm-open .jp-mm-active {\n\tbackground: rgba(127, 127, 127, 0.18);\n}\n\n/* No scrollbar in the expanded panel: the wheel handler owns scrolling,\n * and a visible scrollbar at the panel edge invites overlay-scrollbar\n * style hover/click interference. */\n.jp-mm-list::-webkit-scrollbar { display: none; }\n.jp-mm-list { scrollbar-width: none; }\n\n/* ---- open-to-do section dot: one muted red dot before the tick bar of\n * any section that contains at least one unchecked checkbox ---- */\n.jp-mm-dot {\n\tdisplay: none;\n\twidth: 4px;\n\theight: 4px;\n\tborder-radius: 50%;\n\tbackground: var(--jp-mm-dot, rgba(205, 97, 85, 0.85));\n\tmargin-inline-end: 5px;\n\tflex: none;\n}\n\n.jp-mm-dot.jp-mm-on { display: block; }\n\n/* don't show over printed/exported output */\n/* ---- touch devices ---- */\n\n/* (hover: none) is the device capability, not a screen width: a tablet with\n * a mouse keeps the desktop sizes, a touchscreen laptop gets these. */\n@media (hover: none) {\n\t/* Collapsed, the whole strip is the tap target - give it room. */\n\t.jp-mm-list { padding: 12px 14px; }\n\t.jp-mm-bar { height: 3px; }\n\n\t/* Expanded, each row has to be hittable on its own. */\n\t#jp-minimap.jp-mm-open .jp-mm-item { padding-top: 12px; padding-bottom: 12px; }\n\t#jp-minimap.jp-mm-open .jp-mm-label { font-size: 1.12em; }\n\t#jp-minimap.jp-mm-open .jp-mm-r0 .jp-mm-label { font-size: 1.2em; }\n\t#jp-minimap.jp-mm-open .jp-mm-r2 .jp-mm-label { font-size: 1.04em; }\n\n\t#jp-minimap.jp-mm-open .jp-mm-head { font-size: 1.04em; gap: 10px; padding-bottom: 8px; }\n\t.jp-mm-step { min-width: 32px; padding: 5px 0; }\n}\n@media print {\n\t#jp-minimap { display: none; }\n}\n";
61
+ var MINIMAP_CSS = "/* Joplin Minimap \u2014 collapsed tick bars, hover-expanded ToC panel.\n * Colors use currentColor / rgba so it follows both light and dark themes.\n */\n\n#jp-minimap {\n\tuser-select: none;\n\t-webkit-user-select: none;\n\tcaret-color: transparent;\n\tcursor: default;\n\tposition: fixed;\n\ttop: 50%;\n\tright: 6px;\n\ttransform: translateY(-50%);\n\tz-index: 9999;\n\tfont-size: var(--jp-mm-font, 12.5px);\n\tline-height: 1.35;\n\tcolor: var(--jp-mm-color, inherit);\n\t/* Collapsed geometry is direction-independent: the tick bars always\n\t * hug the docked edge, whichever side that is, even in an RTL note. Per-item direction applies\n\t * to the expanded panel only (see the dir=rtl rule below). */\n\tdirection: ltr;\n}\n\n.jp-mm-list {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: flex-end;\n\tpadding: 8px 6px;\n\tmax-height: 84vh;\n\toverflow: hidden;\n\tborder-radius: 10px;\n\ttransition: background 0.15s ease, box-shadow 0.15s ease;\n}\n\n.jp-mm-item {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: flex-end;\n\tpadding: 3px 4px;\n\tborder-radius: 6px;\n\ttext-decoration: none;\n\tcolor: inherit;\n\topacity: var(--jp-mm-dim, 0.5);\n\tcursor: pointer;\n\toutline: none;\n\tdirection: ltr;\n}\n\n/* ---- collapsed state: tick bars, width by heading level ---- */\n\n.jp-mm-bar {\n\tdisplay: block;\n\theight: 2px;\n\tborder-radius: 1px;\n\tbackground: currentColor;\n}\n\n.jp-mm-l1 .jp-mm-bar { width: 18px; }\n.jp-mm-l2 .jp-mm-bar { width: 13px; }\n.jp-mm-l3 .jp-mm-bar { width: 9px; }\n.jp-mm-l4 .jp-mm-bar { width: 7px; }\n.jp-mm-l5 .jp-mm-bar { width: 5px; }\n.jp-mm-l6 .jp-mm-bar { width: 5px; }\n\n.jp-mm-label { display: none; }\n\n/* ---- depth stepper (issue #2) ---- */\n\n/* Rows deeper than the current depth are HIDDEN, not removed: keeping the\n * nav element alive is what lets the panel stay open across a depth change,\n * since the whole expansion rides on :hover over #jp-minimap. */\n#jp-minimap[data-depth=\"1\"] .jp-mm-l2,\n#jp-minimap[data-depth=\"1\"] .jp-mm-l3,\n#jp-minimap[data-depth=\"1\"] .jp-mm-l4,\n#jp-minimap[data-depth=\"1\"] .jp-mm-l5,\n#jp-minimap[data-depth=\"1\"] .jp-mm-l6,\n#jp-minimap[data-depth=\"2\"] .jp-mm-l3,\n#jp-minimap[data-depth=\"2\"] .jp-mm-l4,\n#jp-minimap[data-depth=\"2\"] .jp-mm-l5,\n#jp-minimap[data-depth=\"2\"] .jp-mm-l6,\n#jp-minimap[data-depth=\"3\"] .jp-mm-l4,\n#jp-minimap[data-depth=\"3\"] .jp-mm-l5,\n#jp-minimap[data-depth=\"3\"] .jp-mm-l6,\n#jp-minimap[data-depth=\"4\"] .jp-mm-l5,\n#jp-minimap[data-depth=\"4\"] .jp-mm-l6,\n#jp-minimap[data-depth=\"5\"] .jp-mm-l6 { display: none; }\n\n/* The stepper is part of the expanded panel only - collapsed, the minimap\n * stays the same narrow strip of tick marks it has always been. */\n.jp-mm-head { display: none; }\n\n\n/* ---- docked on the left edge ---- */\n\n/* Mirror the collapsed alignment so the tick bars hug the left border.\n * direction is pinned to ltr above, so flex-start is unambiguously the left\n * side whatever language the note is in. Placed BEFORE the :hover rules on\n * purpose: they carry the same specificity, so source order decides. */\n#jp-minimap.jp-mm-left .jp-mm-list { align-items: flex-start; }\n#jp-minimap.jp-mm-left .jp-mm-item { justify-content: flex-start; }\n\n/* The to-do dot mirrors too: docked right it hangs left of the tick bar,\n * docked left it must hang RIGHT of it - otherwise a dotted row pushes its\n * bar 9px inward (4px dot + 5px gap) and the flush bar column goes ragged.\n * order= reorders the flex row without touching the DOM, so the expanded\n * panel can put the dot back in front of the label. */\n#jp-minimap.jp-mm-left .jp-mm-dot {\n\torder: 1;\n\tmargin-inline-start: 5px;\n\tmargin-inline-end: 0;\n}\n\n/* ---- expanded state (hover) ---- */\n\n#jp-minimap.jp-mm-open .jp-mm-list {\n\talign-items: stretch;\n\toverflow-y: auto;\n\toverscroll-behavior: contain;\n\tbackground: var(--jp-mm-bg, rgba(127, 127, 127, 0.16));\n\tbackdrop-filter: blur(10px);\n\t-webkit-backdrop-filter: blur(10px);\n\tbox-shadow: 0 6px 28px rgba(0, 0, 0, 0.28);\n}\n\n#jp-minimap.jp-mm-open .jp-mm-bar { display: none; }\n\n#jp-minimap.jp-mm-open .jp-mm-item { justify-content: flex-start; }\n\n#jp-minimap.jp-mm-open .jp-mm-label {\n\tdisplay: block;\n\tmax-width: var(--jp-mm-width, 240px);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\ttext-align: start;\n}\n\n/* RTL headings (Persian/Arabic/Hebrew) read right-to-left in the expanded\n * panel: justify-content, padding-inline-start and text-align:start all flip\n * with the row direction, so the label hugs the right edge and nested levels\n * indent inward from the right. build() sets dir per row (first strong char). */\n#jp-minimap.jp-mm-open .jp-mm-item[dir=\"rtl\"] { direction: rtl; }\n\n/* indent by heading level when expanded (logical: left in LTR, right in RTL) */\n#jp-minimap.jp-mm-open .jp-mm-l2 { padding-inline-start: 16px; }\n#jp-minimap.jp-mm-open .jp-mm-l3 { padding-inline-start: 28px; }\n#jp-minimap.jp-mm-open .jp-mm-l4 { padding-inline-start: 40px; }\n#jp-minimap.jp-mm-open .jp-mm-l5 { padding-inline-start: 52px; }\n#jp-minimap.jp-mm-open .jp-mm-l6 { padding-inline-start: 52px; }\n\n#jp-minimap.jp-mm-open .jp-mm-head {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tgap: 7px;\n\tpadding: 0 4px 5px;\n\tmargin-bottom: 4px;\n\tborder-bottom: 1px solid rgba(127, 127, 127, 0.3);\n\tfont-size: 0.88em;\n\topacity: 0.8;\n\t/* The control reads the same either way round, and the panel is pinned\n\t * to ltr anyway - keep it out of the per-row bidi logic entirely. */\n\tdirection: ltr;\n}\n\n.jp-mm-step {\n\tmin-width: 15px;\n\ttext-align: center;\n\tborder-radius: 4px;\n\tfont-weight: 700;\n\tcursor: pointer;\n\tbackground: rgba(127, 127, 127, 0.22);\n}\n\n#jp-minimap.jp-mm-open .jp-mm-step:hover { background: rgba(127, 127, 127, 0.45); }\n\n/* Already at the shallowest / deepest level this note has. */\n.jp-mm-step-off { opacity: 0.3; cursor: default; }\n#jp-minimap.jp-mm-open .jp-mm-step-off:hover { background: rgba(127, 127, 127, 0.22); }\n\n.jp-mm-depth { font-variant-numeric: tabular-nums; }\n\n/* ---- heading tiers (issue #2): indentation alone is a weak cue when\n * scanning a long outline. Size and weight only - NEVER a colour. The\n * panel has to stay legible on every Joplin theme, so the palette is\n * limited to currentColor and neutral rgba (see CLAUDE.md). ---- */\n#jp-minimap.jp-mm-open .jp-mm-r0 .jp-mm-label { font-size: 1.08em; font-weight: 600; }\n#jp-minimap.jp-mm-open .jp-mm-r1 .jp-mm-label { font-weight: 500; }\n#jp-minimap.jp-mm-open .jp-mm-r2 .jp-mm-label { font-size: 0.92em; opacity: 0.8; }\n\n\n/* Expanded, the dot reads as a marker BEFORE the title on either edge. */\n#jp-minimap.jp-mm-open.jp-mm-left .jp-mm-dot {\n\torder: 0;\n\tmargin-inline-start: 0;\n\tmargin-inline-end: 5px;\n}\n\n/* ---- high contrast (issue #5) ---- */\n\n/* Still routed through the same variables, so a userstyle.css override wins\n * over the mode rather than fighting it. */\n#jp-minimap.jp-mm-hc .jp-mm-item { opacity: var(--jp-mm-dim, 0.82); }\n#jp-minimap.jp-mm-hc.jp-mm-open .jp-mm-r2 .jp-mm-label { opacity: 0.95; }\n\n#jp-minimap.jp-mm-hc.jp-mm-open .jp-mm-list {\n\tbackground: var(--jp-mm-bg, rgba(127, 127, 127, 0.38));\n\tbox-shadow: 0 6px 28px rgba(0, 0, 0, 0.45);\n}\n\n#jp-minimap.jp-mm-hc.jp-mm-open .jp-mm-head { opacity: 1; }\n#jp-minimap.jp-mm-hc .jp-mm-step { background: rgba(127, 127, 127, 0.4); }\n\n/* ---- pinned (issue #6): a permanent sidebar instead of a hover panel.\n * Top-anchored rather than centred, like an outline pane: centring would\n * move the whole panel every time the depth stepper changes its height. ---- */\n#jp-minimap.jp-mm-pinned {\n\ttop: 12px;\n\ttransform: none;\n}\n\n#jp-minimap.jp-mm-pinned .jp-mm-list {\n\twidth: var(--jp-mm-width, 240px);\n\tbox-sizing: border-box;\n\tmax-height: calc(100vh - 24px);\n\toverflow-x: hidden;\n\tbox-shadow: 0 2px 12px rgba(0, 0, 0, 0.14);\n}\n\n/* A fixed width instead of hugging the longest title: the note text is\n * moved aside by exactly this much, so the width must not change with the\n * depth stepper. Long titles ellipsize inside it. */\n#jp-minimap.jp-mm-pinned .jp-mm-label {\n\tmax-width: none;\n\tmin-width: 0;\n\tflex: 1 1 auto;\n}\n\n/* The header always exists (it carries the pin), with or without a stepper.\n * Side padding keeps the centred stepper clear of the pin in a narrow panel. */\n#jp-minimap.jp-mm-open .jp-mm-head {\n\tposition: relative;\n\tmin-height: 16px;\n\tpadding-left: 22px;\n\tpadding-right: 22px;\n}\n\n.jp-mm-pin {\n\tposition: absolute;\n\tright: 0;\n\ttop: 0;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 18px;\n\theight: 16px;\n\tborder-radius: 4px;\n\tcursor: pointer;\n\topacity: 0.55;\n}\n\n#jp-minimap.jp-mm-open .jp-mm-pin:hover { opacity: 1; background: rgba(127, 127, 127, 0.3); }\n#jp-minimap.jp-mm-pinned .jp-mm-pin { opacity: 1; background: rgba(127, 127, 127, 0.25); }\n.jp-mm-pin svg { display: block; }\n\n/* ---- shared states ---- */\n\n.jp-mm-item:hover {\n\topacity: 1;\n\tbackground: rgba(127, 127, 127, 0.22);\n}\n\n.jp-mm-active { opacity: 1; }\n\n#jp-minimap.jp-mm-open .jp-mm-active {\n\tbackground: rgba(127, 127, 127, 0.18);\n}\n\n/* No scrollbar in the expanded panel: the wheel handler owns scrolling,\n * and a visible scrollbar at the panel edge invites overlay-scrollbar\n * style hover/click interference. */\n.jp-mm-list::-webkit-scrollbar { display: none; }\n.jp-mm-list { scrollbar-width: none; }\n\n/* ---- open-to-do section dot: one muted red dot before the tick bar of\n * any section that contains at least one unchecked checkbox ---- */\n.jp-mm-dot {\n\tdisplay: none;\n\twidth: 4px;\n\theight: 4px;\n\tborder-radius: 50%;\n\tbackground: var(--jp-mm-dot, rgba(205, 97, 85, 0.85));\n\tmargin-inline-end: 5px;\n\tflex: none;\n}\n\n.jp-mm-dot.jp-mm-on { display: block; }\n\n/* don't show over printed/exported output */\n/* ---- touch devices ---- */\n\n/* (hover: none) is the device capability, not a screen width: a tablet with\n * a mouse keeps the desktop sizes, a touchscreen laptop gets these. */\n@media (hover: none) {\n\t/* Collapsed, the whole strip is the tap target - give it room. */\n\t.jp-mm-list { padding: 12px 14px; }\n\t.jp-mm-bar { height: 3px; }\n\n\t/* Expanded, each row has to be hittable on its own. */\n\t#jp-minimap.jp-mm-open .jp-mm-item { padding-top: 12px; padding-bottom: 12px; }\n\t#jp-minimap.jp-mm-open .jp-mm-label { font-size: 1.12em; }\n\t#jp-minimap.jp-mm-open .jp-mm-r0 .jp-mm-label { font-size: 1.2em; }\n\t#jp-minimap.jp-mm-open .jp-mm-r2 .jp-mm-label { font-size: 1.04em; }\n\n\t#jp-minimap.jp-mm-open .jp-mm-head { font-size: 1.04em; gap: 10px; padding-bottom: 8px; }\n\t.jp-mm-step { min-width: 32px; padding: 5px 0; }\n}\n@media print {\n\t#jp-minimap { display: none; }\n\t:root { --jp-mm-reserve: 0px !important; }\n}\n";
51
62
 
52
63
  function ensureStyle() {
53
64
  if (document.getElementById('jp-minimap-style')) return;
@@ -116,7 +127,7 @@
116
127
  return Array.prototype.slice.call(root.querySelectorAll('h1, h2, h3, h4, h5, h6'));
117
128
  }
118
129
 
119
- function jumpTo(index, text) {
130
+ function findHeading(index, text) {
120
131
  var hs = liveHeadings();
121
132
  var h = hs[index];
122
133
  if (!h || (text && (h.textContent || '').trim() !== text)) {
@@ -124,7 +135,74 @@
124
135
  if ((hs[i].textContent || '').trim() === text) { h = hs[i]; break; }
125
136
  }
126
137
  }
127
- if (h && h.isConnected) h.scrollIntoView({ behavior: 'smooth', block: 'start' });
138
+ return h;
139
+ }
140
+
141
+ // A smooth scrollIntoView is fire-and-forget: the browser animates towards
142
+ // an offset worked out when it STARTS, and plenty can cut it short or make
143
+ // it land wrong - the reader nudging the wheel, Joplin re-rendering the note
144
+ // underneath, split view writing scrollTop of its own, or images above the
145
+ // target finishing layout and shifting it. The jump then stops somewhere in
146
+ // between, which is the "clicked H1 but only went up to 4.5" report.
147
+ // Some environments ignore behavior:'smooth' altogether (headless Chromium
148
+ // does, measured) and the jump never moves at all.
149
+ //
150
+ // So the animation is started for the feel, then the heading is watched
151
+ // until it stops moving and an INSTANT scroll closes whatever gap is left.
152
+ // The instant call uses the same alignment, so when the smooth scroll did
153
+ // land correctly it is a no-op.
154
+ var jumpWatch = null;
155
+ var jumpRelease = null;
156
+
157
+ function stopJumpWatch() {
158
+ if (jumpWatch) { clearInterval(jumpWatch); jumpWatch = null; }
159
+ if (jumpRelease) { jumpRelease(); jumpRelease = null; }
160
+ }
161
+
162
+ function watchJump(index, text) {
163
+ stopJumpWatch();
164
+
165
+ // The reader always wins: if they scroll themselves we are no longer
166
+ // correcting a broken jump, we are fighting them.
167
+ var takenOver = false;
168
+ function onUserScroll(e) {
169
+ // Wheeling the ToC list itself is not the reader taking over the note -
170
+ // the panel has its own wheel handler, and clicking a row then scrolling
171
+ // the list to pick another is a normal sequence.
172
+ var t = e && e.target;
173
+ if (t && t.closest && t.closest('#jp-minimap')) return;
174
+ takenOver = true;
175
+ }
176
+ document.addEventListener('wheel', onUserScroll, { passive: true, capture: true });
177
+ document.addEventListener('touchstart', onUserScroll, { passive: true, capture: true });
178
+ jumpRelease = function () {
179
+ document.removeEventListener('wheel', onUserScroll, { capture: true });
180
+ document.removeEventListener('touchstart', onUserScroll, { capture: true });
181
+ };
182
+
183
+ var ticks = 0;
184
+ var prevTop = null;
185
+ jumpWatch = setInterval(function () {
186
+ ticks++;
187
+ if (takenOver) { stopJumpWatch(); return; }
188
+ var h = findHeading(index, text);
189
+ // A rebuild can briefly detach it; keep waiting, within reason.
190
+ if (!h || !h.isConnected) { if (ticks > 20) stopJumpWatch(); return; }
191
+ var top = Math.round(h.getBoundingClientRect().top);
192
+ var settled = prevTop !== null && top === prevTop;
193
+ prevTop = top;
194
+ if (settled || ticks > 20) {
195
+ stopJumpWatch();
196
+ h.scrollIntoView({ block: 'start' });
197
+ }
198
+ }, 70);
199
+ }
200
+
201
+ function jumpTo(index, text) {
202
+ var h = findHeading(index, text);
203
+ if (!h || !h.isConnected) return;
204
+ h.scrollIntoView({ behavior: 'smooth', block: 'start' });
205
+ watchJump(index, text);
128
206
  }
129
207
 
130
208
  // Width of the note viewer's right-edge scrollbar zone. Overlay scrollbars
@@ -145,6 +223,30 @@
145
223
  return gap > 0 ? gap : 14;
146
224
  }
147
225
 
226
+ // Pads the note container on the docked side until its content clears the
227
+ // pinned panel. Returns the undo, or null when nothing was reserved. Both
228
+ // rects are viewport-relative and the panel is position:fixed.
229
+ function reserveSpace(el, nav, side) {
230
+ var prop = side === 'left' ? 'padding-left' : 'padding-right';
231
+ var prevInline = el.style.getPropertyValue(prop);
232
+ var base = parseFloat(window.getComputedStyle(el).getPropertyValue(prop)) || 0;
233
+ var er = el.getBoundingClientRect();
234
+ var nr = nav.getBoundingClientRect();
235
+ var need = side === 'left' ? (nr.right + 12 - er.left - base) : (er.right - nr.left + 12 - base);
236
+ if (need <= 0) return null;
237
+ // Keep a readable column: in a narrow split view the pinned panel is
238
+ // better left overlapping than squeezing the note to a sliver.
239
+ var other = parseFloat(window.getComputedStyle(el).getPropertyValue(side === 'left' ? 'padding-right' : 'padding-left')) || 0;
240
+ if (er.width - other - base - need < 320) return null;
241
+ document.documentElement.style.setProperty('--jp-mm-reserve', Math.ceil(need) + 'px');
242
+ el.style.setProperty(prop, 'calc(' + base + 'px + var(--jp-mm-reserve, 0px))');
243
+ return function () {
244
+ if (prevInline) el.style.setProperty(prop, prevInline);
245
+ else el.style.removeProperty(prop);
246
+ document.documentElement.style.removeProperty('--jp-mm-reserve');
247
+ };
248
+ }
249
+
148
250
  function build() {
149
251
  if (cleanup) { cleanup(); cleanup = null; }
150
252
  if (isEditableContext()) {
@@ -191,6 +293,8 @@
191
293
  }
192
294
 
193
295
 
296
+ var pinned = pinOverride !== null ? pinOverride : settings.pinned === true;
297
+
194
298
  var nav = document.createElement('nav');
195
299
  nav.id = 'jp-minimap';
196
300
  // Applied in BOTH collapsed and expanded states: shifting only on hover
@@ -202,6 +306,9 @@
202
306
  // never sits on the left, so no gap is added there.
203
307
  var navClasses = [];
204
308
  if (settings.highContrast === true) navClasses.push('jp-mm-hc');
309
+ // Pinned is just "open, and never told to close": every close path goes
310
+ // through setOpen, which refuses while pinned.
311
+ if (pinned) navClasses.push('jp-mm-pinned', 'jp-mm-open');
205
312
  if (settings.side === 'left') {
206
313
  navClasses.push('jp-mm-left');
207
314
  nav.style.left = settings.rightOffset + 'px';
@@ -240,6 +347,7 @@
240
347
  }, { passive: false });
241
348
 
242
349
  function setOpen(open) {
350
+ if (pinned && !open) return;
243
351
  nav.classList.toggle('jp-mm-open', open);
244
352
  // Drop the stepper's anchoring so the collapsed strip re-centres.
245
353
  if (!open) { nav.style.top = ''; nav.style.transform = ''; }
@@ -284,13 +392,14 @@
284
392
  // cursor over #jp-minimap, so the hover-expanded panel never collapses
285
393
  // mid-interaction and no click-to-pin state has to be invented. Pointless
286
394
  // when the note only has one heading level, so it is omitted entirely.
287
- var head = null;
395
+ // The header always exists now: it carries the pin button (issue #6),
396
+ // and a note with a single heading level still needs a way to pin.
397
+ var head = document.createElement('div');
398
+ head.className = 'jp-mm-head';
288
399
  var minusBtn = null;
289
400
  var plusBtn = null;
290
401
  var depthText = null;
291
402
  if (levels.length > 1) {
292
- head = document.createElement('div');
293
- head.className = 'jp-mm-head';
294
403
  minusBtn = stepButton('-', -1);
295
404
  depthText = document.createElement('span');
296
405
  depthText.className = 'jp-mm-depth';
@@ -298,8 +407,33 @@
298
407
  head.appendChild(minusBtn);
299
408
  head.appendChild(depthText);
300
409
  head.appendChild(plusBtn);
301
- list.appendChild(head);
302
- renderHead();
410
+ }
411
+ head.appendChild(pinButton());
412
+ list.appendChild(head);
413
+ renderHead();
414
+
415
+ // currentColor SVG rather than the pushpin emoji: an emoji brings its own
416
+ // colours, and the panel stays within currentColor/neutral rgba so it
417
+ // reads on every theme (see CLAUDE.md).
418
+ function pinButton() {
419
+ var b = document.createElement('span');
420
+ b.className = 'jp-mm-pin';
421
+ b.title = pinned ? settings.unpinTip : settings.pinTip;
422
+ b.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">'
423
+ + '<path d="M15 2.5l6.5 6.5-1.8 1.1-2.4-.6-3.6 3.6.5 4.4-1.7 1.7-3.9-3.9-5.1 5.1-1.4-1.4 5.1-5.1-3.9-3.9 1.7-1.7 4.4.5 3.6-3.6-.6-2.4z"/></svg>';
424
+ // mousedown, like the rows and the stepper: a click would be lost to the
425
+ // re-render the press can trigger.
426
+ b.addEventListener('mousedown', function (e) {
427
+ if (e.button !== 0) return;
428
+ e.preventDefault();
429
+ e.stopPropagation();
430
+ pinOverride = !pinned;
431
+ if (typeof webviewApi !== 'undefined' && webviewApi.postMessage) {
432
+ webviewApi.postMessage('joplin-minimap', { type: 'setPinned', value: pinOverride }).catch(function () {});
433
+ }
434
+ build();
435
+ });
436
+ return b;
303
437
  }
304
438
 
305
439
  function stepButton(sign, delta) {
@@ -319,7 +453,7 @@
319
453
  }
320
454
 
321
455
  function renderHead() {
322
- if (!head) return;
456
+ if (!depthText) return;
323
457
  var lo = levels[0];
324
458
  depthText.textContent = lo === maxLevel ? ('H' + lo) : ('H' + lo + '-H' + maxLevel);
325
459
  minusBtn.classList.toggle('jp-mm-step-off', maxLevel === lo);
@@ -341,7 +475,8 @@
341
475
  renderHead();
342
476
  applyTodoDots();
343
477
  var afterY = head.getBoundingClientRect().top;
344
- if (afterY !== beforeY) {
478
+ // Pinned is top-anchored, so the header cannot drift there.
479
+ if (!pinned && afterY !== beforeY) {
345
480
  var box = nav.getBoundingClientRect();
346
481
  var limit = window.innerHeight - box.height - 4;
347
482
  var top = box.top + (beforeY - afterY);
@@ -412,6 +547,14 @@
412
547
  nav.appendChild(list);
413
548
  document.body.appendChild(nav);
414
549
 
550
+ // Pinned: move the note text aside so the sidebar never covers it (the
551
+ // third point of issue #6). Padding is added on top of whatever the
552
+ // container already has, and the amount lives in a variable so the print
553
+ // stylesheet can zero it. Skipped when the viewer is too narrow to leave a
554
+ // readable column (see reserveSpace).
555
+ var releaseReserve = null;
556
+ if (pinned) releaseReserve = reserveSpace(root, nav, settings.side === 'left' ? 'left' : 'right');
557
+
415
558
  // Open to-dos in a section the stepper has collapsed away roll up to the
416
559
  // nearest visible ancestor. Without this, lowering the depth silently
417
560
  // hides the only signal that a branch still holds unfinished work - which
@@ -445,6 +588,14 @@
445
588
  for (var j = 0; j < items.length; j++) {
446
589
  items[j].classList.toggle('jp-mm-active', j === activeRow);
447
590
  }
591
+ // A pinned outline follows the reading position like an outline pane.
592
+ // scrollTop on the list only - scrollIntoView would scroll the note too.
593
+ if (pinned && items[activeRow]) {
594
+ var row = items[activeRow];
595
+ if (row.offsetTop < list.scrollTop || row.offsetTop + row.offsetHeight > list.scrollTop + list.clientHeight) {
596
+ list.scrollTop = Math.max(0, row.offsetTop - list.clientHeight * 0.3);
597
+ }
598
+ }
448
599
  }
449
600
 
450
601
  document.addEventListener('scroll', updateActive, { passive: true, capture: true });
@@ -460,6 +611,7 @@
460
611
  cleanup = function () {
461
612
  document.removeEventListener('scroll', updateActive, { capture: true });
462
613
  if (closeOnOutside) document.removeEventListener('touchstart', closeOnOutside);
614
+ if (releaseReserve) releaseReserve();
463
615
  };
464
616
  }
465
617
 
Binary file