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 +2 -0
- package/dist/{chunk-XMGAWBJ5.js → chunk-OXJMUUGP.js} +4 -2
- package/dist/index.js +1 -1
- package/dist/plugins/starlight-dom-patches/page-script.d.ts +2 -1
- package/dist/plugins/starlight-dom-patches/page-script.js +154 -6
- package/dist/plugins/starlight-dom-patches.d.ts +6 -0
- package/dist/plugins/starlight-dom-patches.js +1 -1
- package/dist/styles/_starlight.scss +83 -0
- package/package.json +1 -1
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
|
@@ -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
|
|
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
|
-
|
|
33
|
-
if (
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
@@ -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