starlight-cannoli-plugins 2.7.1 → 2.8.1

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
@@ -399,6 +399,7 @@ An Astro integration that injects a client-side script to apply opt-in DOM patch
399
399
  - `hideSingleLineGutters` (optional, default: `false`): Hides the line number gutter on Expressive Code blocks that contain only a single line.
400
400
  - `syncTocLabelsFromHeadings` (optional, default: `false`): Copies the rendered HTML of each heading into its matching Starlight TOC anchor label, so the TOC properly reflects any custom markup (e.g. math) present in the heading.
401
401
  - `wrapDetailsContent` (optional, default: `false`): Wraps the content of every `<details>` element (excluding its `<summary>`) in a `<div class="details-wrapper">`, useful for applying consistent spacing or animation styles.
402
+ - `offerTabbedContent` (optional, default: `false`): Injects a "Tabbed view" toggle checkbox immediately after `#starlight__on-this-page` in the right sidebar. When enabled by the user, the page's markdown content is reorganised into tabs — one per `<h2>` heading, plus an optional "Main" tab for any content that appears before the first `<h2>`. The toggle state is persisted to `localStorage`; active tab selection is not persisted. Only activates when the page has at least two sections (i.e. two or more `<h2>` elements, or one `<h2>` with pre-heading content). Has no effect on pages that lack `#starlight__on-this-page` (e.g. pages with the TOC disabled). Clicking a TOC anchor while tabs are enabled automatically switches to the tab containing the target heading. The generated elements use the classes `tabbed-content`, `tabbed-content-nav`, `tabbed-content-tab`, and `tabbed-content-panel` for styling; toggle button uses the existing `.toggle-checkbox-btn` class.
402
403
 
403
404
  **Usage:**
404
405
 
@@ -414,6 +415,7 @@ export default defineConfig({
414
415
  hideSingleLineGutters: true,
415
416
  syncTocLabelsFromHeadings: true,
416
417
  wrapDetailsContent: true,
418
+ offerTabbedContent: true,
417
419
  }),
418
420
  starlight({ title: "My Docs" }),
419
421
  ],
@@ -4,7 +4,8 @@ function starlightDomPatches(options = {}) {
4
4
  const {
5
5
  hideSingleLineGutters = false,
6
6
  syncTocLabelsFromHeadings = false,
7
- wrapDetailsContent = false
7
+ wrapDetailsContent = false,
8
+ offerTabbedContent = false
8
9
  } = options;
9
10
  return {
10
11
  name: "starlight-dom-patches",
@@ -29,7 +30,8 @@ function starlightDomPatches(options = {}) {
29
30
  const imports = [
30
31
  hideSingleLineGutters ? "hideSingleLineGutters" : null,
31
32
  syncTocLabelsFromHeadings ? "syncTocLabelsFromHeadings" : null,
32
- wrapDetailsContent ? "wrapDetailsContent" : null
33
+ wrapDetailsContent ? "wrapDetailsContent" : null,
34
+ offerTabbedContent ? "tabbedH2Content" : null
33
35
  ].filter(Boolean);
34
36
  if (imports.length === 0) return;
35
37
  const calls = imports.map((fn) => `${fn}();`).join("\n");
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  } from "./chunk-NCXV367P.js";
25
25
  import {
26
26
  starlightDomPatches
27
- } from "./chunk-XMGAWBJ5.js";
27
+ } from "./chunk-OXJMUUGP.js";
28
28
  import "./chunk-4VNS5WPM.js";
29
29
 
30
30
  // src/plugins/starlight-index-sourced-sidebar/index.ts
@@ -1,5 +1,6 @@
1
1
  declare function hideSingleLineGutters(): void;
2
2
  declare function syncTocLabelsFromHeadings(): void;
3
+ declare function tabbedH2Content(): void;
3
4
  declare function wrapDetailsContent(): void;
4
5
 
5
- export { hideSingleLineGutters, syncTocLabelsFromHeadings, wrapDetailsContent };
6
+ export { hideSingleLineGutters, syncTocLabelsFromHeadings, tabbedH2Content, wrapDetailsContent };
@@ -16,6 +16,12 @@ function hideSingleLineGutters() {
16
16
  }
17
17
  });
18
18
  }
19
+ function headingInnerHTML(heading) {
20
+ if (heading.childElementCount === 1 && heading.children[0].tagName === "STRONG") {
21
+ return heading.children[0].innerHTML;
22
+ }
23
+ return heading.innerHTML;
24
+ }
19
25
  function syncTocLabelsFromHeadings() {
20
26
  document.querySelectorAll("starlight-toc ul li > a, mobile-starlight-toc ul li > a").forEach((anchor) => {
21
27
  const href = anchor.getAttribute("href") ?? "";
@@ -27,13 +33,154 @@ function syncTocLabelsFromHeadings() {
27
33
  if (!heading) return;
28
34
  const span = anchor.querySelector(":scope > span");
29
35
  if (!span) return;
30
- span.innerHTML = heading.innerHTML;
36
+ span.innerHTML = headingInnerHTML(heading);
37
+ });
38
+ }
39
+ function tabbedH2Content() {
40
+ console.log("[tabbedH2Content] running");
41
+ const LS_KEY = "starlight-dom-patches:tabbed-content";
42
+ const container = document.querySelector(
43
+ ".main-pane .sl-markdown-content"
44
+ );
45
+ if (!container) {
46
+ console.log(
47
+ "[tabbedH2Content] no .main-pane .sl-markdown-content found \u2014 aborting"
48
+ );
49
+ return;
50
+ }
51
+ const tocNav = document.querySelector(
52
+ "nav[aria-labelledby='starlight__on-this-page']"
53
+ );
54
+ if (!tocNav) {
55
+ console.log(
56
+ "[tabbedH2Content] nav[aria-labelledby='starlight__on-this-page'] not found \u2014 aborting"
57
+ );
58
+ return;
59
+ }
60
+ const children = Array.from(container.children);
61
+ if (children.length === 0) {
62
+ console.log("[tabbedH2Content] content container is empty \u2014 aborting");
63
+ return;
64
+ }
65
+ console.log(`[tabbedH2Content] container element:`, container);
66
+ console.log(
67
+ `[tabbedH2Content] found ${children.length} children:`,
68
+ children.map((c) => c.tagName).join(", ")
69
+ );
70
+ const preH2Nodes = [];
71
+ const h2Sections = [];
72
+ let currentSection = null;
73
+ const isH2Wrapper = (el) => el.tagName === "DIV" && el.classList.contains("sl-heading-wrapper") && el.classList.contains("level-h2");
74
+ for (const child of children) {
75
+ if (isH2Wrapper(child)) {
76
+ if (currentSection) h2Sections.push(currentSection);
77
+ const h2 = child.querySelector("h2");
78
+ currentSection = {
79
+ label: h2 ? headingInnerHTML(h2) : "",
80
+ nodes: [child]
81
+ };
82
+ } else if (currentSection === null) {
83
+ preH2Nodes.push(child);
84
+ } else {
85
+ currentSection.nodes.push(child);
86
+ }
87
+ }
88
+ if (currentSection) h2Sections.push(currentSection);
89
+ const hasPreContent = preH2Nodes.length > 0;
90
+ const totalSections = (hasPreContent ? 1 : 0) + h2Sections.length;
91
+ console.log(
92
+ `[tabbedH2Content] preH2Nodes: ${preH2Nodes.length}, h2Sections: ${h2Sections.length}, totalSections: ${totalSections}`
93
+ );
94
+ if (totalSections <= 1) {
95
+ console.log(
96
+ "[tabbedH2Content] only one section \u2014 aborting (no tabs needed)"
97
+ );
98
+ return;
99
+ }
100
+ const allSections = [];
101
+ if (hasPreContent) allSections.push({ label: "Main", nodes: preH2Nodes });
102
+ allSections.push(...h2Sections);
103
+ const wrapper = document.createElement("div");
104
+ wrapper.className = "tabbed-content";
105
+ const nav = document.createElement("div");
106
+ nav.className = "tabbed-content-nav not-content";
107
+ const tabButtons = [];
108
+ const panels = [];
109
+ allSections.forEach((section, i) => {
110
+ const btn = document.createElement("button");
111
+ btn.className = "tabbed-content-tab";
112
+ btn.innerHTML = section.label;
113
+ btn.dataset.tab = String(i);
114
+ tabButtons.push(btn);
115
+ nav.appendChild(btn);
116
+ const panel = document.createElement("div");
117
+ panel.className = "tabbed-content-panel";
118
+ panel.dataset.panel = String(i);
119
+ section.nodes.forEach((node) => panel.appendChild(node));
120
+ panels.push(panel);
121
+ });
122
+ wrapper.appendChild(nav);
123
+ panels.forEach((p) => wrapper.appendChild(p));
124
+ container.appendChild(wrapper);
125
+ let activeTab = 0;
126
+ function activateTab(index) {
127
+ activeTab = index;
128
+ tabButtons.forEach((btn, i) => {
129
+ btn.dataset.active = String(i === index);
130
+ });
131
+ panels.forEach((panel, i) => {
132
+ panel.hidden = i !== index;
133
+ });
134
+ }
135
+ function setEnabled(enabled) {
136
+ wrapper.dataset.enabled = String(enabled);
137
+ nav.hidden = !enabled;
138
+ if (enabled) {
139
+ activateTab(activeTab);
140
+ } else {
141
+ panels.forEach((panel) => {
142
+ panel.hidden = false;
143
+ });
144
+ }
145
+ }
146
+ tabButtons.forEach((btn, i) => {
147
+ btn.addEventListener("click", () => {
148
+ if (wrapper.dataset.enabled === "true") activateTab(i);
149
+ });
31
150
  });
32
- document.querySelectorAll("starlight-toc ul > li > a > span").forEach((span) => {
33
- if (span.childElementCount !== 1) return;
34
- const child = span.children[0];
35
- if (child.tagName !== "STRONG") return;
36
- span.innerHTML = child.innerHTML;
151
+ function navigateToHash(hash) {
152
+ if (!hash || wrapper.dataset.enabled !== "true") return;
153
+ const id = hash.startsWith("#") ? hash.slice(1) : hash;
154
+ panels.forEach((panel, i) => {
155
+ if (panel.querySelector(`#${CSS.escape(id)}`)) activateTab(i);
156
+ });
157
+ }
158
+ window.addEventListener(
159
+ "hashchange",
160
+ () => navigateToHash(window.location.hash)
161
+ );
162
+ if (window.location.hash) navigateToHash(window.location.hash);
163
+ const toggleLabel = document.createElement("label");
164
+ toggleLabel.className = "toggle-checkbox-btn";
165
+ const checkbox = document.createElement("input");
166
+ checkbox.type = "checkbox";
167
+ const toggleText = document.createElement("span");
168
+ toggleText.textContent = "Tabbed view";
169
+ toggleLabel.appendChild(checkbox);
170
+ toggleLabel.appendChild(toggleText);
171
+ tocNav.parentNode.insertBefore(toggleLabel, tocNav);
172
+ console.log(
173
+ "[tabbedH2Content] toggle checkbox injected after #starlight__on-this-page"
174
+ );
175
+ const initialEnabled = localStorage.getItem(LS_KEY) === "enabled";
176
+ console.log(
177
+ `[tabbedH2Content] localStorage value: ${localStorage.getItem(LS_KEY)}, initialEnabled: ${initialEnabled}`
178
+ );
179
+ checkbox.checked = initialEnabled;
180
+ setEnabled(initialEnabled);
181
+ checkbox.addEventListener("change", () => {
182
+ setEnabled(checkbox.checked);
183
+ localStorage.setItem(LS_KEY, checkbox.checked ? "enabled" : "disabled");
37
184
  });
38
185
  }
39
186
  function wrapDetailsContent() {
@@ -67,5 +214,6 @@ function wrapDetailsContent() {
67
214
  export {
68
215
  hideSingleLineGutters,
69
216
  syncTocLabelsFromHeadings,
217
+ tabbedH2Content,
70
218
  wrapDetailsContent
71
219
  };
@@ -7,6 +7,12 @@ interface DomPatchesOptions {
7
7
  syncTocLabelsFromHeadings?: boolean;
8
8
  /** Wrap `<details>` content (excluding `<summary>`) in a `.details-wrapper` div. @default false */
9
9
  wrapDetailsContent?: boolean;
10
+ /**
11
+ * Inject a toggle checkbox (after `#starlight__on-this-page`) that reorganises page content
12
+ * into tabs driven by `<h2>` boundaries. Toggle state is persisted to localStorage.
13
+ * @default false
14
+ */
15
+ offerTabbedContent?: boolean;
10
16
  }
11
17
  /**
12
18
  * Astro integration that injects a client-side script to apply DOM patches
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  starlightDomPatches
3
- } from "../chunk-XMGAWBJ5.js";
3
+ } from "../chunk-OXJMUUGP.js";
4
4
  import "../chunk-4VNS5WPM.js";
5
5
  export {
6
6
  starlightDomPatches
@@ -133,6 +133,55 @@ div > div[class="page"] > svg {
133
133
  }
134
134
  }
135
135
 
136
+ /********** Tabbed H2 Content **********/
137
+ .tabbed-content-nav {
138
+ display: flex;
139
+ flex-wrap: wrap;
140
+ gap: 0.375rem;
141
+ margin-bottom: 1.75rem;
142
+ max-height: 9rem;
143
+ overflow-y: auto;
144
+ }
145
+
146
+ .tabbed-content-tab {
147
+ padding: 0.3rem 0.85rem;
148
+ border: none;
149
+ border-radius: 9999px;
150
+ background-color: var(--sl-color-gray-6);
151
+ color: var(--sl-color-gray-3);
152
+ cursor: pointer;
153
+ font-size: 0.8125em;
154
+ font-weight: 500;
155
+ transition:
156
+ color 0.15s ease,
157
+ background-color 0.15s ease;
158
+
159
+ &:hover {
160
+ background-color: var(--sl-color-gray-5);
161
+ color: var(--sl-color-text);
162
+ }
163
+
164
+ &[data-active="true"] {
165
+ background-color: var(--sl-color-accent-high);
166
+ color: var(--sl-color-black);
167
+ }
168
+
169
+ html[data-theme="light"] & {
170
+ background-color: var(--sl-color-gray-7);
171
+ color: var(--sl-color-gray-4);
172
+
173
+ &:hover {
174
+ background-color: var(--sl-color-gray-6);
175
+ color: var(--sl-color-gray-2);
176
+ }
177
+
178
+ &[data-active="true"] {
179
+ background-color: var(--sl-color-accent-low);
180
+ color: var(--sl-color-white);
181
+ }
182
+ }
183
+ }
184
+
136
185
  /********** Toggle Checkbox Styles **********/
137
186
  #toggle-all-details-btn {
138
187
  display: flex;
@@ -167,6 +216,40 @@ div > div[class="page"] > svg {
167
216
  }
168
217
  }
169
218
 
219
+ .toggle-checkbox-btn {
220
+ display: flex;
221
+ align-items: center;
222
+ gap: 0.5rem;
223
+ margin: 0.2rem 0;
224
+ cursor: pointer;
225
+
226
+ input[type="checkbox"] {
227
+ position: relative;
228
+ appearance: none;
229
+ width: 1.2em;
230
+ height: 1.2em;
231
+ border: 2px solid currentColor;
232
+ border-radius: 2px;
233
+ cursor: pointer;
234
+ display: flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ transition: background-color 0.2s ease;
238
+
239
+ &:checked {
240
+ background-color: var(--sl-color-accent);
241
+ border-color: var(--sl-color-accent);
242
+
243
+ &::after {
244
+ content: "✓";
245
+ color: white;
246
+ font-size: 0.8em;
247
+ font-weight: bold;
248
+ }
249
+ }
250
+ }
251
+ }
252
+
170
253
  // invert img.note-svg when on dark mode
171
254
  img.note-svg {
172
255
  padding-top: 1em;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "starlight-cannoli-plugins",
3
3
  "type": "module",
4
- "version": "2.7.1",
4
+ "version": "2.8.1",
5
5
  "description": "Starlight plugins for automatic sidebar generation and link validation",
6
6
  "license": "ISC",
7
7
  "main": "./dist/index.js",