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 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,4 @@
1
+ declare module 'virtual:starlight-telescope-config' {
2
+ const config: import('./schemas/config.js').TelescopeConfig;
3
+ export default config;
4
+ }
@@ -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);