Skip to content

Commit 603a55b

Browse files
authored
docs: add interactive bilingual quick start selector. (#2500)
1 parent 46d99f4 commit 603a55b

10 files changed

Lines changed: 607 additions & 323 deletions

File tree

‎.github/workflows/build_docs.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,4 @@ jobs:
5454
cache-dependency-path: docs/package-lock.json
5555
- run: npm ci
5656
- run: npm run check
57+
- run: npm test

‎docs/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"build": "astro build",
99
"preview": "astro preview",
1010
"check": "astro check",
11+
"test": "node --experimental-strip-types --test tests/*.test.ts",
1112
"astro": "astro"
1213
},
1314
"dependencies": {

‎docs/src/components/MarkdownContent.astro‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Copyright 2026 The xLLM Authors. Licensed under the Apache License, Version 2.0.
33
import DefaultMarkdownContent from '@astrojs/starlight/components/MarkdownContent.astro';
44
import ModelCatalog from './ModelCatalog.astro';
5+
import QuickStart from './QuickStart.astro';
56
67
const { entry } = Astro.locals.starlightRoute;
78
const isModelCatalog = /^(en|zh)\/supported_models$/.test(entry.id);
@@ -11,6 +12,10 @@ const isModelCatalog = /^(en|zh)\/supported_models$/.test(entry.id);
1112
<ModelCatalog locale={entry.id.startsWith('zh/') ? 'zh' : 'en'}>
1213
<DefaultMarkdownContent><slot /></DefaultMarkdownContent>
1314
</ModelCatalog>
15+
) : /^(en|zh)\/getting_started\/quick_start$/.test(entry.id) ? (
16+
<QuickStart locale={entry.id.startsWith('zh/') ? 'zh' : 'en'}>
17+
<DefaultMarkdownContent><slot /></DefaultMarkdownContent>
18+
</QuickStart>
1419
) : (
1520
<DefaultMarkdownContent><slot /></DefaultMarkdownContent>
1621
)}

‎docs/src/components/PageTitle.astro‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
---
22
import { existsSync, readFileSync } from 'node:fs';
33
import { join } from 'node:path';
4+
import { expandQuickStartMarkdown } from '../data/quickStart';
45
56
const { entry } = Astro.locals.starlightRoute;
67
const title = entry.data.title;
78
const sourcePath = entry.filePath ? join(process.cwd(), entry.filePath) : undefined;
89
const sourceMarkdown =
910
sourcePath && existsSync(sourcePath) ? readFileSync(sourcePath, 'utf-8') : '';
10-
const bodyMarkdown = stripFrontmatter(sourceMarkdown).trim();
11+
let bodyMarkdown = stripFrontmatter(sourceMarkdown).trim();
12+
if (/^(en|zh)\/getting_started\/quick_start$/.test(entry.id)) {
13+
bodyMarkdown = expandQuickStartMarkdown(bodyMarkdown, entry.id.startsWith('zh/') ? 'zh' : 'en');
14+
}
1115
const copyMarkdown = [`# ${title}`, bodyMarkdown].filter(Boolean).join('\n\n') + '\n';
1216
const copyPayload = JSON.stringify(copyMarkdown).replace(/</g, '\\u003c');
1317
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
---
2+
// Copyright 2026 The xLLM Authors. Licensed under the Apache License, Version 2.0.
3+
import { Code } from '@astrojs/starlight/components';
4+
import { getQuickStart } from '../data/quickStart';
5+
6+
interface Props {
7+
locale: 'en' | 'zh';
8+
}
9+
10+
const { locale } = Astro.props;
11+
const { text, groups, combinations } = getQuickStart(locale);
12+
---
13+
14+
<xllm-quick-start data-ready={text.ready} data-unavailable={text.unavailable}>
15+
<slot />
16+
<div class="quick-start not-content" data-quick-start-content hidden>
17+
<form aria-label={text.preferences}>
18+
{groups.map((group) => (
19+
<fieldset>
20+
<legend>{group.label}</legend>
21+
<div class="quick-start-options" data-group={group.name}>
22+
{group.options.map((option) => (
23+
<label>
24+
<input type="radio" name={group.name} value={option.id} checked={option.id === group.selected} />
25+
<span>{option.label}</span>
26+
</label>
27+
))}
28+
</div>
29+
</fieldset>
30+
))}
31+
</form>
32+
<div class="quick-start-summary" role="status" aria-live="polite" aria-atomic="true"></div>
33+
{combinations.map(({ platform, build, image, command, notice, key }) => (
34+
<div data-installation={key} hidden>
35+
{image ? (
36+
<>
37+
<Code code={command} lang="bash" title={text.commandTitle} locale={locale === 'zh' ? 'zh-CN' : 'en'} />
38+
<p class="quick-start-next">
39+
{build.id === 'release' ? (
40+
<>{text.releaseBefore}<a href={`/${locale}/getting_started/launch_xllm/`}>{text.releaseLink}</a>{text.releaseAfter}</>
41+
) : (
42+
<>{text.devBefore}<a href={text.buildAnchor}>{text.devLink}</a>{text.devAfter}</>
43+
)}
44+
</p>
45+
</>
46+
) : (
47+
<p class="quick-start-unavailable">{notice}</p>
48+
)}
49+
<p class="quick-start-guide"><a href={`/${locale}/hardware/${platform.guide}/`}>{platform.label} {text.guide} →</a></p>
50+
</div>
51+
))}
52+
</div>
53+
</xllm-quick-start>
54+
55+
<script>
56+
class QuickStart extends HTMLElement {
57+
connectedCallback() {
58+
if (this.dataset.initialized) {
59+
return;
60+
}
61+
const target = this.querySelector('[data-quick-start]');
62+
const content = this.querySelector<HTMLElement>('[data-quick-start-content]');
63+
if (!target || !content) {
64+
return;
65+
}
66+
this.dataset.initialized = 'true';
67+
target.replaceChildren(content);
68+
content.hidden = false;
69+
const form = target.querySelector('form')!;
70+
const summary = target.querySelector('.quick-start-summary')!;
71+
const panels = target.querySelectorAll<HTMLElement>('[data-installation]');
72+
const update = () => {
73+
const selection = new FormData(form);
74+
const key = ['hardware', 'cpu', 'build'].map((name) => selection.get(name)).join('-');
75+
for (const panel of panels) {
76+
panel.hidden = panel.dataset.installation !== key;
77+
}
78+
const labels = Array.from(form.querySelectorAll<HTMLInputElement>('input:checked'))
79+
.map((input) => input.nextElementSibling!.textContent);
80+
const active = target.querySelector<HTMLElement>('[data-installation]:not([hidden])')!;
81+
summary.textContent = `${labels.join(' / ')} — ${active.querySelector('pre') ? this.dataset.ready : this.dataset.unavailable}`;
82+
};
83+
form.addEventListener('submit', (event) => event.preventDefault());
84+
form.addEventListener('change', update);
85+
update();
86+
}
87+
}
88+
if (!customElements.get('xllm-quick-start')) {
89+
customElements.define('xllm-quick-start', QuickStart);
90+
}
91+
</script>
92+
93+
<style is:global>
94+
xllm-quick-start {
95+
display: block;
96+
}
97+
.quick-start {
98+
container: quick-start / inline-size;
99+
margin-block: 1.5rem;
100+
padding: 1.5rem;
101+
border: 1px solid var(--sl-color-gray-5);
102+
border-radius: .8rem;
103+
background: var(--sl-color-bg);
104+
}
105+
.quick-start [hidden] {
106+
display: none !important;
107+
}
108+
.quick-start form {
109+
display: grid;
110+
grid-template-columns: repeat(2, minmax(0, 1fr));
111+
gap: 1.25rem;
112+
}
113+
.quick-start fieldset:first-child {
114+
grid-column: 1 / -1;
115+
}
116+
.quick-start fieldset {
117+
min-width: 0;
118+
margin: 0;
119+
padding: 0;
120+
border: 0;
121+
}
122+
.quick-start legend {
123+
margin-bottom: .65rem;
124+
padding: 0;
125+
color: var(--sl-color-white);
126+
font-size: .9rem;
127+
font-weight: 600;
128+
}
129+
.quick-start-options {
130+
display: flex;
131+
flex-wrap: wrap;
132+
gap: .5rem;
133+
}
134+
.quick-start-options label {
135+
position: relative;
136+
cursor: pointer;
137+
}
138+
.quick-start-options[data-group='hardware'] label {
139+
flex: 1 1 calc(25% - .5rem);
140+
}
141+
.quick-start-options[data-group='hardware'] span {
142+
text-align: center;
143+
white-space: nowrap;
144+
}
145+
.quick-start-options input {
146+
position: absolute;
147+
width: 1px;
148+
height: 1px;
149+
opacity: 0;
150+
}
151+
.quick-start-options span {
152+
display: block;
153+
padding: .5rem .85rem;
154+
border: 1px solid var(--sl-color-gray-5);
155+
border-radius: .4rem;
156+
color: var(--sl-color-gray-2);
157+
font-size: .875rem;
158+
line-height: 1.5;
159+
}
160+
.quick-start-options label:hover span {
161+
border-color: var(--sl-color-gray-3);
162+
}
163+
.quick-start-options input:checked + span {
164+
border-color: var(--sl-color-text-accent);
165+
background: var(--sl-color-accent-low);
166+
color: var(--sl-color-white);
167+
font-weight: 600;
168+
}
169+
.quick-start-options input:focus-visible + span {
170+
outline: 2px solid var(--sl-color-text-accent);
171+
outline-offset: 3px;
172+
}
173+
.quick-start-summary {
174+
margin-block: 1.5rem 1rem;
175+
padding-top: 1.25rem;
176+
border-top: 1px solid var(--sl-color-gray-5);
177+
color: var(--sl-color-gray-2);
178+
font-size: .85rem;
179+
}
180+
.quick-start a {
181+
color: var(--sl-color-text-accent);
182+
text-underline-offset: .2em;
183+
}
184+
.quick-start-next,
185+
.quick-start-unavailable,
186+
.quick-start-guide {
187+
margin: 1rem 0 0;
188+
font-size: .9rem;
189+
line-height: 1.7;
190+
}
191+
.quick-start-unavailable {
192+
padding: 1rem;
193+
border: 1px dashed var(--sl-color-gray-4);
194+
border-radius: .4rem;
195+
background: var(--sl-color-gray-6);
196+
}
197+
@container quick-start (max-width: 38rem) {
198+
.quick-start-options[data-group='hardware'] label {
199+
flex-basis: calc(50% - .5rem);
200+
}
201+
}
202+
@container quick-start (max-width: 24rem) {
203+
.quick-start form {
204+
grid-template-columns: minmax(0, 1fr);
205+
}
206+
}
207+
@media (max-width: 40rem) {
208+
.quick-start {
209+
padding: 1rem;
210+
}
211+
.quick-start-options span {
212+
padding-inline: .65rem;
213+
}
214+
}
215+
</style>

0 commit comments

Comments
 (0)