1---
2/**
3 * Mobile product switcher — the docs tab bar's counterpart inside the
4 * hamburger drawer. Desktop hides it (the horizontal tab bar owns
5 * navigation there); on mobile the tab bar is hidden and this switcher
6 * is how you move between hubs.
7 *
8 * A disclosure (<details>), not a native <select>, so brand marks and
9 * the More-menu category headings render exactly like the desktop
10 * dropdown. Picking a product navigates to that hub's landing page.
11 */
12import { DOCS_TABS, activeTab } from "../../docs-tabs";
13import { TAB_ICONS } from "../../docs-icons";
14
15const current = activeTab(Astro.url.pathname);
16
17/**
18 * Mobile taxonomy. The desktop More ▾ categories only classify the
19 * overflow residents; here the whole tab list renders at once, so the
20 * grouping is redesigned — e.g. PlanetScale, Neon, and Prisma belong
21 * under Data next to SQL, not in the ungrouped primary run.
22 */
23const MOBILE_GROUPS: { heading?: string; labels: string[] }[] = [
24 { labels: ["Core", "CLI"] },
25 { heading: "Clouds", labels: ["Cloudflare", "AWS"] },
26 { heading: "Data", labels: ["PlanetScale", "Neon", "Prisma", "SQL"] },
27 { heading: "Containers", labels: ["Docker", "Kubernetes"] },
28 { heading: "Observability", labels: ["Axiom"] },
29 { heading: "Source & CI", labels: ["GitHub", "Git"] },
30 { heading: "Toolchain", labels: ["Command"] },
31 { heading: "Meta", labels: ["Reference", "Blog", "Compare"] },
32];
33
34const byLabel = new Map(DOCS_TABS.map((tab) => [tab.label, tab]));
35const grouped = MOBILE_GROUPS.map((group) => ({
36 heading: group.heading,
37 tabs: group.labels.flatMap((label) => byLabel.get(label) ?? []),
38})).filter((group) => group.tabs.length > 0);
39
40// Any tab the taxonomy doesn't name still renders (trailing, ungrouped)
41// so a newly added provider can never silently vanish from mobile.
42const covered = new Set(MOBILE_GROUPS.flatMap((group) => group.labels));
43const leftover = DOCS_TABS.filter((tab) => !covered.has(tab.label));
44if (leftover.length > 0) grouped.push({ heading: "More", tabs: leftover });
45---
46
47<details class="alc-switcher">
48 <summary class="alc-switcher__current">
49 {
50 TAB_ICONS[current.label] && (
51 <svg
52 class="alc-switcher__icon"
53 aria-hidden="true"
54 width="16"
55 height="16"
56 viewBox="0 0 24 24"
57 set:html={TAB_ICONS[current.label]}
58 />
59 )
60 }
61 <span>{current.label}</span>
62 <svg
63 class="alc-switcher__chevron"
64 aria-hidden="true"
65 width="12"
66 height="12"
67 viewBox="0 0 24 24"
68 fill="none"
69 stroke="currentColor"
70 stroke-width="2"
71 stroke-linecap="round"
72 stroke-linejoin="round"
73 >
74 <path d="m6 9 6 6 6-6" />
75 </svg>
76 </summary>
77 <div class="alc-switcher__panel">
78 {
79 grouped.map((group) => (
80 <>
81 {group.heading && (
82 <div class="alc-switcher__heading">{group.heading}</div>
83 )}
84 {group.tabs.map((tab) => (
85 <a
86 href={tab.href}
87 class:list={["alc-switcher__item", { "alc-switcher__item--active": tab.label === current.label }]}
88 aria-current={tab.label === current.label ? "true" : undefined}
89 >
90 {TAB_ICONS[tab.label] && (
91 <svg
92 class="alc-switcher__icon"
93 aria-hidden="true"
94 width="15"
95 height="15"
96 viewBox="0 0 24 24"
97 set:html={TAB_ICONS[tab.label]}
98 />
99 )}
100 <span>{tab.label}</span>
101 </a>
102 ))}
103 </>
104 ))
105 }
106 </div>
107</details>
108
109<style>
110 /* Mobile-only: the desktop tab bar owns product navigation there. */
111 .alc-switcher {
112 margin: 0 0 0.75rem;
113 }
114 @media (min-width: 50rem) {
115 .alc-switcher {
116 display: none;
117 }
118 }
119 .alc-switcher__current {
120 display: flex;
121 align-items: center;
122 gap: 9px;
123 padding: 10px 13px;
124 border: 1px solid var(--alc-hairline-2);
125 border-radius: 10px;
126 font-size: var(--sl-text-base);
127 font-weight: 600;
128 color: var(--sl-color-white);
129 cursor: pointer;
130 user-select: none;
131 list-style: none;
132 }
133 .alc-switcher__current::marker,
134 .alc-switcher__current::-webkit-details-marker {
135 display: none;
136 }
137 .alc-switcher[open] .alc-switcher__current {
138 border-color: var(--alc-accent-deep);
139 }
140 .alc-switcher__icon {
141 flex-shrink: 0;
142 opacity: 0.9;
143 }
144 .alc-switcher__chevron {
145 margin-inline-start: auto;
146 color: var(--alc-fg-4);
147 transition: transform var(--alc-dur, 0.2s) var(--alc-ease, ease);
148 }
149 .alc-switcher[open] .alc-switcher__chevron {
150 transform: rotateZ(180deg);
151 }
152 .alc-switcher__panel {
153 margin-top: 6px;
154 border: 1px solid var(--alc-hairline-2);
155 border-radius: 12px;
156 background: var(--alc-bg-elev-1, var(--sl-color-bg-nav));
157 padding: 6px 0;
158 overflow: hidden;
159 }
160 .alc-switcher__heading {
161 padding: 8px 14px 3px;
162 font-size: 10.5px;
163 font-weight: 600;
164 letter-spacing: 0.07em;
165 text-transform: uppercase;
166 color: var(--alc-fg-4);
167 border-top: 1px solid var(--alc-hairline);
168 margin-top: 5px;
169 }
170 .alc-switcher__item {
171 display: flex;
172 align-items: center;
173 gap: 10px;
174 padding: 8px 14px;
175 font-size: var(--sl-text-sm);
176 color: var(--alc-fg-2);
177 text-decoration: none;
178 }
179 .alc-switcher__item:hover {
180 color: var(--alc-accent-deep);
181 }
182 .alc-switcher__item--active {
183 color: var(--alc-accent-deep);
184 font-weight: 600;
185 background: color-mix(in srgb, var(--alc-accent-deep) 10%, transparent);
186 }
187</style>