stack-site-builder 1.10.0 → 1.13.0
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/CHANGELOG.md +126 -0
- package/README.md +48 -1
- package/index.d.ts +10 -0
- package/index.mjs +63 -30
- package/markdown.mjs +24 -12
- package/package.json +9 -3
- package/src/components/ArticleLink.astro +2 -2
- package/src/components/ConceptDetail.astro +2 -2
- package/src/components/ConceptLink.astro +2 -2
- package/src/components/ConceptTools.astro +2 -2
- package/src/components/LanguageSwitcher.astro +7 -7
- package/src/components/PageDetail.astro +52 -0
- package/src/components/ProjectViewer.astro +2 -2
- package/src/components/SampleIndex.astro +2 -2
- package/src/components/SampleProject.astro +3 -3
- package/src/components/StackCard.astro +3 -3
- package/src/components/StackDetail.astro +3 -3
- package/src/components/ThemeToggle.astro +1 -3
- package/src/components/ToolMeta.astro +2 -2
- package/src/content.ts +29 -1
- package/src/i18n/ui.ts +70 -13
- package/src/layouts/BaseLayout.astro +128 -57
- package/src/lib/dates.ts +2 -2
- package/src/lib/facets.ts +2 -2
- package/src/lib/locales.ts +16 -0
- package/src/lib/pages.ts +24 -0
- package/src/lib/sections.ts +34 -0
- package/src/pages/[...lang]/[page].astro +30 -0
- package/src/pages/{ko → [...lang]}/article/[...id].astro +11 -5
- package/src/pages/[...lang]/article/category/[id].astro +25 -0
- package/src/pages/[...lang]/article/index.astro +18 -0
- package/src/pages/[...lang]/categories/[id].astro +28 -0
- package/src/pages/{ko → [...lang]}/concept/[...id].astro +11 -5
- package/src/pages/[...lang]/concept/category/[id].astro +25 -0
- package/src/pages/[...lang]/concept/index.astro +18 -0
- package/src/pages/[...lang]/glossary.astro +18 -0
- package/src/pages/[...lang]/index.astro +16 -0
- package/src/pages/[...lang]/sample/[folder].astro +25 -0
- package/src/pages/[...lang]/sample/index.astro +18 -0
- package/src/pages/[...lang]/slides/[deck].astro +22 -0
- package/src/pages/[...lang]/slides/index.astro +18 -0
- package/src/pages/{ko → [...lang]}/stack/[...id].astro +20 -13
- package/src/pages/[...lang]/tags/[tag].astro +24 -0
- package/src/pages/[...lang]/vendors/[vendor].astro +24 -0
- package/src/styles/global.css +7 -0
- package/src/pages/article/[...id].astro +0 -24
- package/src/pages/article/category/[id].astro +0 -17
- package/src/pages/article/index.astro +0 -12
- package/src/pages/categories/[id].astro +0 -25
- package/src/pages/concept/[...id].astro +0 -24
- package/src/pages/concept/category/[id].astro +0 -17
- package/src/pages/concept/index.astro +0 -12
- package/src/pages/glossary.astro +0 -12
- package/src/pages/index.astro +0 -9
- package/src/pages/ko/article/category/[id].astro +0 -17
- package/src/pages/ko/article/index.astro +0 -12
- package/src/pages/ko/categories/[id].astro +0 -24
- package/src/pages/ko/concept/category/[id].astro +0 -17
- package/src/pages/ko/concept/index.astro +0 -12
- package/src/pages/ko/glossary.astro +0 -12
- package/src/pages/ko/index.astro +0 -9
- package/src/pages/ko/sample/[folder].astro +0 -16
- package/src/pages/ko/sample/index.astro +0 -12
- package/src/pages/ko/slides/[deck].astro +0 -14
- package/src/pages/ko/slides/index.astro +0 -12
- package/src/pages/ko/tags/[tag].astro +0 -18
- package/src/pages/ko/vendors/[vendor].astro +0 -18
- package/src/pages/sample/[folder].astro +0 -16
- package/src/pages/sample/index.astro +0 -12
- package/src/pages/slides/[deck].astro +0 -14
- package/src/pages/slides/index.astro +0 -12
- package/src/pages/stack/[...id].astro +0 -51
- package/src/pages/tags/[tag].astro +0 -18
- package/src/pages/vendors/[vendor].astro +0 -18
|
@@ -7,21 +7,21 @@
|
|
|
7
7
|
// on the linked sample page instead of being embedded in the article.
|
|
8
8
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
9
9
|
import { listProjects } from '../lib/project';
|
|
10
|
-
import { useTranslations, type Lang } from '../i18n/ui';
|
|
10
|
+
import { useTranslations, dateLocaleOf, defaultLang, type Lang } from '../i18n/ui';
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
folder: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const { folder } = Astro.props;
|
|
17
|
-
const lang: Lang = Astro.currentLocale
|
|
17
|
+
const lang: Lang = (Astro.currentLocale ?? defaultLang) as Lang;
|
|
18
18
|
const t = useTranslations(lang);
|
|
19
19
|
const target = listProjects(lang).find((p) => p.folder === folder);
|
|
20
20
|
|
|
21
21
|
function formatDate(iso: string): string {
|
|
22
22
|
const [y, m, d] = iso.split('-').map(Number);
|
|
23
23
|
if (!y || !m || !d) return iso;
|
|
24
|
-
return new Intl.DateTimeFormat(lang
|
|
24
|
+
return new Intl.DateTimeFormat(dateLocaleOf(lang), {
|
|
25
25
|
year: 'numeric',
|
|
26
26
|
month: 'long',
|
|
27
27
|
day: 'numeric',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
|
-
import {
|
|
3
|
+
import { pricingLabel, useTranslations, type Lang } from '../i18n/ui';
|
|
4
4
|
import { getRepoStats, formatStars, stalenessOf } from '../lib/github';
|
|
5
5
|
import { pricingTags, COMPLETELY_FREE } from '../lib/pricing';
|
|
6
6
|
import { isImageRef, resolveImageUrl } from '../lib/assets';
|
|
@@ -155,7 +155,7 @@ const searchText = `${name} ${description} ${tags.join(' ')}`.toLowerCase();
|
|
|
155
155
|
pricingTags(pricing).map((p) => (
|
|
156
156
|
<span
|
|
157
157
|
data-pricing-tag={p}
|
|
158
|
-
title={
|
|
158
|
+
title={pricingLabel(lang, p)}
|
|
159
159
|
class:list={[p === COMPLETELY_FREE ? 'aas-free' : 'aas-pricing', 'rounded-lg px-2.5 py-1']}
|
|
160
160
|
>
|
|
161
161
|
{p === 'open-source' && (
|
|
@@ -175,7 +175,7 @@ const searchText = `${name} ${description} ${tags.join(' ')}`.toLowerCase();
|
|
|
175
175
|
<polyline points="8 6 2 12 8 18" />
|
|
176
176
|
</svg>
|
|
177
177
|
)}
|
|
178
|
-
<span data-chip-label>{
|
|
178
|
+
<span data-chip-label>{pricingLabel(lang, p)}</span>
|
|
179
179
|
</span>
|
|
180
180
|
))
|
|
181
181
|
}
|
|
@@ -3,7 +3,7 @@ import { render } from 'astro:content';
|
|
|
3
3
|
import { Image } from 'astro:assets';
|
|
4
4
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
5
5
|
import { pathOf } from '@aas-data/categories';
|
|
6
|
-
import {
|
|
6
|
+
import { pricingLabel, useTranslations, type Lang } from '../i18n/ui';
|
|
7
7
|
import { pricingTags, COMPLETELY_FREE } from '../lib/pricing';
|
|
8
8
|
import StarIcon from './StarIcon.astro';
|
|
9
9
|
import { slugOf, slugifyName, toolResolver, type StackEntry } from '../lib/stacks';
|
|
@@ -203,7 +203,7 @@ const relatedToolsByFolder = new Map(projectRail.map((p) => [p.folder, p.tools])
|
|
|
203
203
|
pricingTags(data.pricing).map((p) => (
|
|
204
204
|
<span
|
|
205
205
|
data-pricing-tag={p}
|
|
206
|
-
title={
|
|
206
|
+
title={pricingLabel(lang, p)}
|
|
207
207
|
class:list={[p === COMPLETELY_FREE ? 'aas-free' : 'aas-pricing', 'inline-flex items-center gap-1 rounded-lg px-2.5 py-1']}
|
|
208
208
|
>
|
|
209
209
|
{p === 'open-source' && (
|
|
@@ -223,7 +223,7 @@ const relatedToolsByFolder = new Map(projectRail.map((p) => [p.folder, p.tools])
|
|
|
223
223
|
<polyline points="8 6 2 12 8 18" />
|
|
224
224
|
</svg>
|
|
225
225
|
)}
|
|
226
|
-
<span>{
|
|
226
|
+
<span>{pricingLabel(lang, p)}</span>
|
|
227
227
|
</span>
|
|
228
228
|
))
|
|
229
229
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
3
|
-
import ChevronDown from './ChevronDown.astro';
|
|
4
3
|
|
|
5
4
|
interface Props {
|
|
6
5
|
lang: Lang;
|
|
@@ -18,12 +17,11 @@ const options = [
|
|
|
18
17
|
|
|
19
18
|
<details class="aas-theme">
|
|
20
19
|
<summary
|
|
21
|
-
class="flex cursor-pointer list-none items-center
|
|
20
|
+
class="flex cursor-pointer list-none items-center rounded-lg border border-[var(--aas-border)] px-2.5 py-1 text-[var(--aas-text)] select-none"
|
|
22
21
|
aria-label={t('theme.label')}
|
|
23
22
|
title={t('theme.label')}
|
|
24
23
|
>
|
|
25
24
|
<span data-theme-icon aria-hidden="true">☀</span>
|
|
26
|
-
<ChevronDown class="text-[var(--aas-muted)]" />
|
|
27
25
|
</summary>
|
|
28
26
|
<ul
|
|
29
27
|
class="absolute right-0 z-10 mt-1 min-w-[8rem] overflow-hidden rounded-lg border border-[var(--aas-border)] bg-[var(--aas-panel)] py-1 shadow-lg"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
2
|
+
import { pricingLabel, licenseLabel, useTranslations, type Lang } from '../i18n/ui';
|
|
3
3
|
import { formatStars, type Staleness } from '../lib/github';
|
|
4
4
|
import { pricingTags, COMPLETELY_FREE } from '../lib/pricing';
|
|
5
5
|
import StarIcon from './StarIcon.astro';
|
|
@@ -81,7 +81,7 @@ const chip = 'aas-chip rounded-lg px-2.5 py-1';
|
|
|
81
81
|
{language && <span class={chip}>{language}</span>}
|
|
82
82
|
{pricingTags(pricing).map((p) => (
|
|
83
83
|
<span class:list={[p === COMPLETELY_FREE ? 'aas-free' : 'aas-pricing', 'rounded-lg px-2.5 py-1']}>
|
|
84
|
-
{
|
|
84
|
+
{pricingLabel(lang, p)}
|
|
85
85
|
</span>
|
|
86
86
|
))}
|
|
87
87
|
{license && (
|
package/src/content.ts
CHANGED
|
@@ -224,5 +224,33 @@ export function defineAasCollections({ categoryMap }: { categoryMap: Map<string,
|
|
|
224
224
|
}),
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
/**
|
|
228
|
+
* The `pages` collection holds standalone top-level pages — an About/소개, a
|
|
229
|
+
* contact page, terms, etc. Unlike the other collections there's no index or
|
|
230
|
+
* taxonomy: each entry renders on its own at the site root (`/<slug>/`), and
|
|
231
|
+
* (when `nav` is set) shows up as its own item in the header navigation. This
|
|
232
|
+
* is how a site adds a first-class section that is just one page.
|
|
233
|
+
*
|
|
234
|
+
* Locale-partitioned like the rest: `pages/en/about.mdx`, `pages/ko/about.mdx`.
|
|
235
|
+
*/
|
|
236
|
+
const pages = defineCollection({
|
|
237
|
+
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/pages' }),
|
|
238
|
+
schema: ({ image }) =>
|
|
239
|
+
z.object({
|
|
240
|
+
title: z.string(),
|
|
241
|
+
description: z.string().optional(),
|
|
242
|
+
image: image().optional(), // hero image (optimized; relative to the file)
|
|
243
|
+
imageAlt: z.string().optional(),
|
|
244
|
+
// Header-nav placement. `nav` toggles the link; `navLabel` overrides the
|
|
245
|
+
// link's accessible label (defaults to `title`); `order` sorts the page
|
|
246
|
+
// links among themselves (lower first). A page with `nav: false` still
|
|
247
|
+
// renders at its URL but isn't linked from the header.
|
|
248
|
+
nav: z.boolean().default(true),
|
|
249
|
+
navLabel: z.string().optional(),
|
|
250
|
+
order: z.number().default(0),
|
|
251
|
+
draft: z.boolean().default(false),
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return { stacks, articles, concepts, slides, pages };
|
|
228
256
|
}
|
package/src/i18n/ui.ts
CHANGED
|
@@ -8,14 +8,48 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { site } from '@aas-data/site';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/** One locale the site ships. `code` matches the astro.config `i18n` locale and
|
|
12
|
+
* the content folder (`stacks/<code>/…`); `label` names it in the switcher;
|
|
13
|
+
* `dateLocale` is the BCP-47 tag for date formatting (defaults to `code`). */
|
|
14
|
+
export interface LocaleDef {
|
|
15
|
+
code: string;
|
|
16
|
+
label: string;
|
|
17
|
+
dateLocale?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The theme ships English + Korean. A consuming site overrides this list via
|
|
21
|
+
// `site.locales` to rename them, reorder them, or add its own (e.g. `ja`). The
|
|
22
|
+
// list is authoritative and ordered: it drives the language switcher, and its
|
|
23
|
+
// FIRST entry is the default locale (must match astro.config `i18n.defaultLocale`).
|
|
24
|
+
const themeLocales: LocaleDef[] = [
|
|
25
|
+
{ code: 'en', label: 'English', dateLocale: 'en-US' },
|
|
26
|
+
{ code: 'ko', label: '한국어', dateLocale: 'ko-KR' },
|
|
27
|
+
];
|
|
28
|
+
// Read defensively: a site that hasn't opted into custom locales simply has no
|
|
29
|
+
// `locales` field, and should keep the theme's en/ko default (not a type error).
|
|
30
|
+
const siteLocales = (site as { locales?: LocaleDef[] }).locales;
|
|
31
|
+
const localeList: LocaleDef[] =
|
|
32
|
+
Array.isArray(siteLocales) && siteLocales.length ? siteLocales : themeLocales;
|
|
33
|
+
|
|
34
|
+
/** Display names keyed by code — the language switcher iterates this. */
|
|
35
|
+
export const languages: Record<string, string> = Object.fromEntries(
|
|
36
|
+
localeList.map((l) => [l.code, l.label]),
|
|
37
|
+
);
|
|
15
38
|
|
|
16
|
-
|
|
39
|
+
// Locales are site-configurable, so `Lang` can't be a fixed union of codes.
|
|
40
|
+
export type Lang = string;
|
|
17
41
|
|
|
18
|
-
export const defaultLang: Lang =
|
|
42
|
+
export const defaultLang: Lang = localeList[0].code;
|
|
43
|
+
|
|
44
|
+
/** BCP-47 tag for `Intl` date/number formatting in `lang` (falls back to the
|
|
45
|
+
* bare code, then the default locale). */
|
|
46
|
+
export function dateLocaleOf(lang: Lang): string {
|
|
47
|
+
return (
|
|
48
|
+
localeList.find((l) => l.code === lang)?.dateLocale ??
|
|
49
|
+
localeList.find((l) => l.code === defaultLang)?.dateLocale ??
|
|
50
|
+
lang
|
|
51
|
+
);
|
|
52
|
+
}
|
|
19
53
|
|
|
20
54
|
/** UI chrome strings, keyed by a dotted id. */
|
|
21
55
|
export const ui = {
|
|
@@ -29,6 +63,10 @@ export const ui = {
|
|
|
29
63
|
'nav.slides': 'Slides',
|
|
30
64
|
'nav.backToTop': 'Back to top',
|
|
31
65
|
'nav.glossary': 'Glossary',
|
|
66
|
+
'nav.language': 'Language',
|
|
67
|
+
'nav.menu': 'Menu',
|
|
68
|
+
'code.copy': 'Copy code',
|
|
69
|
+
'code.copied': 'Copied',
|
|
32
70
|
'slides.title': 'Slides',
|
|
33
71
|
'slides.tagline': 'Concept decks — the same ideas as the concept pages, in slide form.',
|
|
34
72
|
'slides.deck': 'Deck',
|
|
@@ -167,6 +205,10 @@ export const ui = {
|
|
|
167
205
|
'nav.slides': '슬라이드',
|
|
168
206
|
'nav.backToTop': '맨 위로',
|
|
169
207
|
'nav.glossary': '용어집',
|
|
208
|
+
'nav.language': '언어',
|
|
209
|
+
'nav.menu': '메뉴',
|
|
210
|
+
'code.copy': '코드 복사',
|
|
211
|
+
'code.copied': '복사됨',
|
|
170
212
|
'slides.title': '슬라이드',
|
|
171
213
|
'slides.tagline': '개념 덱 — 개념 페이지와 같은 내용을 슬라이드로 옮겼습니다.',
|
|
172
214
|
'slides.deck': '덱',
|
|
@@ -299,18 +341,33 @@ export const ui = {
|
|
|
299
341
|
export type UIKey = keyof (typeof ui)['en'];
|
|
300
342
|
|
|
301
343
|
/**
|
|
302
|
-
* Returns a `t(key)` translator bound to the given language.
|
|
303
|
-
* override
|
|
304
|
-
*
|
|
344
|
+
* Returns a `t(key)` translator bound to the given language. Lookup order:
|
|
345
|
+
* the site's override for this locale, the theme string for this locale, then
|
|
346
|
+
* the same two for the default locale, then the key itself. The default-locale
|
|
347
|
+
* fallback is what lets a site add a locale (e.g. `ja`) and supply only some
|
|
348
|
+
* strings via `site.ui.ja` — the rest degrade to the default locale instead of
|
|
349
|
+
* throwing on a locale the theme doesn't ship.
|
|
305
350
|
*/
|
|
306
351
|
export function useTranslations(lang: Lang) {
|
|
307
352
|
return function t(key: UIKey): string {
|
|
308
|
-
return
|
|
353
|
+
return (
|
|
354
|
+
site.ui?.[lang]?.[key] ??
|
|
355
|
+
(ui as Record<string, Partial<Record<UIKey, string>>>)[lang]?.[key] ??
|
|
356
|
+
site.ui?.[defaultLang]?.[key] ??
|
|
357
|
+
(ui as Record<string, Partial<Record<UIKey, string>>>)[defaultLang]?.[key] ??
|
|
358
|
+
key
|
|
359
|
+
);
|
|
309
360
|
};
|
|
310
361
|
}
|
|
311
362
|
|
|
363
|
+
/** Localized label for a `pricing` enum value, falling back to the default
|
|
364
|
+
* locale then the raw value (so a site-added locale never crashes). */
|
|
365
|
+
export function pricingLabel(lang: Lang, value: string): string {
|
|
366
|
+
return pricingLabels[lang]?.[value] ?? pricingLabels[defaultLang]?.[value] ?? value;
|
|
367
|
+
}
|
|
368
|
+
|
|
312
369
|
/** Human labels for the `pricing` frontmatter enum, per locale. */
|
|
313
|
-
export const pricingLabels: Record<
|
|
370
|
+
export const pricingLabels: Record<string, Record<string, string>> = {
|
|
314
371
|
en: {
|
|
315
372
|
'completely-free': 'Completely free',
|
|
316
373
|
'open-source': 'Open source',
|
|
@@ -328,10 +385,10 @@ export const pricingLabels: Record<Lang, Record<string, string>> = {
|
|
|
328
385
|
};
|
|
329
386
|
|
|
330
387
|
/** Descriptive (non-name) licenses get localized; real license names pass through. */
|
|
331
|
-
const licenseLabels: Record<
|
|
388
|
+
const licenseLabels: Record<string, Record<string, string>> = {
|
|
332
389
|
en: { proprietary: 'Proprietary' },
|
|
333
390
|
ko: { proprietary: '독점' },
|
|
334
391
|
};
|
|
335
392
|
export function licenseLabel(lang: Lang, value: string): string {
|
|
336
|
-
return licenseLabels[lang][value] ?? value;
|
|
393
|
+
return licenseLabels[lang]?.[value] ?? licenseLabels[defaultLang]?.[value] ?? value;
|
|
337
394
|
}
|
|
@@ -6,6 +6,8 @@ import { useTranslations, languages, type Lang } from '../i18n/ui';
|
|
|
6
6
|
import LanguageSwitcher from '../components/LanguageSwitcher.astro';
|
|
7
7
|
import ThemeToggle from '../components/ThemeToggle.astro';
|
|
8
8
|
import BackToTop from '../components/BackToTop.astro';
|
|
9
|
+
import { getNavPages, pageSlugOf } from '../lib/pages';
|
|
10
|
+
import { sectionEnabled, type SectionKey } from '../lib/sections';
|
|
9
11
|
|
|
10
12
|
interface Props {
|
|
11
13
|
title: string;
|
|
@@ -32,10 +34,79 @@ const localeHomes = Object.fromEntries(
|
|
|
32
34
|
(Object.keys(languages) as Lang[]).map((c) => [c, getRelativeLocaleUrl(c, '')]),
|
|
33
35
|
);
|
|
34
36
|
const isDefaultHome = path === '' && lang === 'en';
|
|
37
|
+
|
|
38
|
+
// Standalone pages (the `pages` collection) that opt into a header-nav link,
|
|
39
|
+
// e.g. an About/소개 page. Rendered as their own nav items alongside the
|
|
40
|
+
// built-in sections; the accessible label comes from the page's own frontmatter.
|
|
41
|
+
const navPages = await getNavPages(lang);
|
|
42
|
+
|
|
43
|
+
// The header sections, as data — rendered twice from one source so the two
|
|
44
|
+
// layouts never drift: an icon-only row (≥sm) and a labelled dropdown menu
|
|
45
|
+
// (<sm, so a phone header stays to a few controls instead of a long icon run).
|
|
46
|
+
// `svg` is the icon's inner markup (drawn into a shared <svg> shell below).
|
|
47
|
+
const allNavItems: {
|
|
48
|
+
href: string;
|
|
49
|
+
label: string;
|
|
50
|
+
svg: string;
|
|
51
|
+
external?: boolean;
|
|
52
|
+
keepFilters?: boolean;
|
|
53
|
+
section?: SectionKey;
|
|
54
|
+
}[] = [
|
|
55
|
+
{
|
|
56
|
+
href: `${home}#categories`,
|
|
57
|
+
label: t('nav.browse'),
|
|
58
|
+
keepFilters: true,
|
|
59
|
+
svg: '<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
href: getRelativeLocaleUrl(lang, 'concept/'),
|
|
63
|
+
label: t('nav.concepts'),
|
|
64
|
+
section: 'concepts',
|
|
65
|
+
svg: '<path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
href: getRelativeLocaleUrl(lang, 'article/'),
|
|
69
|
+
label: t('nav.blog'),
|
|
70
|
+
section: 'articles',
|
|
71
|
+
svg: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M16 13H8"/><path d="M16 17H8"/><path d="M10 9H8"/>',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
href: getRelativeLocaleUrl(lang, 'sample/'),
|
|
75
|
+
label: t('nav.samples'),
|
|
76
|
+
section: 'samples',
|
|
77
|
+
svg: '<polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/>',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
href: getRelativeLocaleUrl(lang, 'slides/'),
|
|
81
|
+
label: t('nav.slides'),
|
|
82
|
+
section: 'slides',
|
|
83
|
+
svg: '<path d="M2 3h20"/><path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"/><path d="m7 21 5-5 5 5"/><path d="M12 12v9"/>',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
href: getRelativeLocaleUrl(lang, 'glossary/'),
|
|
87
|
+
label: t('nav.glossary'),
|
|
88
|
+
section: 'glossary',
|
|
89
|
+
svg: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
|
|
90
|
+
},
|
|
91
|
+
...navPages.map((p) => ({
|
|
92
|
+
href: getRelativeLocaleUrl(lang, `${pageSlugOf(p)}/`),
|
|
93
|
+
label: p.data.navLabel ?? p.data.title,
|
|
94
|
+
section: 'pages' as SectionKey,
|
|
95
|
+
svg: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
|
|
96
|
+
})),
|
|
97
|
+
{
|
|
98
|
+
href: site.repoUrl,
|
|
99
|
+
label: 'GitHub',
|
|
100
|
+
external: true,
|
|
101
|
+
svg: '<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C4 2 3 2 3 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 2 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/>',
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
// Drop the nav links for sections the site turned off (their routes aren't built).
|
|
105
|
+
const navItems = allNavItems.filter((item) => !item.section || sectionEnabled(item.section));
|
|
35
106
|
---
|
|
36
107
|
|
|
37
108
|
<!doctype html>
|
|
38
|
-
<html lang={lang}>
|
|
109
|
+
<html lang={lang} data-copy-label={t('code.copy')} data-copied-label={t('code.copied')}>
|
|
39
110
|
<head>
|
|
40
111
|
<meta charset="utf-8" />
|
|
41
112
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
@@ -107,66 +178,67 @@ const isDefaultHome = path === '' && lang === 'en';
|
|
|
107
178
|
{/* Below 600px only the bolt remains, so the narrow header never truncates the wordmark. */}
|
|
108
179
|
<span class="hidden truncate min-[600px]:inline">{site.name}</span>
|
|
109
180
|
</a>
|
|
110
|
-
{/*
|
|
181
|
+
{/* Section links are icon-only (labels live in aria-label). On phones
|
|
182
|
+
(<sm) they collapse into the labelled dropdown menu below, so the
|
|
183
|
+
header stays to a few controls; from sm up the full icon row shows. */}
|
|
111
184
|
<nav class="flex shrink-0 items-center gap-3 text-sm text-[var(--aas-muted)] sm:gap-4">
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
</
|
|
127
|
-
<a
|
|
128
|
-
href={getRelativeLocaleUrl(lang, 'article/')}
|
|
129
|
-
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
130
|
-
aria-label={t('nav.blog')}
|
|
131
|
-
>
|
|
132
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M16 13H8"/><path d="M16 17H8"/><path d="M10 9H8"/></svg>
|
|
133
|
-
</a>
|
|
134
|
-
<a
|
|
135
|
-
href={getRelativeLocaleUrl(lang, 'sample/')}
|
|
136
|
-
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
137
|
-
aria-label={t('nav.samples')}
|
|
138
|
-
>
|
|
139
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>
|
|
140
|
-
</a>
|
|
141
|
-
<a
|
|
142
|
-
href={getRelativeLocaleUrl(lang, 'slides/')}
|
|
143
|
-
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
144
|
-
aria-label={t('nav.slides')}
|
|
145
|
-
>
|
|
146
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 3h20"/><path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"/><path d="m7 21 5-5 5 5"/><path d="M12 12v9"/></svg>
|
|
147
|
-
</a>
|
|
148
|
-
<a
|
|
149
|
-
href={getRelativeLocaleUrl(lang, 'glossary/')}
|
|
150
|
-
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
151
|
-
aria-label={t('nav.glossary')}
|
|
152
|
-
>
|
|
153
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
|
|
154
|
-
</a>
|
|
155
|
-
<a
|
|
156
|
-
href={site.repoUrl}
|
|
157
|
-
target="_blank"
|
|
158
|
-
rel="noopener noreferrer"
|
|
159
|
-
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
160
|
-
aria-label="GitHub"
|
|
161
|
-
>
|
|
162
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C4 2 3 2 3 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 2 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
|
|
163
|
-
</a>
|
|
185
|
+
<div class="hidden items-center gap-3 sm:flex sm:gap-4">
|
|
186
|
+
{
|
|
187
|
+
navItems.map((item) => (
|
|
188
|
+
<a
|
|
189
|
+
href={item.href}
|
|
190
|
+
class="flex items-center no-underline hover:text-[var(--aas-text)]"
|
|
191
|
+
aria-label={item.label}
|
|
192
|
+
{...(item.keepFilters ? { 'data-keep-filters': true } : {})}
|
|
193
|
+
{...(item.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
194
|
+
>
|
|
195
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" set:html={item.svg} />
|
|
196
|
+
</a>
|
|
197
|
+
))
|
|
198
|
+
}
|
|
199
|
+
</div>
|
|
164
200
|
<ThemeToggle lang={lang} />
|
|
165
201
|
<LanguageSwitcher lang={lang} path={path} />
|
|
202
|
+
<details class="aas-menu relative sm:hidden">
|
|
203
|
+
<summary
|
|
204
|
+
class="flex cursor-pointer list-none items-center rounded-lg border border-[var(--aas-border)] px-2.5 py-1 text-[var(--aas-text)] select-none"
|
|
205
|
+
aria-label={t('nav.menu')}
|
|
206
|
+
title={t('nav.menu')}
|
|
207
|
+
>
|
|
208
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h16"/></svg>
|
|
209
|
+
</summary>
|
|
210
|
+
<ul
|
|
211
|
+
class="absolute right-0 z-20 mt-1 min-w-[11rem] overflow-hidden rounded-lg border border-[var(--aas-border)] bg-[var(--aas-panel)] py-1 shadow-lg"
|
|
212
|
+
>
|
|
213
|
+
{
|
|
214
|
+
navItems.map((item) => (
|
|
215
|
+
<li>
|
|
216
|
+
<a
|
|
217
|
+
href={item.href}
|
|
218
|
+
class="flex items-center gap-2.5 px-3 py-2 whitespace-nowrap text-[var(--aas-text)] no-underline hover:bg-[var(--aas-bg)]"
|
|
219
|
+
{...(item.keepFilters ? { 'data-keep-filters': true } : {})}
|
|
220
|
+
{...(item.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
221
|
+
>
|
|
222
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="shrink-0 text-[var(--aas-muted)]" set:html={item.svg} />
|
|
223
|
+
<span>{item.label}</span>
|
|
224
|
+
</a>
|
|
225
|
+
</li>
|
|
226
|
+
))
|
|
227
|
+
}
|
|
228
|
+
</ul>
|
|
229
|
+
</details>
|
|
166
230
|
</nav>
|
|
167
231
|
</div>
|
|
168
232
|
</header>
|
|
169
233
|
|
|
234
|
+
<style>
|
|
235
|
+
/* Hide the default <details> disclosure triangle on the mobile menu
|
|
236
|
+
(Safari needs the -webkit rule even with list-style: none). */
|
|
237
|
+
.aas-menu > summary::-webkit-details-marker {
|
|
238
|
+
display: none;
|
|
239
|
+
}
|
|
240
|
+
</style>
|
|
241
|
+
|
|
170
242
|
<main class:list={['mx-auto px-5 py-10', container]}>
|
|
171
243
|
<slot />
|
|
172
244
|
</main>
|
|
@@ -209,9 +281,8 @@ const isDefaultHome = path === '' && lang === 'en';
|
|
|
209
281
|
// (.aas-code). The button is pinned to a non-scrolling host so it stays in
|
|
210
282
|
// place on long, horizontally-scrolling code.
|
|
211
283
|
(function mountCopyButtons() {
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
const DONE = koLang ? '복사됨' : 'Copied';
|
|
284
|
+
const LABEL = document.documentElement.dataset.copyLabel || 'Copy code';
|
|
285
|
+
const DONE = document.documentElement.dataset.copiedLabel || 'Copied';
|
|
215
286
|
const COPY_ICON =
|
|
216
287
|
'<svg class="aas-ico-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="13" height="13" x="9" y="9" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
217
288
|
const CHECK_ICON =
|
package/src/lib/dates.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { dateLocaleOf, type Lang } from '../i18n/ui';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Human-facing date in the reader's locale (e.g. "June 28, 2026" / "2026년 6월
|
|
@@ -7,7 +7,7 @@ import type { Lang } from '../i18n/ui';
|
|
|
7
7
|
* attribute on the surrounding `<time>` for machines.
|
|
8
8
|
*/
|
|
9
9
|
export function formatDate(d: Date, lang: Lang): string {
|
|
10
|
-
return new Intl.DateTimeFormat(lang
|
|
10
|
+
return new Intl.DateTimeFormat(dateLocaleOf(lang), {
|
|
11
11
|
year: 'numeric',
|
|
12
12
|
month: 'long',
|
|
13
13
|
day: 'numeric',
|
package/src/lib/facets.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pricingTags } from './pricing';
|
|
2
|
-
import {
|
|
2
|
+
import { pricingLabel, licenseLabel, useTranslations, type Lang } from '../i18n/ui';
|
|
3
3
|
import type { StackEntry } from './stacks';
|
|
4
4
|
|
|
5
5
|
export interface FacetOption {
|
|
@@ -39,7 +39,7 @@ export function deriveFacets(entries: StackEntry[], lang: Lang): Facet[] {
|
|
|
39
39
|
label: t('detail.pricing'),
|
|
40
40
|
options: pricingOrder
|
|
41
41
|
.filter((p) => usedPricing.has(p))
|
|
42
|
-
.map((p) => ({ value: p, label:
|
|
42
|
+
.map((p) => ({ value: p, label: pricingLabel(lang, p) })),
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
key: 'license',
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { languages, defaultLang, type Lang } from '../i18n/ui';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers for the single `src/pages/[...lang]/` route tree. Every page's
|
|
5
|
+
* getStaticPaths walks {@link allLocales} and emits one path per locale, using
|
|
6
|
+
* {@link langParam} for the `[...lang]` rest param: `undefined` for the default
|
|
7
|
+
* locale (so it renders at the site root) and the code otherwise (so it renders
|
|
8
|
+
* under `/<code>/`). Adding a locale is a site-config change, not a new file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** All locale codes the site ships, in switcher order (default first). */
|
|
12
|
+
export const allLocales = Object.keys(languages) as Lang[];
|
|
13
|
+
|
|
14
|
+
/** The `[...lang]` param for a locale: `undefined` for the default, else the code. */
|
|
15
|
+
export const langParam = (lang: Lang): string | undefined =>
|
|
16
|
+
lang === defaultLang ? undefined : lang;
|
package/src/lib/pages.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
2
|
+
import type { Lang } from '../i18n/ui';
|
|
3
|
+
|
|
4
|
+
export type PageEntry = CollectionEntry<'pages'>;
|
|
5
|
+
|
|
6
|
+
/** The url slug of a page, i.e. its id with the `<lang>/` prefix removed. */
|
|
7
|
+
export function pageSlugOf(entry: PageEntry): string {
|
|
8
|
+
return entry.id.replace(/^[a-z]{2}\//, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Standalone pages for one locale, in `order` (then title) order. */
|
|
12
|
+
export async function getPages(lang: Lang): Promise<PageEntry[]> {
|
|
13
|
+
const all = await getCollection('pages');
|
|
14
|
+
return all
|
|
15
|
+
.filter((e) => e.id.startsWith(`${lang}/`) && !e.data.draft)
|
|
16
|
+
.sort(
|
|
17
|
+
(a, b) => a.data.order - b.data.order || a.data.title.localeCompare(b.data.title),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The subset of {@link getPages} that opts into a header-nav link. */
|
|
22
|
+
export async function getNavPages(lang: Lang): Promise<PageEntry[]> {
|
|
23
|
+
return (await getPages(lang)).filter((e) => e.data.nav);
|
|
24
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { site } from '@aas-data/site';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional content sections a site can turn off. The core catalog (home, stack
|
|
5
|
+
* detail, categories, tags, vendors) is always on; these are opt-out. Disabling
|
|
6
|
+
* one removes both its routes and its header-nav item (routes are skipped in the
|
|
7
|
+
* integration, the nav link in BaseLayout).
|
|
8
|
+
*
|
|
9
|
+
* `pages` is the standalone-pages collection (About/소개, contact, …); turning it
|
|
10
|
+
* off drops every page and its nav item at once — finer control is per-page via
|
|
11
|
+
* the `nav` / `draft` frontmatter, or by not authoring the page.
|
|
12
|
+
*
|
|
13
|
+
* A site sets overrides in `src/data/site.ts` (`sections`), which astro.config
|
|
14
|
+
* also forwards to the theme integration for route filtering. Omitted = enabled.
|
|
15
|
+
*/
|
|
16
|
+
export type SectionKey = 'concepts' | 'articles' | 'samples' | 'slides' | 'glossary' | 'pages';
|
|
17
|
+
|
|
18
|
+
const DEFAULTS: Record<SectionKey, boolean> = {
|
|
19
|
+
concepts: true,
|
|
20
|
+
articles: true,
|
|
21
|
+
samples: true,
|
|
22
|
+
slides: true,
|
|
23
|
+
glossary: true,
|
|
24
|
+
pages: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Whether each optional section is enabled, after applying the site's overrides. */
|
|
28
|
+
export const sections: Record<SectionKey, boolean> = {
|
|
29
|
+
...DEFAULTS,
|
|
30
|
+
...((site as { sections?: Partial<Record<SectionKey, boolean>> }).sections ?? {}),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** True unless the site explicitly turned `key` off. */
|
|
34
|
+
export const sectionEnabled = (key: SectionKey): boolean => sections[key];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { site } from '@aas-data/site';
|
|
3
|
+
import type { GetStaticPaths } from 'astro';
|
|
4
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
5
|
+
import PageDetail from '../../components/PageDetail.astro';
|
|
6
|
+
import { getPages, pageSlugOf } from '../../lib/pages';
|
|
7
|
+
import { allLocales, langParam } from '../../lib/locales';
|
|
8
|
+
|
|
9
|
+
export const getStaticPaths = (async () => {
|
|
10
|
+
const paths: Awaited<ReturnType<GetStaticPaths>> = [];
|
|
11
|
+
for (const lang of allLocales) {
|
|
12
|
+
for (const entry of await getPages(lang)) {
|
|
13
|
+
paths.push({ params: { lang: langParam(lang), page: pageSlugOf(entry) }, props: { lang, entry } });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return paths;
|
|
17
|
+
}) satisfies GetStaticPaths;
|
|
18
|
+
|
|
19
|
+
const { lang, entry } = Astro.props;
|
|
20
|
+
const slug = pageSlugOf(entry);
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<BaseLayout
|
|
24
|
+
title={`${entry.data.title} — ${site.name}`}
|
|
25
|
+
description={entry.data.description}
|
|
26
|
+
lang={lang}
|
|
27
|
+
path={`${slug}/`}
|
|
28
|
+
>
|
|
29
|
+
<PageDetail entry={entry} lang={lang} />
|
|
30
|
+
</BaseLayout>
|