starlight-telescope 0.0.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 +15 -0
- package/index.ts +28 -0
- package/package.json +54 -0
- package/src/env.d.ts +4 -0
- package/src/libs/integration.ts +82 -0
- package/src/libs/modal.ts +43 -0
- package/src/libs/telescope-element.ts +73 -0
- package/src/libs/telescope-search.ts +1103 -0
- package/src/libs/url.ts +125 -0
- package/src/libs/vite.ts +19 -0
- package/src/pages/pages.json.ts +72 -0
- package/src/schemas/config.ts +103 -0
- package/src/styles/telescope.css +662 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# `starlight-telescope`
|
|
2
|
+
|
|
3
|
+
Quickly navigate to any page in your Starlight docs with fuzzy search and keyboard-first navigation.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
Want to get started immediately?
|
|
8
|
+
|
|
9
|
+
Check out the `starlight-telescope` getting started guide.
|
|
10
|
+
|
|
11
|
+
## License
|
|
12
|
+
|
|
13
|
+
Licensed under the MIT License, Copyright © frostybee.
|
|
14
|
+
|
|
15
|
+
See [LICENSE](https://github.com/frostybee/starlight-telescope/blob/main/LICENSE) for more information.
|
package/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StarlightPlugin } from '@astrojs/starlight/types';
|
|
2
|
+
import { TelescopeConfigSchema, type TelescopeUserConfig } from './src/schemas/config.js';
|
|
3
|
+
import starlightTelescopeIntegration from './src/libs/integration.js';
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
TelescopeConfig,
|
|
7
|
+
TelescopeUserConfig,
|
|
8
|
+
TelescopePage,
|
|
9
|
+
TelescopeShortcut,
|
|
10
|
+
TelescopeFuseOptions,
|
|
11
|
+
TelescopeTheme,
|
|
12
|
+
} from './src/schemas/config.js';
|
|
13
|
+
|
|
14
|
+
export default function starlightTelescope(
|
|
15
|
+
userConfig: TelescopeUserConfig = {}
|
|
16
|
+
): StarlightPlugin {
|
|
17
|
+
const config = TelescopeConfigSchema.parse(userConfig);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
name: 'starlight-telescope',
|
|
21
|
+
hooks: {
|
|
22
|
+
'config:setup'({ addIntegration, logger }) {
|
|
23
|
+
logger.info('Initializing Starlight Telescope search...');
|
|
24
|
+
addIntegration(starlightTelescopeIntegration(config));
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "starlight-telescope",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Quickly navigate to any page in your Starlight docs with fuzzy search and keyboard-first navigation.",
|
|
6
|
+
"author": "frostybee",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.ts",
|
|
10
|
+
"./libs/telescope-search": "./src/libs/telescope-search.ts",
|
|
11
|
+
"./libs/telescope-element": "./src/libs/telescope-element.ts",
|
|
12
|
+
"./libs/modal": "./src/libs/modal.ts",
|
|
13
|
+
"./styles/telescope.css": "./src/styles/telescope.css",
|
|
14
|
+
"./schemas/config": "./src/schemas/config.ts"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"index.ts",
|
|
18
|
+
"src/"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"fuse.js": "^7.1.0",
|
|
22
|
+
"zod": "^4.3.6"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@astrojs/starlight": "^0.37.4",
|
|
26
|
+
"astro": "^5.16.15"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@astrojs/starlight": ">=0.37"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": "^18.17.1 || ^20.3.0 || >=21.0.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/frostybee/starlight-telescope",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/frostybee/starlight-telescope.git",
|
|
41
|
+
"directory": "packages/starlight-telescope"
|
|
42
|
+
},
|
|
43
|
+
"bugs": "https://github.com/frostybee/starlight-telescope/issues",
|
|
44
|
+
"keywords": [
|
|
45
|
+
"starlight",
|
|
46
|
+
"starlight-plugin",
|
|
47
|
+
"astro",
|
|
48
|
+
"search",
|
|
49
|
+
"fuzzy-search",
|
|
50
|
+
"navigation",
|
|
51
|
+
"withastro",
|
|
52
|
+
"command-palette"
|
|
53
|
+
]
|
|
54
|
+
}
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
import type { TelescopeConfig } from '../schemas/config.js';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { vitePluginTelescopeConfig } from './vite.js';
|
|
5
|
+
|
|
6
|
+
export default function starlightTelescopeIntegration(
|
|
7
|
+
config: TelescopeConfig
|
|
8
|
+
): AstroIntegration {
|
|
9
|
+
return {
|
|
10
|
+
name: 'starlight-telescope-integration',
|
|
11
|
+
hooks: {
|
|
12
|
+
'astro:config:setup': ({ injectRoute, injectScript, updateConfig }) => {
|
|
13
|
+
// 1. Register Vite plugin for virtual module
|
|
14
|
+
updateConfig({
|
|
15
|
+
vite: { plugins: [vitePluginTelescopeConfig(config)] },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// 2. Inject locale-aware /pages.json route
|
|
19
|
+
// Using [...locale] rest parameter handles both localized (/en/pages.json)
|
|
20
|
+
// and non-localized (/pages.json) paths in a single route
|
|
21
|
+
injectRoute({
|
|
22
|
+
pattern: '/[...locale]/pages.json',
|
|
23
|
+
entrypoint: fileURLToPath(new URL('../pages/pages.json.ts', import.meta.url)),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 3. Inject CSS
|
|
27
|
+
injectScript('page', `import 'starlight-telescope/styles/telescope.css';`);
|
|
28
|
+
|
|
29
|
+
// 4. Inject client-side script with custom element
|
|
30
|
+
// Use base64 encoding for safe config transport (avoids XSS via string escaping)
|
|
31
|
+
const configBase64 = Buffer.from(JSON.stringify(config)).toString('base64');
|
|
32
|
+
|
|
33
|
+
injectScript(
|
|
34
|
+
'page',
|
|
35
|
+
`
|
|
36
|
+
import 'starlight-telescope/libs/telescope-element';
|
|
37
|
+
|
|
38
|
+
let initInProgress = false;
|
|
39
|
+
|
|
40
|
+
function initTelescope() {
|
|
41
|
+
// Prevent race conditions with initialization lock
|
|
42
|
+
if (window.__telescopeInitialized || initInProgress) return;
|
|
43
|
+
initInProgress = true;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Inject custom element into header
|
|
47
|
+
if (!document.querySelector('telescope-search')) {
|
|
48
|
+
const rightGroup = document.querySelector('.right-group');
|
|
49
|
+
if (rightGroup) {
|
|
50
|
+
const el = document.createElement('telescope-search');
|
|
51
|
+
// Decode base64 config safely
|
|
52
|
+
const configJson = atob('${configBase64}');
|
|
53
|
+
el.setAttribute('data-config', configJson);
|
|
54
|
+
rightGroup.insertAdjacentElement('afterbegin', el);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
window.__telescopeInitialized = true;
|
|
58
|
+
} finally {
|
|
59
|
+
initInProgress = false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (document.readyState === 'loading') {
|
|
64
|
+
document.addEventListener('DOMContentLoaded', initTelescope);
|
|
65
|
+
} else {
|
|
66
|
+
initTelescope();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
document.addEventListener('astro:page-load', () => {
|
|
70
|
+
window.__telescopeInitialized = false;
|
|
71
|
+
initTelescope();
|
|
72
|
+
});
|
|
73
|
+
`
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
'astro:build:done': ({ logger }) => {
|
|
78
|
+
logger.info('Starlight Telescope plugin installed successfully!');
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function getModalHTML(): string {
|
|
2
|
+
return `
|
|
3
|
+
<dialog id="telescope-dialog" class="telescope" aria-label="Site search">
|
|
4
|
+
<div class="telescope__modal">
|
|
5
|
+
<button class="telescope__close-button" id="telescope-close-button" aria-label="Close">
|
|
6
|
+
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
|
|
7
|
+
</button>
|
|
8
|
+
<div class="telescope__tabs" role="tablist">
|
|
9
|
+
<button class="telescope__tab telescope__tab--active" data-tab="search" role="tab" aria-selected="true" id="tab-search">Search</button>
|
|
10
|
+
<button class="telescope__tab" data-tab="recent" role="tab" aria-selected="false" id="tab-recent">Recent</button>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="telescope__search-header">
|
|
13
|
+
<input id="telescope-search-input" class="telescope__search-input" type="text"
|
|
14
|
+
role="combobox"
|
|
15
|
+
aria-expanded="true"
|
|
16
|
+
aria-autocomplete="list"
|
|
17
|
+
aria-haspopup="listbox"
|
|
18
|
+
aria-controls="telescope-results"
|
|
19
|
+
aria-activedescendant=""
|
|
20
|
+
placeholder="Search pages..." autocomplete="off" spellcheck="false">
|
|
21
|
+
</div>
|
|
22
|
+
<div id="telescope-search-section" class="telescope__section telescope__section--active" role="tabpanel" aria-labelledby="tab-search">
|
|
23
|
+
<div id="telescope-loading" class="telescope__loading">
|
|
24
|
+
<div class="telescope__spinner"></div>
|
|
25
|
+
<span>Loading pages...</span>
|
|
26
|
+
</div>
|
|
27
|
+
<ul id="telescope-results" class="telescope__results" role="listbox" aria-label="Search results"></ul>
|
|
28
|
+
</div>
|
|
29
|
+
<div id="telescope-recent-section" class="telescope__section" role="tabpanel" aria-labelledby="tab-recent">
|
|
30
|
+
<ul id="telescope-recent-results" class="telescope__results" role="listbox" aria-label="Recent pages"></ul>
|
|
31
|
+
</div>
|
|
32
|
+
<div id="telescope-live-region" class="sr-only" aria-live="polite" aria-atomic="true"></div>
|
|
33
|
+
<div class="telescope__footer">
|
|
34
|
+
<div class="telescope__shortcuts">
|
|
35
|
+
<span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
|
|
36
|
+
<span><kbd>↵</kbd> select</span>
|
|
37
|
+
<span><kbd>Space</kbd> pin</span>
|
|
38
|
+
<span><kbd>Esc</kbd> close</span>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</dialog>`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import TelescopeSearch from './telescope-search.js';
|
|
2
|
+
import { getModalHTML } from './modal.js';
|
|
3
|
+
import type { TelescopeConfig } from '../schemas/config.js';
|
|
4
|
+
|
|
5
|
+
export class TelescopeSearchElement extends HTMLElement {
|
|
6
|
+
private telescopeSearch: TelescopeSearch | null = null;
|
|
7
|
+
|
|
8
|
+
connectedCallback() {
|
|
9
|
+
const configStr = this.dataset.config;
|
|
10
|
+
if (!configStr) {
|
|
11
|
+
console.error('[Telescope] Missing data-config attribute');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let config: TelescopeConfig;
|
|
16
|
+
try {
|
|
17
|
+
config = JSON.parse(configStr);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.error('[Telescope] Invalid config JSON:', e);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Render trigger button
|
|
24
|
+
this.innerHTML = this.getTriggerButtonHTML();
|
|
25
|
+
|
|
26
|
+
// Platform detection for Mac (userAgentData with fallback to userAgent)
|
|
27
|
+
const shortcutKey = this.querySelector('.telescope__shortcut-key');
|
|
28
|
+
const isMac = (navigator as Navigator & { userAgentData?: { platform: string } }).userAgentData?.platform === 'macOS'
|
|
29
|
+
|| /Mac|iPhone|iPod|iPad/i.test(navigator.userAgent);
|
|
30
|
+
if (shortcutKey && isMac) {
|
|
31
|
+
shortcutKey.textContent = '⌘';
|
|
32
|
+
this.querySelector('button')?.setAttribute('aria-keyshortcuts', 'Meta+/');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Inject dialog if not present
|
|
36
|
+
if (!document.getElementById('telescope-dialog')) {
|
|
37
|
+
document.body.insertAdjacentHTML('beforeend', getModalHTML());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Initialize search controller
|
|
41
|
+
this.telescopeSearch = new TelescopeSearch(config);
|
|
42
|
+
|
|
43
|
+
// Button click opens modal
|
|
44
|
+
this.querySelector('button')?.addEventListener('click', () => {
|
|
45
|
+
this.telescopeSearch?.open();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
disconnectedCallback() {
|
|
50
|
+
// Cleanup to prevent memory leaks
|
|
51
|
+
this.telescopeSearch?.destroy();
|
|
52
|
+
this.telescopeSearch = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private getTriggerButtonHTML(): string {
|
|
56
|
+
return `<button class="telescope__trigger-btn" aria-label="Open Telescope Search" aria-keyshortcuts="Control+/">
|
|
57
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
58
|
+
<path d="m10.065 12.493-6.18 1.318a.934.934 0 0 1-1.108-.702l-.537-2.15a1.07 1.07 0 0 1 .691-1.265l13.504-4.44"/>
|
|
59
|
+
<path d="m13.56 11.747 4.332-.924"/>
|
|
60
|
+
<path d="m16 21-3.105-6.21"/>
|
|
61
|
+
<path d="M16.485 5.94a2 2 0 0 1 1.455-2.425l1.09-.272a1 1 0 0 1 1.212.727l1.515 6.06a1 1 0 0 1-.727 1.213l-1.09.272a2 2 0 0 1-2.425-1.455z"/>
|
|
62
|
+
<path d="m6.158 8.633 1.114 4.456"/>
|
|
63
|
+
<path d="m8 21 3.105-6.21"/>
|
|
64
|
+
</svg>
|
|
65
|
+
<kbd class="telescope__shortcut sl-hidden md:sl-flex">
|
|
66
|
+
<kbd class="telescope__shortcut-key">Ctrl</kbd>
|
|
67
|
+
<kbd>/</kbd>
|
|
68
|
+
</kbd>
|
|
69
|
+
</button>`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
customElements.define('telescope-search', TelescopeSearchElement);
|