1---
2/**
3 * Drop-in replacement for Starlight's internal `SidebarSublist`, keeping its
4 * markup, classes, and persistence hooks, plus one addition: group headings
5 * get a monochrome icon resolved from the label (lucide concepts, official
6 * simple-icons brand marks for providers) — see `src/docs-icons.ts`.
7 * The mockup styling itself (small-caps headings, accent bar) lives in
8 * `styles/custom.css`.
9 */
10import { Badge } from "@astrojs/starlight/components";
11import type { StarlightRouteData } from "@astrojs/starlight/route-data";
12import { sidebarGroupIcon } from "../../docs-icons";
13
14type SidebarEntry = StarlightRouteData["sidebar"][number];
15
16interface Props {
17 sublist: SidebarEntry[];
18 nested?: boolean;
19}
20
21const { sublist, nested } = Astro.props;
22
23const isCurrentDeep = (entries: SidebarEntry[]): boolean =>
24 entries.some((entry) =>
25 entry.type === "link" ? entry.isCurrent : isCurrentDeep(entry.entries),
26 );
27
28// Mirror of Starlight's internal SidebarRestorePoint: a running per-request
29// index that `SidebarPersister` uses to persist <details> open state.
30const currentGroupIndexSymbol = Symbol.for("starlight-sidebar-group-index");
31const locals = Astro.locals as App.Locals & {
32 [currentGroupIndexSymbol]?: number;
33};
34const nextGroupIndex = () => {
35 const index = locals[currentGroupIndexSymbol] || 0;
36 locals[currentGroupIndexSymbol] = index + 1;
37 return index;
38};
39---
40
41<ul class:list={{ "top-level": !nested }}>
42 {
43 sublist.map((entry) => (
44 <li>
45 {entry.type === "link" ? (
46 <a
47 href={entry.href}
48 aria-current={entry.isCurrent && "page"}
49 class:list={[{ large: !nested }, entry.attrs.class]}
50 {...entry.attrs}
51 >
52 <span>{entry.label}</span>
53 {entry.badge && (
54 <Badge
55 variant={entry.badge.variant}
56 class={entry.badge.class}
57 text={entry.badge.text}
58 />
59 )}
60 </a>
61 ) : (
62 <details open={isCurrentDeep(entry.entries) || !entry.collapsed}>
63 <sl-sidebar-restore data-index={nextGroupIndex()} />
64 <summary>
65 <div class="group-label">
66 {sidebarGroupIcon(entry.label) && (
67 <svg
68 class="group-icon"
69 aria-hidden="true"
70 width="13"
71 height="13"
72 viewBox="0 0 24 24"
73 set:html={sidebarGroupIcon(entry.label)}
74 />
75 )}
76 <span class="large">{entry.label}</span>
77 {entry.badge && (
78 <Badge
79 variant={entry.badge.variant}
80 class={entry.badge.class}
81 text={entry.badge.text}
82 />
83 )}
84 </div>
85 <svg
86 aria-hidden="true"
87 class="caret"
88 width="16"
89 height="16"
90 viewBox="0 0 24 24"
91 fill="currentColor"
92 >
93 <path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z" />
94 </svg>
95 </summary>
96 <Astro.self sublist={entry.entries} nested />
97 </details>
98 )}
99 </li>
100 ))
101 }
102</ul>
103
104<style>
105 ul {
106 --sl-sidebar-item-padding-inline: 0.5rem;
107 list-style: none;
108 padding: 0;
109 }
110
111 li {
112 overflow-wrap: anywhere;
113 }
114
115 ul ul li {
116 margin-inline-start: var(--sl-sidebar-item-padding-inline);
117 border-inline-start: 1px solid var(--sl-color-hairline-light);
118 padding-inline-start: var(--sl-sidebar-item-padding-inline);
119 }
120
121 .large {
122 font-size: var(--sl-text-lg);
123 font-weight: 600;
124 color: var(--sl-color-white);
125 }
126
127 .top-level > li + li {
128 margin-top: 0.75rem;
129 }
130
131 summary {
132 display: flex;
133 align-items: center;
134 justify-content: space-between;
135 padding: 0.2em var(--sl-sidebar-item-padding-inline);
136 line-height: 1.4;
137 /* WCAG 2.2 target size: at least 24px tall. */
138 min-height: 24px;
139 cursor: pointer;
140 user-select: none;
141 }
142 summary::marker,
143 summary::-webkit-details-marker {
144 display: none;
145 }
146
147 .group-label {
148 display: flex;
149 align-items: center;
150 }
151 .group-icon {
152 flex-shrink: 0;
153 color: var(--alc-fg-4);
154 }
155
156 .caret {
157 transition: transform 0.2s ease-in-out;
158 flex-shrink: 0;
159 }
160 :global([dir="rtl"]) .caret {
161 transform: rotateZ(180deg);
162 }
163 [open] > summary .caret {
164 transform: rotateZ(90deg);
165 }
166
167 a {
168 display: block;
169 border-radius: 0.25rem;
170 text-decoration: none;
171 color: var(--sl-color-gray-2);
172 padding: 0.3em var(--sl-sidebar-item-padding-inline);
173 line-height: 1.4;
174 }
175
176 a:hover,
177 a:focus {
178 color: var(--sl-color-white);
179 }
180
181 [aria-current="page"],
182 [aria-current="page"]:hover,
183 [aria-current="page"]:focus {
184 font-weight: 600;
185 color: var(--sl-color-text-invert);
186 background-color: var(--sl-color-text-accent);
187 }
188
189 a > :global(*:not(:last-child)),
190 .group-label > :global(*:not(:last-child)) {
191 margin-inline-end: 0.35em;
192 }
193
194 @media (min-width: 50rem) {
195 .top-level > li + li {
196 margin-top: 0.5rem;
197 }
198 .large {
199 font-size: var(--sl-text-base);
200 }
201 a {
202 font-size: var(--sl-text-sm);
203 }
204 }
205</style>