@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.
- package/README.md +7 -2
- package/bin/install-skills.js +251 -0
- package/package.json +12 -6
- package/scripts/build-python-docs.mjs +385 -0
- package/scripts/build-ts-docs.mjs +349 -0
- package/scripts/python-autodoc.json +10 -0
- package/scripts/ts-autodoc.json +10 -0
- package/skills/claude/CLAUDE.md +46 -0
- package/skills/claude/abstract-data-docs-author/SKILL.md +305 -0
- package/skills/claude/abstract-data-setup/SKILL.md +555 -0
- package/skills/cursor/abstract-data-docs-author.mdc +311 -0
- package/skills/cursor/abstract-data-setup.mdc +561 -0
- package/skills/cursor/welcome.mdc +29 -0
- package/skills/github/copilot-instructions.md +893 -0
- package/src/components/SocialIcons.astro +17 -2
- package/src/components/VersionPicker.astro +238 -0
- package/src/index.ts +17 -1
- package/src/styles/hud.css +222 -210
- package/src/styles/theme.css +444 -432
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* Override of Starlight's <SocialIcons /> slot.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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,
|