@abstractdata/starlight-theme 0.3.1 → 0.3.3

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.
@@ -1,17 +1,32 @@
1
1
  ---
2
2
  /**
3
3
  * Override of Starlight's <SocialIcons /> slot.
4
- * Renders the default content, then appends the Abstract Data version chip
5
- * if `version` is configured on the theme plugin.
4
+ *
5
+ * Renders, in order:
6
+ * 1. <VersionPicker /> — auto-discovers versions from frontmatter; renders
7
+ * nothing when fewer than 2 versions exist or the user isn't on an
8
+ * API page. Zero-config: as soon as the autodoc orchestrator emits
9
+ * versioned pages, the picker appears.
10
+ * 2. The default SocialIcons (GitHub, etc.).
11
+ * 3. The Abstract Data version chip if `version` is configured on the
12
+ * theme plugin.
6
13
  *
7
14
  * The chip carries the `ad-version-chip` class. When `motion: 'full'`,
8
15
  * `hud.css` adds an idle glitch-pulse + a hover-triggered glitch.
16
+ *
17
+ * Why this lives in the plugin (not a user override): Starlight gives
18
+ * plugin-level `components` overrides higher precedence than user-level
19
+ * ones, so a user override would lose to ours and the version chip would
20
+ * disappear. Composing here keeps both features without conflicts.
9
21
  */
10
22
  import type { Props } from '@astrojs/starlight/props';
11
23
  import Default from '@astrojs/starlight/components/SocialIcons.astro';
24
+ import VersionPicker from './VersionPicker.astro';
12
25
  import { config } from 'virtual:abstractdata/config';
13
26
  ---
14
27
 
28
+ <VersionPicker apiBase={config.apiBase ?? '/api'} />
29
+
15
30
  <Default {...Astro.props} />
16
31
 
17
32
  {
@@ -0,0 +1,238 @@
1
+ ---
2
+ /**
3
+ * <VersionPicker /> — dropdown for navigating between API doc versions.
4
+ *
5
+ * Two ways to use it:
6
+ *
7
+ * (a) Auto-discover (recommended). Pass no `versions` prop:
8
+ *
9
+ * ---
10
+ * import VersionPicker from '@abstractdata/starlight-theme/components/VersionPicker.astro';
11
+ * ---
12
+ * <VersionPicker apiBase="/api" />
13
+ *
14
+ * The component walks `getCollection('docs')` at build time, picks up
15
+ * every page that has a `version:` frontmatter field (set by the autodoc
16
+ * orchestrators on per-version pages), and dedupes by tag. The default
17
+ * version is detected via `versionDefault: true` on those same pages.
18
+ * No duplication: the autodoc JSON config is the canonical source.
19
+ *
20
+ * (b) Manual list. Pass an explicit `versions` array if you want to
21
+ * curate the dropdown — e.g. hide pre-release tags, reorder, override
22
+ * labels:
23
+ *
24
+ * ---
25
+ * const versions = [
26
+ * { tag: 'v0.4.0', label: '0.4 (latest)', default: true },
27
+ * { tag: 'v0.3.0', label: '0.3' },
28
+ * ];
29
+ * ---
30
+ * <VersionPicker {versions} apiBase="/api" />
31
+ *
32
+ * On change the component navigates to the equivalent page in the chosen
33
+ * version (same module, different version subdirectory). If the equivalent
34
+ * page doesn't exist (the symbol was added later or removed) it falls back
35
+ * to the version's API root.
36
+ *
37
+ * Schema requirement for auto-discovery: your `src/content.config.ts`
38
+ * must accept these optional fields (the create-docs template already
39
+ * does):
40
+ *
41
+ * docsSchema({
42
+ * extend: z.object({
43
+ * version: z.string().optional(),
44
+ * versionLabel: z.string().optional(),
45
+ * versionDefault: z.boolean().optional(),
46
+ * }),
47
+ * })
48
+ */
49
+ import { getCollection } from 'astro:content';
50
+
51
+ interface Version {
52
+ /** Git tag (used as the URL segment after the safeTag transform: `v0.3.0` → `0.3.0`). */
53
+ tag: string;
54
+ /** Human label shown in the dropdown (e.g. "0.3 (legacy)"). */
55
+ label?: string;
56
+ /** True for the version aliased to the un-versioned URL. */
57
+ default?: boolean;
58
+ }
59
+
60
+ interface Props {
61
+ /** Optional: override the auto-discovered list. Omit to use frontmatter-based discovery. */
62
+ versions?: Version[];
63
+ /** Base URL of the API reference (default `/api`). Match `outputDir` in autodoc config. */
64
+ apiBase?: string;
65
+ }
66
+
67
+ const { versions: propVersions, apiBase = '/api' } = Astro.props;
68
+ const base = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
69
+
70
+ // Match `safeTag()` in the autodoc scripts: strip leading `v`, then
71
+ // replace dots and any other non-alphanumeric chars with `-`. Dots get
72
+ // stripped by Astro's URL slug normalizer (`0.1.0` → `010`) so we use
73
+ // dashes from the start to keep filesystem and URL byte-identical.
74
+ function safeTag(tag: string): string {
75
+ return tag.replace(/^v/, '').replace(/[^a-zA-Z0-9_-]/g, '-');
76
+ }
77
+
78
+ // ─── Resolve the version list ─────────────────────────────────────────
79
+ //
80
+ // 1. Caller passed `versions` prop → use as-is.
81
+ // 2. Otherwise, walk the docs collection looking for `version:` frontmatter
82
+ // fields written by the autodoc orchestrators. Dedupe, sort, mark default.
83
+ // 3. If nothing turns up, leave `versions` empty and the component renders
84
+ // nothing (the `{currentTag &&` guard below short-circuits).
85
+ let versions: Version[] = propVersions ?? [];
86
+ if (!propVersions || propVersions.length === 0) {
87
+ try {
88
+ const all = await getCollection('docs');
89
+ const seen = new Map<string, Version>();
90
+ for (const entry of all) {
91
+ const data = entry.data as {
92
+ version?: string;
93
+ versionLabel?: string;
94
+ versionDefault?: boolean;
95
+ };
96
+ if (!data.version) continue;
97
+ const existing = seen.get(data.version);
98
+ // First occurrence wins for the tag/label; if any page in this
99
+ // version sets `versionDefault`, treat the version as default.
100
+ if (!existing) {
101
+ seen.set(data.version, {
102
+ tag: data.version,
103
+ label: data.versionLabel ?? data.version,
104
+ default: !!data.versionDefault,
105
+ });
106
+ } else if (data.versionDefault && !existing.default) {
107
+ existing.default = true;
108
+ }
109
+ }
110
+ versions = [...seen.values()];
111
+
112
+ // Sort: default first, then descending semver-ish. `localeCompare`
113
+ // with `numeric: true` handles `0.4.0` vs `0.10.0` correctly.
114
+ versions.sort((a, b) => {
115
+ if (a.default !== b.default) return a.default ? -1 : 1;
116
+ return b.tag.localeCompare(a.tag, 'en', { numeric: true });
117
+ });
118
+
119
+ // Fallback: if nothing was marked default, promote the first sorted
120
+ // entry. The autodoc orchestrators always emit `versionDefault: true`,
121
+ // so this only triggers if the user wired versions some other way.
122
+ if (versions.length > 0 && !versions.some((v) => v.default)) {
123
+ versions[0].default = true;
124
+ }
125
+ } catch {
126
+ // No `docs` collection or no version frontmatter — render nothing.
127
+ versions = [];
128
+ }
129
+ }
130
+
131
+ // ─── Detect current version from URL ──────────────────────────────────
132
+ // /api/0.3.0/auditkit_config/ → current = "0.3.0"
133
+ // /api/auditkit_config/ → current = default
134
+ const path = Astro.url.pathname.replace(base, '');
135
+ const apiPrefix = apiBase.replace(/\/$/, '') + '/';
136
+ let currentTag: string | null = null;
137
+ let currentPage: string | null = null;
138
+ if (path.startsWith(apiPrefix)) {
139
+ const rest = path.slice(apiPrefix.length).replace(/\/$/, '');
140
+ const [first, ...remainder] = rest.split('/');
141
+ const matched = versions.find((v) => safeTag(v.tag) === first);
142
+ if (matched) {
143
+ currentTag = matched.tag;
144
+ currentPage = remainder.join('/');
145
+ } else {
146
+ const def = versions.find((v) => v.default);
147
+ if (def) {
148
+ currentTag = def.tag;
149
+ currentPage = rest;
150
+ }
151
+ }
152
+ }
153
+ ---
154
+
155
+ {currentTag && versions.length > 1 && (
156
+ <starlight-version-picker class="ad-version-picker" data-api-base={apiBase} data-current-page={currentPage ?? ''} data-base={base}>
157
+ <label>
158
+ <span class="sr-only">API version</span>
159
+ <select aria-label="API version">
160
+ {versions.map((v) => (
161
+ <option value={safeTag(v.tag)} selected={v.tag === currentTag} data-default={v.default ? 'true' : 'false'}>
162
+ {v.label ?? v.tag}
163
+ </option>
164
+ ))}
165
+ </select>
166
+ </label>
167
+ </starlight-version-picker>
168
+ )}
169
+
170
+ <style>
171
+ .ad-version-picker {
172
+ display: inline-flex;
173
+ align-items: center;
174
+ margin-inline-end: 0.75rem;
175
+ }
176
+ .ad-version-picker select {
177
+ appearance: none;
178
+ -webkit-appearance: none;
179
+ background: transparent;
180
+ color: var(--sl-color-white, var(--ad-cream));
181
+ border: 1px solid var(--sl-color-gray-5, var(--ad-cyan-deep));
182
+ border-radius: 999px;
183
+ padding: 0.125rem 1.75rem 0.125rem 0.75rem;
184
+ font: 600 0.75rem/1 var(--sl-font-system, ui-sans-serif), system-ui;
185
+ letter-spacing: 0.04em;
186
+ text-transform: uppercase;
187
+ cursor: pointer;
188
+ background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
189
+ linear-gradient(135deg, currentColor 50%, transparent 50%);
190
+ background-position: calc(100% - 0.7rem) center, calc(100% - 0.4rem) center;
191
+ background-size: 5px 5px, 5px 5px;
192
+ background-repeat: no-repeat;
193
+ transition: border-color 0.18s ease, color 0.18s ease;
194
+ }
195
+ .ad-version-picker select:hover,
196
+ .ad-version-picker select:focus-visible {
197
+ border-color: var(--ad-cyan, #00d9ff);
198
+ color: var(--ad-cyan, #00d9ff);
199
+ outline: none;
200
+ }
201
+ .sr-only {
202
+ position: absolute;
203
+ width: 1px; height: 1px;
204
+ padding: 0; margin: -1px;
205
+ overflow: hidden; clip: rect(0, 0, 0, 0);
206
+ white-space: nowrap; border: 0;
207
+ }
208
+ </style>
209
+
210
+ <script>
211
+ // Custom element gives us a clean place to attach the change handler
212
+ // without inline scripts (CSP-friendly).
213
+ class StarlightVersionPicker extends HTMLElement {
214
+ connectedCallback() {
215
+ const select = this.querySelector('select');
216
+ if (!(select instanceof HTMLSelectElement)) return;
217
+ const apiBase = (this.dataset.apiBase || '/api').replace(/\/$/, '');
218
+ const currentPage = this.dataset.currentPage || '';
219
+ const base = this.dataset.base || '';
220
+ select.addEventListener('change', (e) => {
221
+ const value = (e.target as HTMLSelectElement).value;
222
+ const option = (e.target as HTMLSelectElement).selectedOptions[0];
223
+ const isDefault = option?.dataset.default === 'true';
224
+ // Default version's pages live at the un-versioned URL.
225
+ const versionSegment = isDefault ? '' : `/${value}`;
226
+ const target = `${base}${apiBase}${versionSegment}/${currentPage}`.replace(/\/+$/, '/');
227
+ // First try the equivalent page in the chosen version. If it 404s
228
+ // we want to fall back to that version's index — but we can't
229
+ // detect 404 from a navigation, so trust the build: if the page
230
+ // exists for one version it usually exists for adjacent ones.
231
+ window.location.href = target;
232
+ });
233
+ }
234
+ }
235
+ if (!customElements.get('starlight-version-picker')) {
236
+ customElements.define('starlight-version-picker', StarlightVersionPicker);
237
+ }
238
+ </script>
package/src/index.ts CHANGED
@@ -36,6 +36,21 @@ export interface AbstractDataThemeConfig {
36
36
  * `expressiveCode.themes` in `astro.config.mjs`.
37
37
  */
38
38
  shiki?: boolean;
39
+
40
+ /**
41
+ * Base URL of the API reference for the bundled `<VersionPicker>`.
42
+ * Match `outputDir` in your autodoc JSON config (relative to
43
+ * `src/content/docs/`). Defaults to `/api`.
44
+ *
45
+ * The picker auto-discovers the version list from the docs collection's
46
+ * `version:` frontmatter (written by the autodoc orchestrator), so you
47
+ * don't need to maintain a separate version array — point this at the
48
+ * correct base path and the rest is automatic.
49
+ *
50
+ * @example "/api" // python-autodoc.json: "outputDir": "src/content/docs/api"
51
+ * @example "/api/ts" // ts-autodoc.json: "outputDir": "src/content/docs/api/ts"
52
+ */
53
+ apiBase?: string;
39
54
  }
40
55
 
41
56
  const PLUGIN_NAME = '@abstractdata/starlight-theme';
@@ -62,8 +77,9 @@ export default function abstractDataTheme(
62
77
  const credit = opts.credit ?? 'auto';
63
78
  const version = opts.version ?? null;
64
79
  const shiki = opts.shiki ?? true;
80
+ const apiBase = opts.apiBase ?? '/api';
65
81
 
66
- const runtimeConfig = JSON.stringify({ motion, credit, version });
82
+ const runtimeConfig = JSON.stringify({ motion, credit, version, apiBase });
67
83
 
68
84
  return {
69
85
  name: PLUGIN_NAME,