@hagicode/hagilight-starlight 0.1.0-dev.148.1.e0c630e

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.
Files changed (81) hide show
  1. package/AIDisclosureNotice.astro +12 -0
  2. package/ArticlePromotion.astro +630 -0
  3. package/ArticlePromotionPicker.astro +9 -0
  4. package/ContentLayoutToggle.astro +31 -0
  5. package/Footer.astro +201 -0
  6. package/Header.astro +131 -0
  7. package/LanguageChooser.astro +386 -0
  8. package/MarkdownContent.astro +45 -0
  9. package/NotFoundHero.astro +44 -0
  10. package/PageTitle.astro +7 -0
  11. package/PromotoFooter.astro +7 -0
  12. package/README.md +178 -0
  13. package/SEOHead.astro +67 -0
  14. package/ThemeSelect.astro +176 -0
  15. package/assets/showcase/hero-avatar.webp +0 -0
  16. package/assets/showcase/heroes.webp +0 -0
  17. package/assets/showcase/manifest.json +333 -0
  18. package/assets/showcase/pillar-efficient.svg +1 -0
  19. package/assets/showcase/pillar-fun.svg +1 -0
  20. package/assets/showcase/pillar-smart.svg +1 -0
  21. package/assets/showcase/proposal-workflow.webp +0 -0
  22. package/assets/showcase/workbench.webp +0 -0
  23. package/content-width.css +81 -0
  24. package/dist/ai-disclosures.d.ts +31 -0
  25. package/dist/ai-disclosures.js +105 -0
  26. package/dist/article-promotion-copy/de-DE.d.ts +34 -0
  27. package/dist/article-promotion-copy/de-DE.js +43 -0
  28. package/dist/article-promotion-copy/en-US.d.ts +34 -0
  29. package/dist/article-promotion-copy/en-US.js +43 -0
  30. package/dist/article-promotion-copy/es-ES.d.ts +34 -0
  31. package/dist/article-promotion-copy/es-ES.js +43 -0
  32. package/dist/article-promotion-copy/fr-FR.d.ts +34 -0
  33. package/dist/article-promotion-copy/fr-FR.js +43 -0
  34. package/dist/article-promotion-copy/index.d.ts +32 -0
  35. package/dist/article-promotion-copy/index.js +50 -0
  36. package/dist/article-promotion-copy/ja-JP.d.ts +34 -0
  37. package/dist/article-promotion-copy/ja-JP.js +43 -0
  38. package/dist/article-promotion-copy/ko-KR.d.ts +34 -0
  39. package/dist/article-promotion-copy/ko-KR.js +43 -0
  40. package/dist/article-promotion-copy/pt-BR.d.ts +34 -0
  41. package/dist/article-promotion-copy/pt-BR.js +43 -0
  42. package/dist/article-promotion-copy/ru-RU.d.ts +34 -0
  43. package/dist/article-promotion-copy/ru-RU.js +43 -0
  44. package/dist/article-promotion-copy/zh-CN.d.ts +34 -0
  45. package/dist/article-promotion-copy/zh-CN.js +43 -0
  46. package/dist/article-promotion-copy/zh-Hant.d.ts +34 -0
  47. package/dist/article-promotion-copy/zh-Hant.js +43 -0
  48. package/dist/article-promotion.d.ts +40 -0
  49. package/dist/article-promotion.js +14 -0
  50. package/dist/content-width-i18n.d.ts +6 -0
  51. package/dist/content-width-i18n.js +15 -0
  52. package/dist/content-width.d.ts +5 -0
  53. package/dist/content-width.js +42 -0
  54. package/dist/index.d.ts +105 -0
  55. package/dist/index.js +583 -0
  56. package/dist/language-routing.d.ts +18 -0
  57. package/dist/language-routing.js +95 -0
  58. package/dist/locales.d.ts +48 -0
  59. package/dist/locales.js +13 -0
  60. package/dist/not-found.d.ts +9 -0
  61. package/dist/not-found.js +28 -0
  62. package/dist/rss-renderer.d.ts +14 -0
  63. package/dist/rss-renderer.js +30 -0
  64. package/dist/rss-utils.d.ts +27 -0
  65. package/dist/rss-utils.js +65 -0
  66. package/dist/schema.d.ts +32 -0
  67. package/dist/schema.js +25 -0
  68. package/dist/seo-utils.d.ts +74 -0
  69. package/dist/seo-utils.js +219 -0
  70. package/dist/showcase.d.ts +6 -0
  71. package/dist/showcase.js +5 -0
  72. package/dist/site-metadata.d.ts +38 -0
  73. package/dist/site-metadata.js +109 -0
  74. package/dist/themes.d.ts +43 -0
  75. package/dist/themes.js +156 -0
  76. package/dist/windows-download.d.ts +4 -0
  77. package/dist/windows-download.js +22 -0
  78. package/package.json +88 -0
  79. package/rss.[language].xml.ts +20 -0
  80. package/rss.xml.ts +15 -0
  81. package/themes.css +160 -0
package/Footer.astro ADDED
@@ -0,0 +1,201 @@
1
+ ---
2
+ import Copyright from '@hagicode/hagilight-core/Copyright';
3
+ import FooterBrand from '@hagicode/hagilight-core/FooterBrand';
4
+ import DefaultFooter from '@astrojs/starlight/components/Footer.astro';
5
+ import { siteLinkGaAttributes } from '@hagicode/hagilight-core/analytics-events';
6
+ import { resolveSiteLinks, type SiteLinksOptions } from '@hagicode/hagilight-core/links';
7
+ import { buildInfoItems, type BuildInfo } from '@hagicode/hagilight-core/build-info';
8
+ import manifest from './package.json';
9
+
10
+ interface Props {
11
+ locale?: string;
12
+ links?: SiteLinksOptions;
13
+ rssFeedUrl?: string;
14
+ rssLocaleFeedUrls?: Readonly<Record<string, string>>;
15
+ buildInfo?: BuildInfo;
16
+ avatar?: boolean;
17
+ randomAvatar?: boolean;
18
+ }
19
+
20
+ const {
21
+ locale = Astro.locals?.starlightRoute?.locale,
22
+ links,
23
+ rssFeedUrl,
24
+ rssLocaleFeedUrls,
25
+ buildInfo,
26
+ avatar,
27
+ randomAvatar,
28
+ } = Astro.props;
29
+ const buildItems = buildInfoItems(buildInfo);
30
+ if (typeof manifest.version !== 'string' || manifest.version.trim().length === 0) {
31
+ throw new Error('@hagicode/hagilight-starlight requires a nonempty string version in its package.json.');
32
+ }
33
+ const currentLanguage = Astro.locals?.starlightRoute?.lang;
34
+ const sitemapOverride = links?.overrides?.sitemap;
35
+ const resolvedLinks = resolveSiteLinks(currentLanguage ?? locale, {
36
+ ...links,
37
+ overrides: {
38
+ ...links?.overrides,
39
+ sitemap: {
40
+ ...sitemapOverride,
41
+ href: sitemapOverride?.href ?? `${import.meta.env.BASE_URL}sitemap-index.xml`,
42
+ },
43
+ },
44
+ ...(rssFeedUrl === undefined ? {} : { rssFeedUrl }),
45
+ ...(currentLanguage && rssLocaleFeedUrls?.[currentLanguage]
46
+ ? { rssLocaleFeedUrl: rssLocaleFeedUrls[currentLanguage] }
47
+ : {}),
48
+ });
49
+ ---
50
+
51
+ <DefaultFooter />
52
+ <div class="hagilight-brand-host">
53
+ <FooterBrand locale={resolvedLinks.locale} links={resolvedLinks.quick} avatar={avatar} randomAvatar={randomAvatar} />
54
+ </div>
55
+ <div class="hagilight-site-links">
56
+ {resolvedLinks.relatedSites.length > 0 && (
57
+ <section>
58
+ <h2>{resolvedLinks.labels.relatedSites}</h2>
59
+ <nav aria-label={resolvedLinks.labels.navigation.relatedSites}>
60
+ <ul>
61
+ {resolvedLinks.relatedSites.map((site) => (
62
+ <li>
63
+ <a href={site.href} target={site.target} rel={site.rel}>
64
+ {site.name}
65
+ </a>
66
+ </li>
67
+ ))}
68
+ </ul>
69
+ </nav>
70
+ </section>
71
+ )}
72
+ <section>
73
+ <h2>{resolvedLinks.labels.quickLinks}</h2>
74
+ <nav aria-label={resolvedLinks.labels.navigation.quickLinks}>
75
+ <ul>
76
+ {resolvedLinks.quick.map((link) => (
77
+ <li>
78
+ <a
79
+ href={link.href}
80
+ target={link.target}
81
+ rel={link.rel}
82
+ {...siteLinkGaAttributes(link, 'footer')}
83
+ >{link.label}</a>
84
+ </li>
85
+ ))}
86
+ </ul>
87
+ </nav>
88
+ </section>
89
+ <section>
90
+ <h2>{resolvedLinks.labels.community}</h2>
91
+ <nav aria-label={resolvedLinks.labels.navigation.community}>
92
+ <ul>
93
+ {resolvedLinks.community.map((link) => (
94
+ <li>
95
+ <a
96
+ href={link.href}
97
+ target={link.target}
98
+ rel={link.rel}
99
+ {...siteLinkGaAttributes(link, 'footer')}
100
+ >{link.label}</a>
101
+ </li>
102
+ ))}
103
+ </ul>
104
+ </nav>
105
+ </section>
106
+ <nav class="hagilight-filings" aria-label={resolvedLinks.labels.navigation.filings}>
107
+ {resolvedLinks.filings.map((link) => (
108
+ <a
109
+ href={link.href}
110
+ target={link.target}
111
+ rel={link.rel}
112
+ aria-label={link.ariaLabel}
113
+ >{link.label}</a>
114
+ ))}
115
+ </nav>
116
+ </div>
117
+ <p class="hagilight-copyright">
118
+ <Copyright name="HagiCode" />
119
+ {' | '}
120
+ <span class="hagilight-copyright__attribution">Powered By hagilight-starlight@{manifest.version}</span>
121
+ {buildItems.map((item) => (
122
+ <>
123
+ {' | '}
124
+ <span
125
+ class="hagilight-copyright__build"
126
+ data-build-field={item.field}
127
+ title={item.title}
128
+ data-build-commit={item.title}
129
+ >{item.label}: {item.datetime ? <time datetime={item.datetime}>{item.text}</time> : item.text}</span>
130
+ </>
131
+ ))}
132
+ </p>
133
+
134
+ <style>
135
+ .hagilight-brand-host {
136
+ margin-top: 2rem;
137
+ padding-inline: 1rem;
138
+ }
139
+
140
+ .hagilight-site-links {
141
+ display: grid;
142
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
143
+ gap: 1.5rem;
144
+ margin: 0 auto 1rem;
145
+ max-width: 72rem;
146
+ padding-inline: 1rem;
147
+ }
148
+
149
+ .hagilight-site-links section {
150
+ min-width: 0;
151
+ }
152
+
153
+ .hagilight-site-links h2 {
154
+ margin: 0 0 0.75rem;
155
+ color: var(--sl-color-white);
156
+ font-size: var(--sl-text-sm);
157
+ }
158
+
159
+ .hagilight-site-links ul {
160
+ display: grid;
161
+ gap: 0.5rem;
162
+ margin: 0;
163
+ padding: 0;
164
+ list-style: none;
165
+ }
166
+
167
+ .hagilight-site-links a {
168
+ color: var(--sl-color-gray-3);
169
+ font-size: var(--sl-text-sm);
170
+ text-decoration: none;
171
+ }
172
+
173
+ .hagilight-site-links a:hover {
174
+ color: var(--sl-color-white);
175
+ }
176
+
177
+ .hagilight-filings {
178
+ grid-column: 1 / -1;
179
+ display: flex;
180
+ flex-wrap: wrap;
181
+ justify-content: center;
182
+ gap: 0.75rem 1.5rem;
183
+ border-top: 1px solid var(--sl-color-gray-5);
184
+ padding-top: 1rem;
185
+ }
186
+
187
+ .hagilight-copyright {
188
+ margin-block: 1rem;
189
+ color: var(--sl-color-gray-3);
190
+ font-size: var(--sl-text-xs);
191
+ text-align: center;
192
+ }
193
+
194
+ .hagilight-copyright__attribution {
195
+ overflow-wrap: anywhere;
196
+ }
197
+
198
+ .hagilight-copyright__build {
199
+ overflow-wrap: anywhere;
200
+ }
201
+ </style>
package/Header.astro ADDED
@@ -0,0 +1,131 @@
1
+ ---
2
+ import config from 'virtual:starlight/user-config';
3
+ import Search from 'virtual:starlight/components/Search';
4
+ import SiteTitle from 'virtual:starlight/components/SiteTitle';
5
+ import SocialIcons from 'virtual:starlight/components/SocialIcons';
6
+ import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
7
+ import { siteLinkGaAttributes } from '@hagicode/hagilight-core/analytics-events';
8
+ import { resolveSiteLinks, type SiteLinksOptions } from '@hagicode/hagilight-core/links';
9
+ import LanguageChooser from './LanguageChooser.astro';
10
+
11
+ interface Props {
12
+ links?: SiteLinksOptions;
13
+ }
14
+
15
+ const { links } = Astro.props;
16
+ const route = Astro.locals.starlightRoute;
17
+ const locale = route.lang ?? route.locale;
18
+ const resolvedLinks = resolveSiteLinks(locale, links);
19
+ const shouldRenderSearch =
20
+ config.pagefind || config.components.Search !== '@astrojs/starlight/components/Search.astro';
21
+ ---
22
+
23
+ <div class="header">
24
+ <div class="title-wrapper sl-flex">
25
+ <SiteTitle />
26
+ </div>
27
+ <div class="sl-flex print:hidden">
28
+ {shouldRenderSearch && <Search />}
29
+ </div>
30
+ <div class="sl-hidden md:sl-flex print:hidden right-group">
31
+ <nav class="header-links" aria-label="Site navigation">
32
+ {resolvedLinks.header.map((link) => (
33
+ <a href={link.href} target={link.target} rel={link.rel} {...siteLinkGaAttributes(link, 'header')}>{link.label}</a>
34
+ ))}
35
+ </nav>
36
+ <div class="sl-flex social-icons">
37
+ <SocialIcons />
38
+ </div>
39
+ <ThemeSelect />
40
+ <LanguageChooser />
41
+ </div>
42
+ </div>
43
+
44
+ <style>
45
+ @layer starlight.core {
46
+ .header {
47
+ display: flex;
48
+ gap: var(--sl-nav-gap);
49
+ justify-content: space-between;
50
+ align-items: center;
51
+ height: 100%;
52
+ }
53
+
54
+ .title-wrapper {
55
+ overflow: clip;
56
+ padding: 0.25rem;
57
+ margin: -0.25rem;
58
+ min-width: 0;
59
+ }
60
+
61
+ .right-group,
62
+ .social-icons {
63
+ gap: 1rem;
64
+ align-items: center;
65
+ }
66
+
67
+ .right-group {
68
+ flex-wrap: nowrap;
69
+ }
70
+
71
+ .social-icons:has(a)::after {
72
+ content: '';
73
+ height: 2rem;
74
+ border-inline-end: 1px solid var(--sl-color-gray-5);
75
+ }
76
+
77
+ .header-links {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 0.5rem;
81
+ white-space: nowrap;
82
+ }
83
+
84
+ .header-links a {
85
+ padding: 0.375rem 0.5rem;
86
+ border-radius: var(--sl-radius-sm);
87
+ color: var(--sl-color-text-high);
88
+ font-size: var(--sl-text-sm);
89
+ text-decoration: none;
90
+ }
91
+
92
+ .header-links a:hover {
93
+ background: var(--sl-color-gray-6);
94
+ color: var(--sl-color-white);
95
+ }
96
+
97
+ @media (max-width: 72rem) {
98
+ .header-links {
99
+ display: none;
100
+ }
101
+ }
102
+
103
+ @media (min-width: 50rem) {
104
+ :global(:root[data-has-sidebar]) {
105
+ --__sidebar-pad: calc(2 * var(--sl-nav-pad-x));
106
+ }
107
+ :global(:root:not([data-has-toc])) {
108
+ --__toc-width: 0rem;
109
+ }
110
+ .header {
111
+ --__sidebar-width: max(0rem, var(--sl-content-inline-start, 0rem) - var(--sl-nav-pad-x));
112
+ --__main-column-fr: calc(
113
+ (
114
+ 100% + var(--__sidebar-pad, 0rem) - var(--__toc-width, var(--sl-sidebar-width)) -
115
+ (2 * var(--__toc-width, var(--sl-nav-pad-x))) - var(--sl-content-inline-start, 0rem) -
116
+ var(--sl-content-width)
117
+ ) / 2
118
+ );
119
+ display: grid;
120
+ grid-template-columns:
121
+ minmax(
122
+ calc(var(--__sidebar-width) + max(0rem, var(--__main-column-fr) - var(--sl-nav-gap))),
123
+ auto
124
+ )
125
+ 1fr
126
+ auto;
127
+ align-content: center;
128
+ }
129
+ }
130
+ }
131
+ </style>
@@ -0,0 +1,386 @@
1
+ ---
2
+ import config from 'virtual:starlight/user-config';
3
+ import context from 'virtual:starlight/project-context';
4
+ import {
5
+ buildLocaleNavigationTarget,
6
+ getConfiguredLanguageOptions,
7
+ } from './dist/language-routing.js';
8
+
9
+ const currentLocale = Astro.locals.starlightRoute.locale ?? config.defaultLocale?.locale ?? 'root';
10
+ const options = getConfiguredLanguageOptions(config.locales, currentLocale);
11
+ const currentOption = options.find((option) => option.selected) ?? options[0];
12
+ const accessibleLabel = Astro.locals.t('languageSelect.accessibleLabel');
13
+ const copy = {
14
+ 'zh-cn': ['当前语言', '已选', '关闭语言选择'],
15
+ 'en-us': ['Current language', 'Selected', 'Close language selector'],
16
+ 'zh-hant': ['目前語言', '已選取', '關閉語言選擇'],
17
+ 'fr-fr': ['Langue actuelle', 'Sélectionné', 'Fermer le sélecteur de langue'],
18
+ 'de-de': ['Aktuelle Sprache', 'Ausgewählt', 'Sprachauswahl schließen'],
19
+ 'es-es': ['Idioma actual', 'Seleccionado', 'Cerrar selector de idioma'],
20
+ 'ja-jp': ['現在の言語', '選択中', '言語選択を閉じる'],
21
+ 'ko-kr': ['현재 언어', '선택됨', '언어 선택 닫기'],
22
+ 'pt-br': ['Idioma atual', 'Selecionado', 'Fechar seletor de idioma'],
23
+ 'ru-ru': ['Текущий язык', 'Выбрано', 'Закрыть выбор языка'],
24
+ };
25
+ const langKey = (currentOption?.lang ?? currentLocale).toLowerCase();
26
+ const languagePrefix = langKey.split('-')[0];
27
+ const fallbackCopy = copy[langKey]
28
+ ?? Object.entries(copy).find(([key]) => key.startsWith(`${languagePrefix}-`))?.[1]
29
+ ?? copy['en-us'];
30
+ const currentLabel = fallbackCopy[0];
31
+ const selectedLabel = fallbackCopy[1];
32
+ const closeLabel = fallbackCopy[2];
33
+ const basePath = import.meta.env.BASE_URL;
34
+ const routeLocales = Object.keys(config.locales ?? {});
35
+ const navigationOptions = options.map((option) => ({
36
+ ...option,
37
+ href: buildLocaleNavigationTarget(
38
+ Astro.url,
39
+ option.code,
40
+ routeLocales,
41
+ basePath,
42
+ context.trailingSlash,
43
+ ).pathname,
44
+ }));
45
+ ---
46
+
47
+ {
48
+ config.isMultilingual && navigationOptions.length > 1 && (
49
+ <hagilight-language-chooser class="language-switcher">
50
+ <button
51
+ type="button"
52
+ class="language-trigger"
53
+ aria-haspopup="dialog"
54
+ aria-controls="hagilight-language-dialog"
55
+ aria-expanded="false"
56
+ aria-label={`${accessibleLabel}: ${currentOption?.label ?? currentLocale}`}
57
+ title={currentOption?.label ?? currentLocale}
58
+ >
59
+ <svg
60
+ class="trigger-icon"
61
+ aria-hidden="true"
62
+ focusable="false"
63
+ viewBox="0 0 24 24"
64
+ width="16"
65
+ height="16"
66
+ fill="none"
67
+ stroke="currentColor"
68
+ stroke-width="1.8"
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"
71
+ >
72
+ <circle cx="12" cy="12" r="9"></circle>
73
+ <path d="M3 12h18"></path>
74
+ <path d="M12 3c2.6 2.7 3.9 5.7 3.9 9s-1.3 6.3-3.9 9c-2.6-2.7-3.9-5.7-3.9-9S9.4 5.7 12 3z"></path>
75
+ </svg>
76
+ <span class="trigger-label">{currentOption?.label ?? currentLocale}</span>
77
+ <span class="trigger-chevron" aria-hidden="true">▾</span>
78
+ </button>
79
+ <dialog
80
+ id="hagilight-language-dialog"
81
+ class="language-dialog"
82
+ aria-modal="true"
83
+ aria-labelledby="language-dialog-title"
84
+ >
85
+ <div class="dialog-header">
86
+ <div>
87
+ <h2 id="language-dialog-title" class="dialog-title">{accessibleLabel}</h2>
88
+ <p class="dialog-current">
89
+ {currentLabel}: <strong>{currentOption?.label ?? currentLocale}</strong>
90
+ </p>
91
+ </div>
92
+ <button type="button" class="dialog-close" aria-label={closeLabel}>×</button>
93
+ </div>
94
+ <div class="language-grid-scroller">
95
+ <div class="language-grid" role="listbox" aria-label={accessibleLabel}>
96
+ {navigationOptions.map((option) => (
97
+ <button
98
+ type="button"
99
+ role="option"
100
+ data-locale={option.code}
101
+ data-href={option.href}
102
+ lang={option.lang}
103
+ aria-selected={option.selected ? 'true' : 'false'}
104
+ class:list={['language-option', { 'language-option-selected': option.selected }]}
105
+ >
106
+ <span class="language-option-label">{option.label}</span>
107
+ {option.selected && <span class="selected-badge">{selectedLabel}</span>}
108
+ </button>
109
+ ))}
110
+ </div>
111
+ </div>
112
+ </dialog>
113
+ </hagilight-language-chooser>
114
+ )
115
+ }
116
+
117
+ <script>
118
+ import {
119
+ getKeyboardTargetIndex,
120
+ persistStarlightLocaleSelection,
121
+ } from './dist/language-routing.js';
122
+
123
+ class HagilightLanguageChooser extends HTMLElement {
124
+ connectedCallback() {
125
+ if (this.dataset.initialized) return;
126
+ this.dataset.initialized = 'true';
127
+
128
+ const trigger = this.querySelector('.language-trigger');
129
+ const dialog = this.querySelector('.language-dialog');
130
+ const closeButton = this.querySelector('.dialog-close');
131
+ const options = [...this.querySelectorAll('[role="option"]')];
132
+ if (!(trigger instanceof HTMLButtonElement)
133
+ || !(dialog instanceof HTMLDialogElement)
134
+ || !(closeButton instanceof HTMLButtonElement)) return;
135
+
136
+ const closeDialog = () => {
137
+ if (dialog.open) dialog.close();
138
+ else trigger.focus();
139
+ };
140
+
141
+ trigger.addEventListener('click', () => {
142
+ if (dialog.open) {
143
+ closeDialog();
144
+ return;
145
+ }
146
+ dialog.showModal();
147
+ trigger.setAttribute('aria-expanded', 'true');
148
+ const activeOption = options.find((option) => option.getAttribute('aria-selected') === 'true');
149
+ (activeOption ?? options[0])?.focus();
150
+ });
151
+ closeButton.addEventListener('click', closeDialog);
152
+ dialog.addEventListener('cancel', (event) => {
153
+ event.preventDefault();
154
+ closeDialog();
155
+ });
156
+ dialog.addEventListener('close', () => {
157
+ trigger.setAttribute('aria-expanded', 'false');
158
+ trigger.focus();
159
+ });
160
+ dialog.addEventListener('click', (event) => {
161
+ if (event.target === dialog) closeDialog();
162
+ });
163
+ dialog.querySelector('[role="listbox"]')?.addEventListener('keydown', (event) => {
164
+ if (!(event.target instanceof HTMLButtonElement)) return;
165
+ const index = options.indexOf(event.target);
166
+ const nextIndex = getKeyboardTargetIndex(index, event.key, options.length);
167
+ if (nextIndex !== index) {
168
+ event.preventDefault();
169
+ options[nextIndex]?.focus();
170
+ }
171
+ });
172
+ for (const option of options) {
173
+ option.addEventListener('click', () => {
174
+ const locale = option.dataset.locale;
175
+ const href = option.dataset.href;
176
+ if (!locale || !href) return;
177
+ closeDialog();
178
+ if (option.getAttribute('aria-selected') === 'true') return;
179
+ persistStarlightLocaleSelection(locale);
180
+ const target = new URL(href, window.location.origin);
181
+ target.search = window.location.search;
182
+ target.hash = window.location.hash;
183
+ window.location.assign(target.toString());
184
+ });
185
+ }
186
+ }
187
+ }
188
+
189
+ if (!customElements.get('hagilight-language-chooser')) {
190
+ customElements.define('hagilight-language-chooser', HagilightLanguageChooser);
191
+ }
192
+ </script>
193
+
194
+ <style>
195
+ .language-switcher {
196
+ display: inline-flex;
197
+ align-items: center;
198
+ }
199
+
200
+ .language-trigger {
201
+ display: inline-flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ gap: 0.55rem;
205
+ min-width: 7.25rem;
206
+ min-height: 2.125rem;
207
+ padding: 0 0.8rem;
208
+ border: 1px solid var(--sl-color-gray-5);
209
+ border-radius: 999px;
210
+ background: color-mix(in srgb, var(--sl-color-black) 8%, transparent);
211
+ color: var(--sl-color-text-high);
212
+ cursor: pointer;
213
+ font: inherit;
214
+ transition: background-color var(--sl-transition-fast), border-color var(--sl-transition-fast);
215
+ }
216
+
217
+ .language-trigger:hover,
218
+ .dialog-close:hover,
219
+ .language-option:hover {
220
+ background: var(--sl-color-gray-6);
221
+ border-color: var(--sl-color-accent-high);
222
+ }
223
+
224
+ .language-trigger:focus-visible,
225
+ .dialog-close:focus-visible,
226
+ .language-option:focus-visible {
227
+ outline: 2px solid var(--sl-color-accent-high);
228
+ outline-offset: 2px;
229
+ }
230
+
231
+ .trigger-icon {
232
+ flex: none;
233
+ width: 1rem;
234
+ height: 1rem;
235
+ color: var(--sl-color-gray-2);
236
+ }
237
+
238
+ .trigger-label {
239
+ max-width: 10rem;
240
+ overflow: hidden;
241
+ text-overflow: ellipsis;
242
+ white-space: nowrap;
243
+ font-size: var(--sl-text-sm);
244
+ font-weight: var(--sl-font-weight-medium);
245
+ }
246
+
247
+ .trigger-chevron {
248
+ font-size: 0.75rem;
249
+ transition: transform var(--sl-transition-fast);
250
+ }
251
+
252
+ .language-switcher:has(.language-dialog[open]) .trigger-chevron {
253
+ transform: rotate(180deg);
254
+ }
255
+
256
+ .language-dialog {
257
+ position: fixed;
258
+ top: 50%;
259
+ left: 50%;
260
+ width: min(54rem, calc(100vw - 3rem));
261
+ max-width: none;
262
+ max-height: min(30rem, calc(100dvh - 3rem));
263
+ margin: 0;
264
+ padding: 1.1rem 1.1rem 1rem;
265
+ border: 1px solid var(--sl-color-gray-5);
266
+ border-radius: 1rem;
267
+ background: color-mix(in srgb, var(--sl-color-bg) 94%, transparent);
268
+ color: var(--sl-color-text);
269
+ box-shadow: 0 24px 64px rgba(2, 6, 23, 0.32);
270
+ transform: translate(-50%, -50%);
271
+ }
272
+
273
+ .language-dialog::backdrop {
274
+ background: rgba(15, 23, 42, 0.52);
275
+ }
276
+
277
+ .dialog-header {
278
+ display: flex;
279
+ align-items: flex-start;
280
+ justify-content: space-between;
281
+ gap: 1rem;
282
+ margin-bottom: 1rem;
283
+ }
284
+
285
+ .dialog-title {
286
+ margin: 0;
287
+ color: var(--sl-color-white);
288
+ font-size: 1rem;
289
+ font-weight: 700;
290
+ }
291
+
292
+ .dialog-current {
293
+ margin: 0.35rem 0 0;
294
+ font-size: var(--sl-text-sm);
295
+ }
296
+
297
+ .dialog-close {
298
+ display: inline-flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ width: 2.25rem;
302
+ height: 2.25rem;
303
+ border: 1px solid var(--sl-color-gray-5);
304
+ border-radius: 999px;
305
+ background: transparent;
306
+ color: var(--sl-color-text-high);
307
+ cursor: pointer;
308
+ font: inherit;
309
+ font-size: 1.125rem;
310
+ }
311
+
312
+ .language-grid-scroller {
313
+ max-height: min(20rem, calc(100dvh - 11rem));
314
+ overflow: auto;
315
+ border: 1px solid var(--sl-color-gray-5);
316
+ border-radius: 0.875rem;
317
+ background: color-mix(in srgb, var(--sl-color-black) 6%, transparent);
318
+ padding: 0.95rem 1rem;
319
+ }
320
+
321
+ .language-grid {
322
+ display: grid;
323
+ grid-template-columns: repeat(5, minmax(7.75rem, 8.75rem));
324
+ justify-content: center;
325
+ gap: 0.7rem;
326
+ }
327
+
328
+ .language-option {
329
+ display: flex;
330
+ min-height: 4.2rem;
331
+ width: 100%;
332
+ flex-direction: column;
333
+ align-items: center;
334
+ justify-content: center;
335
+ gap: 0.45rem;
336
+ border: 1px solid var(--sl-color-gray-5);
337
+ border-radius: 0.875rem;
338
+ background: color-mix(in srgb, var(--sl-color-bg-nav) 88%, transparent);
339
+ color: var(--sl-color-text-high);
340
+ cursor: pointer;
341
+ font: inherit;
342
+ font-size: 0.95rem;
343
+ font-weight: 700;
344
+ padding: 0.75rem 0.65rem;
345
+ text-align: center;
346
+ }
347
+
348
+ .language-option-selected {
349
+ border-color: var(--sl-color-accent-high);
350
+ background: color-mix(in srgb, var(--sl-color-accent-high) 18%, transparent);
351
+ }
352
+
353
+ .selected-badge {
354
+ display: inline-flex;
355
+ align-items: center;
356
+ justify-content: center;
357
+ min-height: 1.45rem;
358
+ padding: 0 0.55rem;
359
+ border-radius: 999px;
360
+ background: color-mix(in srgb, var(--sl-color-accent-high) 18%, transparent);
361
+ color: var(--sl-color-accent-high);
362
+ font-size: 0.75rem;
363
+ }
364
+
365
+ @media (max-width: 72rem) {
366
+ .language-dialog {
367
+ width: min(46rem, calc(100vw - 2.5rem));
368
+ }
369
+
370
+ .language-grid {
371
+ grid-template-columns: repeat(4, minmax(7.35rem, 8.35rem));
372
+ }
373
+ }
374
+
375
+ @media (max-width: 50rem) {
376
+ .language-dialog {
377
+ width: calc(100vw - 2rem);
378
+ max-height: calc(100dvh - 2rem);
379
+ padding: 1rem;
380
+ }
381
+
382
+ .language-grid {
383
+ grid-template-columns: repeat(2, minmax(0, 1fr));
384
+ }
385
+ }
386
+ </style>