stack-site-builder 1.10.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/LICENSE +21 -0
- package/README.md +45 -0
- package/dev/local-samples-client.js +284 -0
- package/dev/local-samples.mjs +594 -0
- package/index.d.ts +12 -0
- package/index.mjs +124 -0
- package/markdown.mjs +394 -0
- package/package.json +52 -0
- package/src/components/ArrowUpRight.astro +24 -0
- package/src/components/ArticleCard.astro +56 -0
- package/src/components/ArticleDetail.astro +100 -0
- package/src/components/ArticleLink.astro +45 -0
- package/src/components/BackToTop.astro +47 -0
- package/src/components/BlogIndex.astro +137 -0
- package/src/components/Breadcrumb.astro +37 -0
- package/src/components/CategoryIndex.astro +115 -0
- package/src/components/ChevronDown.astro +23 -0
- package/src/components/CodeSamples.astro +141 -0
- package/src/components/CollapsibleGrid.astro +50 -0
- package/src/components/ConceptCard.astro +59 -0
- package/src/components/ConceptDetail.astro +102 -0
- package/src/components/ConceptIndex.astro +143 -0
- package/src/components/ConceptLink.astro +43 -0
- package/src/components/ConceptTools.astro +49 -0
- package/src/components/DeckView.astro +772 -0
- package/src/components/DetailTabs.astro +219 -0
- package/src/components/FilterControls.astro +407 -0
- package/src/components/Glossary.astro +264 -0
- package/src/components/Home.astro +90 -0
- package/src/components/LanguageSwitcher.astro +86 -0
- package/src/components/ListControls.astro +182 -0
- package/src/components/MermaidLoader.astro +96 -0
- package/src/components/PricingSection.astro +100 -0
- package/src/components/ProjectFileList.astro +68 -0
- package/src/components/ProjectFileTree.astro +95 -0
- package/src/components/ProjectRelatedTools.astro +70 -0
- package/src/components/ProjectViewer.astro +431 -0
- package/src/components/RelatedArticles.astro +52 -0
- package/src/components/RelatedConcepts.astro +60 -0
- package/src/components/RelatedTools.astro +55 -0
- package/src/components/SampleIndex.astro +464 -0
- package/src/components/SamplePage.astro +95 -0
- package/src/components/SampleProject.astro +56 -0
- package/src/components/Slide.astro +36 -0
- package/src/components/SlidesIndex.astro +60 -0
- package/src/components/StackCard.astro +183 -0
- package/src/components/StackDetail.astro +507 -0
- package/src/components/StarIcon.astro +21 -0
- package/src/components/TagIndex.astro +51 -0
- package/src/components/ThemeToggle.astro +106 -0
- package/src/components/Toc.astro +39 -0
- package/src/components/TocRail.astro +74 -0
- package/src/components/ToolAvatar.astro +133 -0
- package/src/components/ToolMeta.astro +95 -0
- package/src/components/VendorIndex.astro +51 -0
- package/src/content.ts +228 -0
- package/src/i18n/ui.ts +337 -0
- package/src/layouts/BaseLayout.astro +287 -0
- package/src/lib/articles.ts +27 -0
- package/src/lib/assets.ts +19 -0
- package/src/lib/category-tree.ts +64 -0
- package/src/lib/concepts.ts +35 -0
- package/src/lib/dates.ts +21 -0
- package/src/lib/facets.ts +57 -0
- package/src/lib/github.ts +186 -0
- package/src/lib/inline-md.ts +30 -0
- package/src/lib/pricing.ts +22 -0
- package/src/lib/project.ts +309 -0
- package/src/lib/samples.ts +88 -0
- package/src/lib/slides.ts +25 -0
- package/src/lib/stacks.ts +129 -0
- package/src/lib/toc-rail-client.ts +79 -0
- package/src/pages/article/[...id].astro +24 -0
- package/src/pages/article/category/[id].astro +17 -0
- package/src/pages/article/index.astro +12 -0
- package/src/pages/categories/[id].astro +25 -0
- package/src/pages/concept/[...id].astro +24 -0
- package/src/pages/concept/category/[id].astro +17 -0
- package/src/pages/concept/index.astro +12 -0
- package/src/pages/glossary.astro +12 -0
- package/src/pages/index.astro +9 -0
- package/src/pages/ko/article/[...id].astro +24 -0
- package/src/pages/ko/article/category/[id].astro +17 -0
- package/src/pages/ko/article/index.astro +12 -0
- package/src/pages/ko/categories/[id].astro +24 -0
- package/src/pages/ko/concept/[...id].astro +24 -0
- package/src/pages/ko/concept/category/[id].astro +17 -0
- package/src/pages/ko/concept/index.astro +12 -0
- package/src/pages/ko/glossary.astro +12 -0
- package/src/pages/ko/index.astro +9 -0
- package/src/pages/ko/sample/[folder].astro +16 -0
- package/src/pages/ko/sample/index.astro +12 -0
- package/src/pages/ko/slides/[deck].astro +14 -0
- package/src/pages/ko/slides/index.astro +12 -0
- package/src/pages/ko/stack/[...id].astro +51 -0
- package/src/pages/ko/tags/[tag].astro +18 -0
- package/src/pages/ko/vendors/[vendor].astro +18 -0
- package/src/pages/sample/[folder].astro +16 -0
- package/src/pages/sample/index.astro +12 -0
- package/src/pages/slides/[deck].astro +14 -0
- package/src/pages/slides/index.astro +12 -0
- package/src/pages/stack/[...id].astro +51 -0
- package/src/pages/tags/[tag].astro +18 -0
- package/src/pages/vendors/[vendor].astro +18 -0
- package/src/styles/global.css +2350 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { render } from 'astro:content';
|
|
3
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
4
|
+
import { getArticles, articleSlugOf, type ArticleEntry } from '../lib/articles';
|
|
5
|
+
import { getConcepts, type ConceptEntry } from '../lib/concepts';
|
|
6
|
+
import { Image } from 'astro:assets';
|
|
7
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
8
|
+
import { formatDate } from '../lib/dates';
|
|
9
|
+
import { inlineMd } from '../lib/inline-md';
|
|
10
|
+
import TocRail from './TocRail.astro';
|
|
11
|
+
import MermaidLoader from './MermaidLoader.astro';
|
|
12
|
+
import Breadcrumb from './Breadcrumb.astro';
|
|
13
|
+
import { articleTree, articleCatOf } from '@aas-data/article-categories';
|
|
14
|
+
import RelatedTools from './RelatedTools.astro';
|
|
15
|
+
import RelatedConcepts from './RelatedConcepts.astro';
|
|
16
|
+
import RelatedArticles from './RelatedArticles.astro';
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
entry: ArticleEntry;
|
|
20
|
+
lang: Lang;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { entry, lang } = Astro.props;
|
|
24
|
+
const t = useTranslations(lang);
|
|
25
|
+
const { Content, headings } = await render(entry);
|
|
26
|
+
const data = entry.data;
|
|
27
|
+
const dateStr = data.date.toISOString().slice(0, 10);
|
|
28
|
+
const updatedStr = data.updated?.toISOString().slice(0, 10);
|
|
29
|
+
const tocItems = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
|
|
30
|
+
// Articles may embed Mermaid diagrams (and a <SampleProject/> whose README can
|
|
31
|
+
// too); load the client renderer when the body uses any.
|
|
32
|
+
const bodyHasMermaid = (entry.body ?? '').includes('```mermaid');
|
|
33
|
+
|
|
34
|
+
// Related concepts: concepts whose frontmatter lists this article (backlink).
|
|
35
|
+
const articleSlug = articleSlugOf(entry);
|
|
36
|
+
const relatedConcepts = (await getConcepts(lang)).filter((c) =>
|
|
37
|
+
c.data.articles.includes(articleSlug),
|
|
38
|
+
) as ConceptEntry[];
|
|
39
|
+
|
|
40
|
+
// Related writing: articles this one explicitly cross-links (frontmatter
|
|
41
|
+
// `related`), kept in the authored order.
|
|
42
|
+
const articlesBySlug = new Map((await getArticles(lang)).map((a) => [articleSlugOf(a), a]));
|
|
43
|
+
const relatedArticles = (data.related ?? [])
|
|
44
|
+
.map((s) => articlesBySlug.get(s))
|
|
45
|
+
.filter((a): a is ArticleEntry => Boolean(a) && articleSlugOf(a!) !== articleSlug);
|
|
46
|
+
|
|
47
|
+
// A referenced sample (frontmatter `project`) is no longer embedded here — the
|
|
48
|
+
// body's <SampleProject/> renders a callout card to its sample/<folder>/ page,
|
|
49
|
+
// which carries the viewer, the file rail, and the related tools itself.
|
|
50
|
+
|
|
51
|
+
// Breadcrumb: All writing › category › subcategory… (each segment linked).
|
|
52
|
+
const crumbs = [
|
|
53
|
+
{ label: t('article.backToBlog'), href: getRelativeLocaleUrl(lang, 'article/') },
|
|
54
|
+
...articleTree.pathOf(articleCatOf(data.category)).map((c) => ({
|
|
55
|
+
label: c.label[lang],
|
|
56
|
+
href: getRelativeLocaleUrl(lang, `article/category/${c.id}/`),
|
|
57
|
+
})),
|
|
58
|
+
];
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
<Breadcrumb segments={crumbs} />
|
|
62
|
+
|
|
63
|
+
<header class="mt-4 pb-6">
|
|
64
|
+
<p class="text-sm text-[var(--aas-muted)]">
|
|
65
|
+
<time datetime={dateStr}>{formatDate(data.date, lang)}</time>
|
|
66
|
+
{data.version && <span> · {t('meta.docVersion')} {data.version}</span>}
|
|
67
|
+
{data.updated && updatedStr !== dateStr && (
|
|
68
|
+
<span>
|
|
69
|
+
{' '}
|
|
70
|
+
· {t('meta.updated')} <time datetime={updatedStr}>{formatDate(data.updated, lang)}</time>
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
</p>
|
|
74
|
+
<h1 class="mt-1 text-3xl font-bold tracking-tight">{data.title}</h1>
|
|
75
|
+
<p class="mt-2 aas-md text-lg whitespace-pre-line text-[var(--aas-muted)]" set:html={inlineMd(data.description)} />
|
|
76
|
+
{
|
|
77
|
+
data.image && (
|
|
78
|
+
<Image
|
|
79
|
+
src={data.image}
|
|
80
|
+
alt={data.imageAlt ?? data.title}
|
|
81
|
+
class="mt-5 aspect-[2/1] w-full rounded-2xl border border-[var(--aas-border)] object-cover"
|
|
82
|
+
/>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
</header>
|
|
86
|
+
|
|
87
|
+
<RelatedConcepts lang={lang} concepts={relatedConcepts} />
|
|
88
|
+
|
|
89
|
+
<div class="mt-8 flex items-start gap-8">
|
|
90
|
+
<article class="prose max-w-none aas-reading min-w-0 flex-1">
|
|
91
|
+
<Content />
|
|
92
|
+
</article>
|
|
93
|
+
<TocRail lang={lang} items={tocItems} />
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<RelatedTools lang={lang} slugs={data.tools} />
|
|
97
|
+
|
|
98
|
+
<RelatedArticles lang={lang} articles={relatedArticles} />
|
|
99
|
+
|
|
100
|
+
{bodyHasMermaid && <MermaidLoader />}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Inline, full-width callout linking to a blog article, for use inside a
|
|
3
|
+
// concept's MDX body, e.g.
|
|
4
|
+
// <ArticleLink slug="web-search-fx-agent" />
|
|
5
|
+
// Deliberately distinct from <ConceptLink/> (a filled accent tint, "learn
|
|
6
|
+
// more"): this is a plain panel with an accent left-bar, a "related writing"
|
|
7
|
+
// label, and the post's date — signalling a read, not another concept.
|
|
8
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
9
|
+
import { getArticles, articleSlugOf, type ArticleEntry } from '../lib/articles';
|
|
10
|
+
import { formatDate } from '../lib/dates';
|
|
11
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
|
+
import { inlineMd } from '../lib/inline-md';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
slug: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { slug } = Astro.props;
|
|
19
|
+
const lang: Lang = Astro.currentLocale === 'ko' ? 'ko' : 'en';
|
|
20
|
+
const t = useTranslations(lang);
|
|
21
|
+
const articles = await getArticles(lang);
|
|
22
|
+
const target = articles.find((a) => articleSlugOf(a) === slug) as ArticleEntry | undefined;
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
target && (
|
|
27
|
+
<a
|
|
28
|
+
href={getRelativeLocaleUrl(lang, `article/${articleSlugOf(target)}/`)}
|
|
29
|
+
class="aas-lift mt-3 flex items-start gap-4 rounded-xl border border-l-4 p-4 no-underline"
|
|
30
|
+
style="border-color: var(--aas-border); border-left-color: var(--aas-accent); background: var(--aas-panel);"
|
|
31
|
+
>
|
|
32
|
+
<div class="min-w-0 flex-1">
|
|
33
|
+
<span class="block text-xs font-semibold tracking-wide text-[var(--aas-accent)] uppercase">
|
|
34
|
+
{t('detail.relatedWriting')}
|
|
35
|
+
</span>
|
|
36
|
+
<span class="mt-0.5 block font-semibold text-[var(--aas-text)]">{target.data.title}</span>
|
|
37
|
+
<span class="aas-md mt-1 block text-sm text-[var(--aas-muted)]" set:html={inlineMd(target.data.description)} />
|
|
38
|
+
<span class="mt-1.5 block text-xs text-[var(--aas-muted)]">
|
|
39
|
+
{formatDate(target.data.date, lang)}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
<span aria-hidden="true" class="shrink-0 text-xl text-[var(--aas-accent)]">→</span>
|
|
43
|
+
</a>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Floating "back to top" button. Mobile only — on wide viewports (≥1200px) the
|
|
3
|
+
// sticky TOC rail already gives a way to jump around, so it is hidden there via
|
|
4
|
+
// the .aas-totop rule in global.css. Appears once the page is scrolled down.
|
|
5
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
lang: Lang;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { lang } = Astro.props;
|
|
12
|
+
const t = useTranslations(lang);
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
data-totop
|
|
18
|
+
aria-label={t('nav.backToTop')}
|
|
19
|
+
class="aas-totop fixed right-5 bottom-5 z-40 h-11 w-11 items-center justify-center rounded-full border border-[var(--aas-border)] bg-[var(--aas-panel)] text-[var(--aas-text)] shadow-lg transition hover:text-[var(--aas-accent)]"
|
|
20
|
+
>
|
|
21
|
+
<svg
|
|
22
|
+
width="20"
|
|
23
|
+
height="20"
|
|
24
|
+
viewBox="0 0 24 24"
|
|
25
|
+
fill="none"
|
|
26
|
+
stroke="currentColor"
|
|
27
|
+
stroke-width="2"
|
|
28
|
+
stroke-linecap="round"
|
|
29
|
+
stroke-linejoin="round"
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
>
|
|
32
|
+
<path d="m18 15-6-6-6 6" />
|
|
33
|
+
</svg>
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
<script>
|
|
37
|
+
const btn = document.querySelector('[data-totop]');
|
|
38
|
+
if (btn) {
|
|
39
|
+
const onScroll = () => {
|
|
40
|
+
if (window.scrollY > 400) btn.setAttribute('data-visible', '');
|
|
41
|
+
else btn.removeAttribute('data-visible');
|
|
42
|
+
};
|
|
43
|
+
onScroll();
|
|
44
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
45
|
+
btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
|
+
import ArticleCard from './ArticleCard.astro';
|
|
4
|
+
import Breadcrumb from './Breadcrumb.astro';
|
|
5
|
+
import ListControls from './ListControls.astro';
|
|
6
|
+
import ArrowUpRight from './ArrowUpRight.astro';
|
|
7
|
+
|
|
8
|
+
const PAGE = 12;
|
|
9
|
+
import { articleTree, articleCatOf } from '@aas-data/article-categories';
|
|
10
|
+
import { getArticles, articleSlugOf, type ArticleEntry } from '../lib/articles';
|
|
11
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
|
+
|
|
13
|
+
// Doubles as the writing index (no `categoryId` → all articles, grouped by
|
|
14
|
+
// category) and a category browse page (`categoryId` → that category, with a
|
|
15
|
+
// breadcrumb, its direct articles, and a section per subcategory).
|
|
16
|
+
interface Props {
|
|
17
|
+
lang: Lang;
|
|
18
|
+
categoryId?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { lang, categoryId } = Astro.props;
|
|
22
|
+
const t = useTranslations(lang);
|
|
23
|
+
const all = await getArticles(lang);
|
|
24
|
+
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `article/category/${cid}/`);
|
|
25
|
+
|
|
26
|
+
const itemsIn = (cid: string): ArticleEntry[] => {
|
|
27
|
+
const ids = new Set(articleTree.descendantIds(cid));
|
|
28
|
+
return all.filter((a) => ids.has(articleCatOf(a.data.category)));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const node = categoryId ? articleTree.map.get(categoryId) : undefined;
|
|
32
|
+
const sections = node ? articleTree.childrenOf(node.id) : articleTree.roots;
|
|
33
|
+
const direct = node ? all.filter((a) => articleCatOf(a.data.category) === node.id) : [];
|
|
34
|
+
|
|
35
|
+
// A leaf category page shows one flat list — that grid gets ?page= pagination.
|
|
36
|
+
// Index and parent-category pages stay grouped overviews: each section is
|
|
37
|
+
// capped to PAGE cards with a "see all" link to the fuller list.
|
|
38
|
+
const isLeaf = !!node && sections.length === 0;
|
|
39
|
+
|
|
40
|
+
const path = node ? articleTree.pathOf(node.id) : [];
|
|
41
|
+
const crumbs = node
|
|
42
|
+
? [
|
|
43
|
+
{ label: t('article.backToBlog'), href: getRelativeLocaleUrl(lang, 'article/') },
|
|
44
|
+
...path.map((c, i) => ({
|
|
45
|
+
label: c.label[lang],
|
|
46
|
+
href: i === path.length - 1 ? undefined : catUrl(c.id),
|
|
47
|
+
})),
|
|
48
|
+
]
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
const card = (a: ArticleEntry) => ({
|
|
52
|
+
lang,
|
|
53
|
+
slug: articleSlugOf(a),
|
|
54
|
+
title: a.data.title,
|
|
55
|
+
description: a.data.description,
|
|
56
|
+
date: a.data.date,
|
|
57
|
+
version: a.data.version,
|
|
58
|
+
tags: a.data.tags,
|
|
59
|
+
image: a.data.image,
|
|
60
|
+
imageAlt: a.data.imageAlt,
|
|
61
|
+
});
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
node ? (
|
|
66
|
+
<>
|
|
67
|
+
<Breadcrumb segments={crumbs} />
|
|
68
|
+
<header class="mt-4">
|
|
69
|
+
<h1 class="text-3xl font-bold tracking-tight">{node.label[lang]}</h1>
|
|
70
|
+
<p class="mt-2 max-w-2xl text-lg text-[var(--aas-muted)]">
|
|
71
|
+
{node.detail?.[lang] ?? node.description[lang]}
|
|
72
|
+
</p>
|
|
73
|
+
</header>
|
|
74
|
+
</>
|
|
75
|
+
) : (
|
|
76
|
+
<section class="py-6">
|
|
77
|
+
<h1 class="text-4xl font-bold tracking-tight">{t('blog.title')}</h1>
|
|
78
|
+
<p class="mt-3 max-w-2xl text-lg text-[var(--aas-muted)]">{t('blog.tagline')}</p>
|
|
79
|
+
</section>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
{all.length === 0 && <p class="mt-8 text-[var(--aas-muted)]">{t('blog.empty')}</p>}
|
|
84
|
+
|
|
85
|
+
{all.length > 0 && <div class="mt-6"><ListControls lang={lang} /></div>}
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
direct.length > 0 && (
|
|
89
|
+
<div
|
|
90
|
+
class="mt-4 grid gap-5 sm:grid-cols-2"
|
|
91
|
+
data-sortgrid
|
|
92
|
+
data-paginate={isLeaf ? true : undefined}
|
|
93
|
+
data-page-size={isLeaf ? PAGE : undefined}
|
|
94
|
+
>
|
|
95
|
+
{direct.map((a) => (
|
|
96
|
+
<ArticleCard {...card(a)} />
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
sections.map((cat) => {
|
|
104
|
+
const items = itemsIn(cat.id);
|
|
105
|
+
return (
|
|
106
|
+
items.length > 0 && (
|
|
107
|
+
<section class="mt-8">
|
|
108
|
+
<h2 class="text-2xl font-semibold">
|
|
109
|
+
<a
|
|
110
|
+
href={catUrl(cat.id)}
|
|
111
|
+
class="text-[var(--aas-text)] no-underline hover:text-[var(--aas-accent)]"
|
|
112
|
+
>
|
|
113
|
+
{cat.label[lang]}
|
|
114
|
+
<ArrowUpRight size={20} class="ml-1 text-[var(--aas-muted)]" />
|
|
115
|
+
</a>
|
|
116
|
+
</h2>
|
|
117
|
+
<p class="mt-1 text-sm text-[var(--aas-muted)]">{cat.description[lang]}</p>
|
|
118
|
+
<div class="mt-4 grid gap-5 sm:grid-cols-2" data-sortgrid>
|
|
119
|
+
{items.slice(0, PAGE).map((a) => (
|
|
120
|
+
<ArticleCard {...card(a)} />
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
{items.length > PAGE && (
|
|
124
|
+
<a
|
|
125
|
+
href={catUrl(cat.id)}
|
|
126
|
+
class="mt-3 inline-block text-sm font-medium text-[var(--aas-accent)] no-underline"
|
|
127
|
+
>
|
|
128
|
+
{t('list.seeAll').replace('{n}', String(items.length))} →
|
|
129
|
+
</a>
|
|
130
|
+
)}
|
|
131
|
+
</section>
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
{isLeaf && <nav data-pager aria-label="Pagination" class="mt-8 flex flex-wrap items-center justify-center gap-1.5" />}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
// A simple breadcrumb trail. Segments with an `href` render as links; the
|
|
3
|
+
// last (current) segment is typically passed without one.
|
|
4
|
+
interface Segment {
|
|
5
|
+
label: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
segments: Segment[];
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
const { segments, class: className } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<nav
|
|
16
|
+
aria-label="Breadcrumb"
|
|
17
|
+
class:list={['flex flex-wrap items-center gap-1.5 text-sm text-[var(--aas-muted)]', className]}
|
|
18
|
+
>
|
|
19
|
+
{
|
|
20
|
+
segments.map((s, i) => (
|
|
21
|
+
<Fragment>
|
|
22
|
+
{i > 0 && (
|
|
23
|
+
<span aria-hidden="true" class="opacity-60">
|
|
24
|
+
›
|
|
25
|
+
</span>
|
|
26
|
+
)}
|
|
27
|
+
{s.href ? (
|
|
28
|
+
<a href={s.href} class="text-[var(--aas-accent)] no-underline hover:underline">
|
|
29
|
+
{s.label}
|
|
30
|
+
</a>
|
|
31
|
+
) : (
|
|
32
|
+
<span class="text-[var(--aas-text)]">{s.label}</span>
|
|
33
|
+
)}
|
|
34
|
+
</Fragment>
|
|
35
|
+
))
|
|
36
|
+
}
|
|
37
|
+
</nav>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
|
+
import StackCard from './StackCard.astro';
|
|
4
|
+
import Breadcrumb from './Breadcrumb.astro';
|
|
5
|
+
import FilterControls from './FilterControls.astro';
|
|
6
|
+
import ArrowUpRight from './ArrowUpRight.astro';
|
|
7
|
+
import { categoryMap, childrenOf, pathOf } from '@aas-data/categories';
|
|
8
|
+
import { getStacks, getStacksByCategory, slugOf, type StackEntry } from '../lib/stacks';
|
|
9
|
+
import { deriveFacets } from '../lib/facets';
|
|
10
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
lang: Lang;
|
|
14
|
+
id: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { lang, id } = Astro.props;
|
|
18
|
+
const t = useTranslations(lang);
|
|
19
|
+
const node = categoryMap.get(id)!;
|
|
20
|
+
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `categories/${cid}/`);
|
|
21
|
+
|
|
22
|
+
// Breadcrumb: All tools › ancestors… › current (current is not a link).
|
|
23
|
+
const path = pathOf(id);
|
|
24
|
+
const crumbs = [
|
|
25
|
+
{ label: t('detail.allTools'), href: getRelativeLocaleUrl(lang, '') },
|
|
26
|
+
...path.map((c, i) => ({
|
|
27
|
+
label: c.label[lang],
|
|
28
|
+
href: i === path.length - 1 ? undefined : catUrl(c.id),
|
|
29
|
+
})),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const children = childrenOf(id);
|
|
33
|
+
// Stacks attached directly to this node (vs. nested under a subcategory).
|
|
34
|
+
const direct = (await getStacksByCategory(lang, id)).filter((s) => s.data.category === id);
|
|
35
|
+
// Each child subcategory with its own rolled-up stacks.
|
|
36
|
+
const childGroups = await Promise.all(
|
|
37
|
+
children.map(async (c) => ({ cat: c, items: await getStacksByCategory(lang, c.id) })),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Filter facets are built from the WHOLE catalog (not just this page's tools) so
|
|
41
|
+
// they're identical on every page — a sort/filter carried in from elsewhere can
|
|
42
|
+
// always be restored here, even if no tool in this category has that value.
|
|
43
|
+
const facets = deriveFacets(await getStacks(lang), lang);
|
|
44
|
+
|
|
45
|
+
const cardProps = (e: StackEntry) => ({
|
|
46
|
+
slug: slugOf(e),
|
|
47
|
+
lang,
|
|
48
|
+
name: e.data.name,
|
|
49
|
+
formerNames: e.data.formerNames,
|
|
50
|
+
vendor: e.data.vendor,
|
|
51
|
+
description: e.data.description,
|
|
52
|
+
tags: e.data.tags,
|
|
53
|
+
pricing: e.data.pricing,
|
|
54
|
+
license: e.data.license,
|
|
55
|
+
language: e.data.language,
|
|
56
|
+
category: e.data.category,
|
|
57
|
+
logo: e.data.logo,
|
|
58
|
+
logoDark: e.data.logoDark,
|
|
59
|
+
repo: e.data.repo,
|
|
60
|
+
version: e.data.version,
|
|
61
|
+
deprecated: e.data.deprecated,
|
|
62
|
+
});
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
<section class="py-6">
|
|
66
|
+
<Breadcrumb segments={crumbs} />
|
|
67
|
+
<h1 class="mt-3 text-3xl font-bold tracking-tight">{node.label[lang]}</h1>
|
|
68
|
+
<p class="mt-3 max-w-3xl leading-relaxed text-[var(--aas-muted)]">
|
|
69
|
+
{node.detail?.[lang] ?? node.description[lang]}
|
|
70
|
+
</p>
|
|
71
|
+
</section>
|
|
72
|
+
|
|
73
|
+
{/* Search · filter · sort controls (client-side, URL-synced) */}
|
|
74
|
+
<div class="mb-8">
|
|
75
|
+
<FilterControls lang={lang} facets={facets} />
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Tools sitting directly on this node (no subcategory). */}
|
|
79
|
+
{
|
|
80
|
+
direct.length > 0 && (
|
|
81
|
+
<div data-section>
|
|
82
|
+
<div class="grid gap-4 sm:grid-cols-2" data-grid>
|
|
83
|
+
{direct.map((e) => (
|
|
84
|
+
<StackCard {...cardProps(e)} />
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
{/* One section per subcategory, each rolling up its own subtree. */}
|
|
92
|
+
<div class="mt-8 space-y-12">
|
|
93
|
+
{
|
|
94
|
+
childGroups
|
|
95
|
+
.filter((g) => g.items.length > 0)
|
|
96
|
+
.map((g) => (
|
|
97
|
+
<div data-section>
|
|
98
|
+
<div class="pb-1">
|
|
99
|
+
<h2 class="text-2xl font-semibold">
|
|
100
|
+
<a href={catUrl(g.cat.id)} data-keep-filters class="text-[var(--aas-text)] no-underline hover:text-[var(--aas-accent)]">
|
|
101
|
+
{g.cat.label[lang]}
|
|
102
|
+
<ArrowUpRight size={20} class="ml-1 text-[var(--aas-muted)]" />
|
|
103
|
+
</a>
|
|
104
|
+
</h2>
|
|
105
|
+
<p class="mt-1 text-sm text-[var(--aas-muted)]">{g.cat.description[lang]}</p>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="mt-5 grid gap-4 sm:grid-cols-2" data-grid>
|
|
108
|
+
{g.items.map((e) => (
|
|
109
|
+
<StackCard {...cardProps(e)} />
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
))
|
|
114
|
+
}
|
|
115
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
size?: number;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { size = 16, class: className } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<svg
|
|
11
|
+
width={size}
|
|
12
|
+
height={size}
|
|
13
|
+
viewBox="0 0 24 24"
|
|
14
|
+
fill="none"
|
|
15
|
+
stroke="currentColor"
|
|
16
|
+
stroke-width="2"
|
|
17
|
+
stroke-linecap="round"
|
|
18
|
+
stroke-linejoin="round"
|
|
19
|
+
aria-hidden="true"
|
|
20
|
+
class:list={['shrink-0', className]}
|
|
21
|
+
>
|
|
22
|
+
<path d="m6 9 6 6 6-6"></path>
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
3
|
+
import ChevronDown from './ChevronDown.astro';
|
|
4
|
+
import type { RenderedSample } from '../lib/samples';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
samples: RenderedSample[];
|
|
8
|
+
lang: Lang;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { samples, lang } = Astro.props;
|
|
12
|
+
const t = useTranslations(lang);
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div class="aas-samples" data-samples>
|
|
16
|
+
<div class="aas-sample-picker flex items-center gap-2 text-sm text-[var(--aas-muted)]">
|
|
17
|
+
{t('sample.example')}
|
|
18
|
+
<details class="aas-vdd" data-samples-dd>
|
|
19
|
+
<summary
|
|
20
|
+
class="flex cursor-pointer list-none items-center gap-2 rounded-lg border border-[var(--aas-border)] bg-[var(--aas-panel)] px-3 py-1.5 text-[var(--aas-text)] select-none"
|
|
21
|
+
>
|
|
22
|
+
<span data-sample-current>{samples[0]?.version}</span>
|
|
23
|
+
<ChevronDown class="text-[var(--aas-muted)]" />
|
|
24
|
+
</summary>
|
|
25
|
+
<ul
|
|
26
|
+
class="absolute left-0 z-10 mt-1 min-w-[13rem] overflow-hidden rounded-lg border border-[var(--aas-border)] bg-[var(--aas-panel)] py-1 shadow-lg"
|
|
27
|
+
>
|
|
28
|
+
{
|
|
29
|
+
samples.map((s, i) => (
|
|
30
|
+
<li>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
data-sample-option={i}
|
|
34
|
+
data-sample-version={s.version}
|
|
35
|
+
aria-current={i === 0 ? 'true' : 'false'}
|
|
36
|
+
class="flex w-full items-center justify-between gap-2 px-3 py-1.5 text-left text-[var(--aas-muted)] hover:bg-[var(--aas-tint)] aria-[current=true]:font-semibold aria-[current=true]:text-[var(--aas-text)]"
|
|
37
|
+
>
|
|
38
|
+
{s.version}
|
|
39
|
+
<span data-check aria-hidden="true" class="opacity-0">
|
|
40
|
+
✓
|
|
41
|
+
</span>
|
|
42
|
+
</button>
|
|
43
|
+
</li>
|
|
44
|
+
))
|
|
45
|
+
}
|
|
46
|
+
</ul>
|
|
47
|
+
</details>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="mt-4 space-y-4">
|
|
51
|
+
{
|
|
52
|
+
samples.map((s, i) => (
|
|
53
|
+
<div data-sample-panel={i} class:list={[i === 0 ? '' : 'hidden']}>
|
|
54
|
+
{s.descHtml && <div class="prose mb-4 max-w-none" set:html={s.descHtml} />}
|
|
55
|
+
{s.diagram && (
|
|
56
|
+
<figure class="mb-4">
|
|
57
|
+
<figcaption class="mb-1 text-xs tracking-wide text-[var(--aas-muted)] uppercase">
|
|
58
|
+
{t('sample.diagram')}
|
|
59
|
+
</figcaption>
|
|
60
|
+
<div class="aas-diagram">
|
|
61
|
+
<pre class="mermaid">{s.diagram}</pre>
|
|
62
|
+
</div>
|
|
63
|
+
</figure>
|
|
64
|
+
)}
|
|
65
|
+
<div class="aas-code overflow-x-auto rounded-lg border border-[var(--aas-border)]" set:html={s.codeHtml} />
|
|
66
|
+
{s.note && <p class="mt-2 text-sm text-[var(--aas-muted)]">{s.note}</p>}
|
|
67
|
+
</div>
|
|
68
|
+
))
|
|
69
|
+
}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<style>
|
|
74
|
+
.aas-vdd {
|
|
75
|
+
position: relative;
|
|
76
|
+
}
|
|
77
|
+
.aas-vdd > summary::-webkit-details-marker {
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
.aas-vdd [data-sample-option][aria-current='true'] [data-check] {
|
|
81
|
+
opacity: 1;
|
|
82
|
+
}
|
|
83
|
+
/* Shiki emits its own background; pad it and keep corners rounded. */
|
|
84
|
+
.aas-code :global(pre.shiki) {
|
|
85
|
+
margin: 0;
|
|
86
|
+
padding: 1rem 1.1rem;
|
|
87
|
+
border-radius: 0.5rem;
|
|
88
|
+
overflow-x: auto;
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
91
|
+
|
|
92
|
+
<script>
|
|
93
|
+
document.querySelectorAll<HTMLElement>('[data-samples]').forEach((root) => {
|
|
94
|
+
const dd = root.querySelector<HTMLDetailsElement>('[data-samples-dd]');
|
|
95
|
+
const current = root.querySelector<HTMLElement>('[data-sample-current]');
|
|
96
|
+
const options = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-sample-option]'));
|
|
97
|
+
const panels = Array.from(root.querySelectorAll<HTMLElement>('[data-sample-panel]'));
|
|
98
|
+
|
|
99
|
+
// The selected example lives in ?v=. The default (index 0) drops the param
|
|
100
|
+
// so there's one canonical URL per view — mirroring how DetailTabs drops
|
|
101
|
+
// ?tab for the default Overview tab.
|
|
102
|
+
const V_PARAM = 'v';
|
|
103
|
+
function writeV(index: string) {
|
|
104
|
+
const url = new URL(location.href);
|
|
105
|
+
if (index === '0') url.searchParams.delete(V_PARAM);
|
|
106
|
+
else url.searchParams.set(V_PARAM, index);
|
|
107
|
+
history.replaceState(null, '', url.pathname + url.search + url.hash);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function apply(index: string, write: boolean) {
|
|
111
|
+
panels.forEach((p) => p.classList.toggle('hidden', p.dataset.samplePanel !== index));
|
|
112
|
+
options.forEach((o) =>
|
|
113
|
+
o.setAttribute('aria-current', String(o.dataset.sampleOption === index)),
|
|
114
|
+
);
|
|
115
|
+
const opt = options.find((o) => o.dataset.sampleOption === index);
|
|
116
|
+
if (current && opt) current.textContent = opt.dataset.sampleVersion ?? '';
|
|
117
|
+
dd?.removeAttribute('open');
|
|
118
|
+
(window as unknown as { __aasRenderMermaid?: () => void }).__aasRenderMermaid?.();
|
|
119
|
+
document.dispatchEvent(new CustomEvent('aas:sample', { detail: { index: Number(index) } }));
|
|
120
|
+
if (write) writeV(index);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
options.forEach((o) => o.addEventListener('click', () => apply(o.dataset.sampleOption!, true)));
|
|
124
|
+
|
|
125
|
+
// External request (e.g. a #s<n>-... deep link from DetailTabs).
|
|
126
|
+
document.addEventListener('aas:select-sample', (e) =>
|
|
127
|
+
apply(String((e as CustomEvent<{ index: number }>).detail.index), true),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Restore the version from ?v= on load.
|
|
131
|
+
const v = new URLSearchParams(location.search).get(V_PARAM);
|
|
132
|
+
if (v && options.some((o) => o.dataset.sampleOption === v)) apply(v, false);
|
|
133
|
+
|
|
134
|
+
// Reflect the current version when the code tab becomes active (0 → drop ?v).
|
|
135
|
+
document.addEventListener('aas:tabchange', (e) => {
|
|
136
|
+
if ((e as CustomEvent<{ id: string }>).detail.id !== 'code') return;
|
|
137
|
+
const cur = options.find((o) => o.getAttribute('aria-current') === 'true');
|
|
138
|
+
if (cur) writeV(cur.dataset.sampleOption!);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
</script>
|