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,507 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { render } from 'astro:content';
|
|
3
|
+
import { Image } from 'astro:assets';
|
|
4
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
5
|
+
import { pathOf } from '@aas-data/categories';
|
|
6
|
+
import { pricingLabels, useTranslations, type Lang } from '../i18n/ui';
|
|
7
|
+
import { pricingTags, COMPLETELY_FREE } from '../lib/pricing';
|
|
8
|
+
import StarIcon from './StarIcon.astro';
|
|
9
|
+
import { slugOf, slugifyName, toolResolver, type StackEntry } from '../lib/stacks';
|
|
10
|
+
import { isImageRef, resolveImageUrl } from '../lib/assets';
|
|
11
|
+
import { inlineMd } from '../lib/inline-md';
|
|
12
|
+
import { getArticlesForTool, articleSlugOf } from '../lib/articles';
|
|
13
|
+
import { getConceptsForTool, conceptSlugOf } from '../lib/concepts';
|
|
14
|
+
import ToolAvatar from './ToolAvatar.astro';
|
|
15
|
+
import ToolMeta from './ToolMeta.astro';
|
|
16
|
+
import Breadcrumb from './Breadcrumb.astro';
|
|
17
|
+
import PricingSection from './PricingSection.astro';
|
|
18
|
+
import DetailTabs from './DetailTabs.astro';
|
|
19
|
+
import CodeSamples from './CodeSamples.astro';
|
|
20
|
+
import MermaidLoader from './MermaidLoader.astro';
|
|
21
|
+
import RelatedTools from './RelatedTools.astro';
|
|
22
|
+
import CollapsibleGrid from './CollapsibleGrid.astro';
|
|
23
|
+
import ProjectViewer from './ProjectViewer.astro';
|
|
24
|
+
import ProjectRelatedTools from './ProjectRelatedTools.astro';
|
|
25
|
+
import ProjectFileList from './ProjectFileList.astro';
|
|
26
|
+
import Toc from './Toc.astro';
|
|
27
|
+
import { getRepoStats, stalenessOf, formatStars } from '../lib/github';
|
|
28
|
+
import { formatDate, isoDate } from '../lib/dates';
|
|
29
|
+
import { renderSamples } from '../lib/samples';
|
|
30
|
+
import { renderProjects } from '../lib/project';
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
entry: StackEntry;
|
|
34
|
+
lang: Lang;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { entry, lang } = Astro.props;
|
|
38
|
+
const t = useTranslations(lang);
|
|
39
|
+
const { Content, headings } = await render(entry);
|
|
40
|
+
const data = entry.data;
|
|
41
|
+
// The overview body can contain a Mermaid diagram even with no samples/projects
|
|
42
|
+
// (and thus no tabs), so the loader must not be gated on the tabs alone.
|
|
43
|
+
const overviewHasMermaid = (entry.body ?? '').includes('```mermaid');
|
|
44
|
+
// Breadcrumb: All tools › category › subcategory… (each segment linked).
|
|
45
|
+
const crumbs = [
|
|
46
|
+
{ label: t('detail.allTools'), href: getRelativeLocaleUrl(lang, '') },
|
|
47
|
+
...pathOf(data.category).map((c) => ({
|
|
48
|
+
label: c.label[lang],
|
|
49
|
+
href: getRelativeLocaleUrl(lang, `categories/${c.id}/`),
|
|
50
|
+
})),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Right-rail table of contents: body headings, h2–h3 only.
|
|
54
|
+
const tocItems = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
|
|
55
|
+
|
|
56
|
+
// Backlinks: writing that references this tool, and concepts that use it.
|
|
57
|
+
const relatedArticles = await getArticlesForTool(lang, slugOf(entry));
|
|
58
|
+
const relatedConcepts = await getConceptsForTool(lang, slugOf(entry));
|
|
59
|
+
|
|
60
|
+
// Live GitHub stats (build-time), with the manual `version` as fallback.
|
|
61
|
+
const stats = await getRepoStats(data.repo);
|
|
62
|
+
const displayVersion = stats?.version ?? data.version;
|
|
63
|
+
|
|
64
|
+
// Resolved logo URL for the hero card's blurred ambient backdrop (image logos
|
|
65
|
+
// only; monograms/emoji have nothing to blur).
|
|
66
|
+
const logoUrl = isImageRef(data.logo) ? resolveImageUrl(data.logo) : undefined;
|
|
67
|
+
// Vendor link slug; empty (e.g. a non-Latin vendor name) → plain text, no link.
|
|
68
|
+
const vendorSlug = data.vendor ? slugifyName(data.vendor) : '';
|
|
69
|
+
// Warn when an open-source project's latest release is getting old (6m / 1y).
|
|
70
|
+
const staleness = data.pricing.includes('open-source') ? stalenessOf(stats?.releasedAt) : 'fresh';
|
|
71
|
+
|
|
72
|
+
const links = [
|
|
73
|
+
data.website && { label: t('detail.website'), href: data.website },
|
|
74
|
+
data.repo && { label: t('detail.repository'), href: data.repo },
|
|
75
|
+
data.docs && { label: t('detail.docs'), href: data.docs },
|
|
76
|
+
].filter(Boolean) as { label: string; href: string }[];
|
|
77
|
+
|
|
78
|
+
const hasSamples = data.samples.length > 0;
|
|
79
|
+
const renderedSamples = hasSamples ? await renderSamples(data.samples) : [];
|
|
80
|
+
|
|
81
|
+
// Real runnable mini-projects (samples/<folder>/) listed in frontmatter. Each
|
|
82
|
+
// entry is a bare folder name or { folder, related: [slugs] }; normalize both.
|
|
83
|
+
const projectSpecs = data.projects.map((p) =>
|
|
84
|
+
typeof p === 'string' ? { folder: p, related: [] as string[] } : { folder: p.folder, related: p.related },
|
|
85
|
+
);
|
|
86
|
+
const projects = projectSpecs.length
|
|
87
|
+
? await renderProjects(projectSpecs.map((p) => p.folder), lang)
|
|
88
|
+
: [];
|
|
89
|
+
const hasProject = projects.length > 0;
|
|
90
|
+
const hasPricing = Boolean(data.pricingNote || data.pricingTiers.length);
|
|
91
|
+
const hasTabs = hasSamples || hasProject || hasPricing;
|
|
92
|
+
|
|
93
|
+
// Resolve each project's related tool slugs against the stacks collection. A
|
|
94
|
+
// known slug links to its page; an unknown one shows as "not in our catalog".
|
|
95
|
+
const relatedByFolder = new Map(projectSpecs.map((p) => [p.folder, p.related]));
|
|
96
|
+
const resolveTool = await toolResolver(lang);
|
|
97
|
+
// The right rail's impl block carries, per example: its related tools plus its
|
|
98
|
+
// file list (the file list lives here on ≥2xl screens, inline otherwise).
|
|
99
|
+
const projectRail = projects.map((p) => ({
|
|
100
|
+
folder: p.folder,
|
|
101
|
+
files: p.files,
|
|
102
|
+
headings: p.headings,
|
|
103
|
+
tools: (relatedByFolder.get(p.folder) ?? []).map(resolveTool),
|
|
104
|
+
}));
|
|
105
|
+
const relatedToolsByFolder = new Map(projectRail.map((p) => [p.folder, p.tools]));
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
<Breadcrumb segments={crumbs} />
|
|
109
|
+
|
|
110
|
+
<header class="mt-4">
|
|
111
|
+
<div class="relative isolate overflow-hidden rounded-3xl border border-[var(--aas-border)] bg-[var(--aas-panel)] p-6 sm:p-8">
|
|
112
|
+
{/* Ambient blurred-logo backdrop, echoing the listing cards' tint. */}
|
|
113
|
+
{logoUrl && (
|
|
114
|
+
<div class="aas-hero-bg" aria-hidden="true" style={`background-image:url("${logoUrl}")`} />
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
<div class="flex flex-col gap-5 min-[900px]:flex-row min-[900px]:items-start">
|
|
118
|
+
{/* Left: identity, description, links, facts, tags. */}
|
|
119
|
+
<div class="min-w-0 flex-1">
|
|
120
|
+
<div class="flex items-center gap-4">
|
|
121
|
+
<ToolAvatar name={data.name} logo={data.logo} logoDark={data.logoDark} category={data.category} size={56} />
|
|
122
|
+
<div class="min-w-0">
|
|
123
|
+
<h1 class="text-3xl font-bold tracking-tight">{data.name}</h1>
|
|
124
|
+
{data.vendor && (
|
|
125
|
+
<p class="mt-0.5 text-sm text-[var(--aas-muted)]">
|
|
126
|
+
{t('vendor.heading')}{' '}
|
|
127
|
+
{vendorSlug ? (
|
|
128
|
+
<a
|
|
129
|
+
href={getRelativeLocaleUrl(lang, `vendors/${vendorSlug}/`)}
|
|
130
|
+
class="text-[var(--aas-accent)] no-underline hover:underline"
|
|
131
|
+
>
|
|
132
|
+
{data.vendor}
|
|
133
|
+
</a>
|
|
134
|
+
) : (
|
|
135
|
+
<span>{data.vendor}</span>
|
|
136
|
+
)}
|
|
137
|
+
</p>
|
|
138
|
+
)}
|
|
139
|
+
{data.formerNames.length > 0 && (
|
|
140
|
+
<p class="mt-0.5 text-sm text-[var(--aas-muted)]">
|
|
141
|
+
{t('meta.formerly')} {data.formerNames.join(' · ')}
|
|
142
|
+
</p>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<p class="mt-3 aas-md max-w-2xl text-lg text-[var(--aas-muted)]" set:html={inlineMd(data.description)} />
|
|
148
|
+
|
|
149
|
+
<div class="mt-4 flex flex-wrap gap-2 text-sm">
|
|
150
|
+
{
|
|
151
|
+
links.map((l) => (
|
|
152
|
+
<a
|
|
153
|
+
href={l.href}
|
|
154
|
+
target="_blank"
|
|
155
|
+
rel="noopener noreferrer"
|
|
156
|
+
class="aas-lift rounded-lg border border-[var(--aas-border)] bg-[var(--aas-panel)] px-3 py-1.5 text-[var(--aas-text)] no-underline"
|
|
157
|
+
>
|
|
158
|
+
{l.label} <span class="text-[var(--aas-muted)]">↗</span>
|
|
159
|
+
</a>
|
|
160
|
+
))
|
|
161
|
+
}
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<ToolMeta
|
|
165
|
+
lang={lang}
|
|
166
|
+
pricing={[]}
|
|
167
|
+
license={data.license}
|
|
168
|
+
language={data.language}
|
|
169
|
+
version={displayVersion}
|
|
170
|
+
released={stats?.releasedAt?.slice(0, 10)}
|
|
171
|
+
deprecated={data.deprecated}
|
|
172
|
+
staleness={staleness}
|
|
173
|
+
class="mt-5"
|
|
174
|
+
/>
|
|
175
|
+
|
|
176
|
+
{
|
|
177
|
+
data.tags.length > 0 && (
|
|
178
|
+
<div class="mt-4 flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--aas-muted)]">
|
|
179
|
+
{data.tags.map((tag) => (
|
|
180
|
+
<a
|
|
181
|
+
href={getRelativeLocaleUrl(lang, `tags/${tag}/`)}
|
|
182
|
+
class="no-underline opacity-80 hover:text-[var(--aas-accent)] hover:opacity-100"
|
|
183
|
+
>
|
|
184
|
+
#{tag}
|
|
185
|
+
</a>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Right: key facts — stars + coloured pricing chips, as on the cards. */}
|
|
193
|
+
<div class="flex shrink-0 flex-wrap gap-1.5 text-xs min-[900px]:flex-col min-[900px]:items-end">
|
|
194
|
+
{
|
|
195
|
+
typeof stats?.stars === 'number' && (
|
|
196
|
+
<span class="aas-chip inline-flex items-center gap-1 rounded-lg px-2.5 py-1">
|
|
197
|
+
<StarIcon class="text-[var(--aas-text)]" />
|
|
198
|
+
{formatStars(stats.stars)}
|
|
199
|
+
</span>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
{
|
|
203
|
+
pricingTags(data.pricing).map((p) => (
|
|
204
|
+
<span
|
|
205
|
+
data-pricing-tag={p}
|
|
206
|
+
title={pricingLabels[lang][p] ?? p}
|
|
207
|
+
class:list={[p === COMPLETELY_FREE ? 'aas-free' : 'aas-pricing', 'inline-flex items-center gap-1 rounded-lg px-2.5 py-1']}
|
|
208
|
+
>
|
|
209
|
+
{p === 'open-source' && (
|
|
210
|
+
<svg
|
|
211
|
+
class="aas-oss-icon"
|
|
212
|
+
width="15"
|
|
213
|
+
height="15"
|
|
214
|
+
viewBox="0 0 24 24"
|
|
215
|
+
fill="none"
|
|
216
|
+
stroke="currentColor"
|
|
217
|
+
stroke-width="2"
|
|
218
|
+
stroke-linecap="round"
|
|
219
|
+
stroke-linejoin="round"
|
|
220
|
+
aria-hidden="true"
|
|
221
|
+
>
|
|
222
|
+
<polyline points="16 18 22 12 16 6" />
|
|
223
|
+
<polyline points="8 6 2 12 8 18" />
|
|
224
|
+
</svg>
|
|
225
|
+
)}
|
|
226
|
+
<span>{pricingLabels[lang][p] ?? p}</span>
|
|
227
|
+
</span>
|
|
228
|
+
))
|
|
229
|
+
}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* Editorial meta sits outside the card, just below it. */}
|
|
235
|
+
{
|
|
236
|
+
(data.docVersion || data.updated) && (
|
|
237
|
+
<p class="mt-2 px-1 text-xs text-[var(--aas-muted)]">
|
|
238
|
+
{data.docVersion && <span>{t('meta.docVersion')} {data.docVersion}</span>}
|
|
239
|
+
{data.docVersion && data.updated && <span> · </span>}
|
|
240
|
+
{data.updated && (
|
|
241
|
+
<span>
|
|
242
|
+
{t('meta.updated')}{' '}
|
|
243
|
+
<time datetime={isoDate(data.updated)}>{formatDate(data.updated, lang)}</time>
|
|
244
|
+
</span>
|
|
245
|
+
)}
|
|
246
|
+
</p>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
</header>
|
|
250
|
+
|
|
251
|
+
{/* Concepts that use this tool (computed backlink), shown above related tools. */}
|
|
252
|
+
{
|
|
253
|
+
relatedConcepts.length > 0 && (
|
|
254
|
+
<section class="mt-8">
|
|
255
|
+
<h2 class="text-xs font-semibold tracking-wide text-[var(--aas-muted)] uppercase">
|
|
256
|
+
{t('concept.relatedConcepts')}
|
|
257
|
+
</h2>
|
|
258
|
+
<CollapsibleGrid lang={lang} count={relatedConcepts.length}>
|
|
259
|
+
{relatedConcepts.map((c) => (
|
|
260
|
+
<a
|
|
261
|
+
href={getRelativeLocaleUrl(lang, `concept/${conceptSlugOf(c)}/`)}
|
|
262
|
+
class="aas-lift flex items-stretch overflow-hidden rounded-2xl border border-[var(--aas-border)] bg-[var(--aas-panel)] no-underline"
|
|
263
|
+
>
|
|
264
|
+
{c.data.image && (
|
|
265
|
+
<Image src={c.data.image} alt="" class="w-28 shrink-0 self-stretch object-cover" />
|
|
266
|
+
)}
|
|
267
|
+
<div class="min-w-0 p-4">
|
|
268
|
+
<span class="block font-semibold text-[var(--aas-text)]">{c.data.title}</span>
|
|
269
|
+
{(c.data.version || c.data.updated) && (
|
|
270
|
+
<span class="mt-0.5 block text-xs text-[var(--aas-muted)]">
|
|
271
|
+
{c.data.version && <span>{t('meta.docVersion')} {c.data.version}</span>}
|
|
272
|
+
{c.data.version && c.data.updated && <span> · </span>}
|
|
273
|
+
{c.data.updated && (
|
|
274
|
+
<span>
|
|
275
|
+
{t('concept.updated')} {formatDate(c.data.updated, lang)}
|
|
276
|
+
</span>
|
|
277
|
+
)}
|
|
278
|
+
</span>
|
|
279
|
+
)}
|
|
280
|
+
<span
|
|
281
|
+
class="aas-md mt-1.5 line-clamp-2 text-xs leading-relaxed text-[var(--aas-muted)] opacity-75"
|
|
282
|
+
set:html={inlineMd(c.data.description)}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
</a>
|
|
286
|
+
))}
|
|
287
|
+
</CollapsibleGrid>
|
|
288
|
+
</section>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
<RelatedTools lang={lang} slugs={data.related} />
|
|
293
|
+
|
|
294
|
+
{/* Content + a sticky right-rail TOC, side by side in one centered column. */}
|
|
295
|
+
<div class="mt-8 flex items-start gap-8">
|
|
296
|
+
<div class="min-w-0 flex-1 aas-reading">
|
|
297
|
+
{
|
|
298
|
+
hasTabs ? (
|
|
299
|
+
<DetailTabs
|
|
300
|
+
overviewLabel={t('tab.overview')}
|
|
301
|
+
pricingLabel={hasPricing ? t('tab.pricing') : undefined}
|
|
302
|
+
codeLabel={hasSamples ? t('tab.code') : undefined}
|
|
303
|
+
implLabel={hasProject ? t('tab.impl') : undefined}
|
|
304
|
+
>
|
|
305
|
+
<article slot="overview" class="prose max-w-none">
|
|
306
|
+
<Content />
|
|
307
|
+
</article>
|
|
308
|
+
{hasPricing && (
|
|
309
|
+
<article slot="pricing" class="prose max-w-none">
|
|
310
|
+
<PricingSection
|
|
311
|
+
lang={lang}
|
|
312
|
+
note={data.pricingNote}
|
|
313
|
+
tiers={data.pricingTiers}
|
|
314
|
+
source={data.pricingSource}
|
|
315
|
+
checkedAt={data.pricingCheckedAt}
|
|
316
|
+
/>
|
|
317
|
+
</article>
|
|
318
|
+
)}
|
|
319
|
+
{hasSamples && (
|
|
320
|
+
<div slot="code">
|
|
321
|
+
<CodeSamples samples={renderedSamples} lang={lang} />
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
{hasProject && (
|
|
325
|
+
<div slot="impl">
|
|
326
|
+
<ProjectViewer lang={lang} projects={projects} related={relatedToolsByFolder} />
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
</DetailTabs>
|
|
330
|
+
) : (
|
|
331
|
+
<article class="prose max-w-none">
|
|
332
|
+
<Content />
|
|
333
|
+
</article>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
{/* Fixed right-rail TOC: follows scroll, swaps with the active tab, and hides
|
|
339
|
+
near the footer. */}
|
|
340
|
+
<aside class="aas-toc-rail" data-toc-rail>
|
|
341
|
+
<div class="aas-toc-scroll">
|
|
342
|
+
<div data-toc="overview">
|
|
343
|
+
<Toc items={tocItems} title={t('toc.onThisPage')} />
|
|
344
|
+
</div>
|
|
345
|
+
{
|
|
346
|
+
hasSamples && (
|
|
347
|
+
<div data-toc="code" class="hidden">
|
|
348
|
+
{renderedSamples.map((s, i) => (
|
|
349
|
+
<div data-sample-toc={i} class:list={[i === 0 ? '' : 'hidden']}>
|
|
350
|
+
<Toc items={s.headings} title={t('toc.onThisPage')} />
|
|
351
|
+
</div>
|
|
352
|
+
))}
|
|
353
|
+
</div>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
{
|
|
357
|
+
hasProject && (
|
|
358
|
+
<div data-toc="impl" class="hidden">
|
|
359
|
+
{projectRail.map((pr, i) => (
|
|
360
|
+
<div data-project-related={pr.folder} class:list={[i === 0 ? '' : 'hidden', 'space-y-5']}>
|
|
361
|
+
<Toc items={pr.headings} title={t('toc.onThisPage')} />
|
|
362
|
+
<ProjectRelatedTools lang={lang} tools={pr.tools} />
|
|
363
|
+
<ProjectFileList lang={lang} folder={pr.folder} files={pr.files} />
|
|
364
|
+
</div>
|
|
365
|
+
))}
|
|
366
|
+
</div>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
</div>
|
|
370
|
+
<div class="aas-toc-foot">
|
|
371
|
+
<button type="button" class="aas-toc-top" data-back-to-top>↑ {t('top.label')}</button>
|
|
372
|
+
<button
|
|
373
|
+
type="button"
|
|
374
|
+
class="aas-toc-allbtn"
|
|
375
|
+
data-toggle-all
|
|
376
|
+
data-collapse={t('toc.collapseAll')}
|
|
377
|
+
data-expand={t('toc.expandAll')}
|
|
378
|
+
hidden
|
|
379
|
+
>
|
|
380
|
+
{t('toc.collapseAll')}
|
|
381
|
+
</button>
|
|
382
|
+
</div>
|
|
383
|
+
</aside>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
{(hasSamples || hasProject || overviewHasMermaid) && <MermaidLoader />}
|
|
387
|
+
|
|
388
|
+
{/* Restore collapsed file-tree folders from localStorage before first paint
|
|
389
|
+
(placed after every tree in the DOM, so no expand→collapse flash). */}
|
|
390
|
+
{hasProject && (
|
|
391
|
+
<script is:inline>
|
|
392
|
+
(function () {
|
|
393
|
+
try {
|
|
394
|
+
var collapsed = new Set(JSON.parse(localStorage.getItem('aas:tree-collapsed') || '[]'));
|
|
395
|
+
document.querySelectorAll('details.aas-tree-dir').forEach(function (d) {
|
|
396
|
+
if (collapsed.has(d.dataset.treeFolder + '::' + d.dataset.treeDir)) d.removeAttribute('open');
|
|
397
|
+
});
|
|
398
|
+
} catch (e) {}
|
|
399
|
+
})();
|
|
400
|
+
</script>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
<script>
|
|
404
|
+
import { startScrollSpy, initBackToTop, initReveals } from '../lib/toc-rail-client';
|
|
405
|
+
|
|
406
|
+
const rail = document.querySelector<HTMLElement>('[data-toc-rail]');
|
|
407
|
+
// Scroll-spy + back-to-top + prose-<details> persistence, shared with
|
|
408
|
+
// TocRail.astro. The tab/version/example handlers below re-run the spy after
|
|
409
|
+
// changing which headings are visible.
|
|
410
|
+
const spy = rail ? startScrollSpy(rail) : () => {};
|
|
411
|
+
if (rail) {
|
|
412
|
+
initBackToTop(rail);
|
|
413
|
+
initReveals(rail);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Swap the rail's TOC with the active tab.
|
|
417
|
+
const syncToc = (id: string) =>
|
|
418
|
+
rail
|
|
419
|
+
?.querySelectorAll<HTMLElement>('[data-toc]')
|
|
420
|
+
.forEach((el) => el.classList.toggle('hidden', el.dataset.toc !== id));
|
|
421
|
+
|
|
422
|
+
document.addEventListener('aas:tabchange', (e) => {
|
|
423
|
+
syncToc((e as CustomEvent<{ id: string }>).detail.id);
|
|
424
|
+
spy();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const active = document.querySelector<HTMLElement>('[data-tab][aria-selected="true"]');
|
|
428
|
+
if (active?.dataset.tab) syncToc(active.dataset.tab);
|
|
429
|
+
|
|
430
|
+
// Impl tab: the rail's related-tools block depends on the selected example.
|
|
431
|
+
const syncProjectRelated = (folder: string) =>
|
|
432
|
+
rail
|
|
433
|
+
?.querySelectorAll<HTMLElement>('[data-project-related]')
|
|
434
|
+
.forEach((b) => b.classList.toggle('hidden', b.dataset.projectRelated !== folder));
|
|
435
|
+
|
|
436
|
+
document.addEventListener('aas:project', (e) => {
|
|
437
|
+
syncProjectRelated((e as CustomEvent<{ folder: string }>).detail.folder);
|
|
438
|
+
spy();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Initial example (ProjectViewer may have restored one from the URL already).
|
|
442
|
+
const activeProj = document.querySelector<HTMLElement>('[data-project-panel]:not(.hidden)');
|
|
443
|
+
if (activeProj?.dataset.projectFolder) syncProjectRelated(activeProj.dataset.projectFolder);
|
|
444
|
+
|
|
445
|
+
// Code tab: show the selected version's internal headings (CodeSamples emits
|
|
446
|
+
// `aas:sample` when the version changes).
|
|
447
|
+
const tocBlocks = Array.from(document.querySelectorAll<HTMLElement>('[data-sample-toc]'));
|
|
448
|
+
document.addEventListener('aas:sample', (e) => {
|
|
449
|
+
const idx = String((e as CustomEvent<{ index: number }>).detail.index);
|
|
450
|
+
tocBlocks.forEach((b) => b.classList.toggle('hidden', b.dataset.sampleToc !== idx));
|
|
451
|
+
spy();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Re-run once after the initial tab/example sync above settled visibility.
|
|
455
|
+
spy();
|
|
456
|
+
|
|
457
|
+
// Persist file-tree folder collapse to localStorage. The same folder shows
|
|
458
|
+
// twice (inline list + rail), so toggling one mirrors the other. The initial
|
|
459
|
+
// closed state is applied flash-free by the inline script above.
|
|
460
|
+
const TREE_KEY = 'aas:tree-collapsed';
|
|
461
|
+
const treeDirs = Array.from(document.querySelectorAll<HTMLDetailsElement>('details.aas-tree-dir'));
|
|
462
|
+
const treeKey = (d: HTMLDetailsElement) => `${d.dataset.treeFolder}::${d.dataset.treeDir}`;
|
|
463
|
+
const loadCollapsed = (): Set<string> => {
|
|
464
|
+
try {
|
|
465
|
+
return new Set(JSON.parse(localStorage.getItem(TREE_KEY) || '[]'));
|
|
466
|
+
} catch {
|
|
467
|
+
return new Set();
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
treeDirs.forEach((d) =>
|
|
471
|
+
d.addEventListener('toggle', () => {
|
|
472
|
+
const key = treeKey(d);
|
|
473
|
+
const set = loadCollapsed();
|
|
474
|
+
d.open ? set.delete(key) : set.add(key);
|
|
475
|
+
try {
|
|
476
|
+
localStorage.setItem(TREE_KEY, JSON.stringify([...set]));
|
|
477
|
+
} catch {}
|
|
478
|
+
// Mirror the other copy of this folder (guard prevents a toggle loop).
|
|
479
|
+
treeDirs.forEach((o) => {
|
|
480
|
+
if (o !== d && treeKey(o) === key && o.open !== d.open) o.open = d.open;
|
|
481
|
+
});
|
|
482
|
+
}),
|
|
483
|
+
);
|
|
484
|
+
</script>
|
|
485
|
+
|
|
486
|
+
{
|
|
487
|
+
relatedArticles.length > 0 && (
|
|
488
|
+
<aside class="mt-10 border-t border-[var(--aas-border)] pt-6">
|
|
489
|
+
<h2 class="text-sm font-semibold tracking-wide text-[var(--aas-muted)] uppercase">
|
|
490
|
+
{t('detail.relatedWriting')}
|
|
491
|
+
</h2>
|
|
492
|
+
<ul class="mt-3 space-y-2">
|
|
493
|
+
{relatedArticles.map((a) => (
|
|
494
|
+
<li>
|
|
495
|
+
<a
|
|
496
|
+
href={getRelativeLocaleUrl(lang, `article/${articleSlugOf(a)}/`)}
|
|
497
|
+
class="text-[var(--aas-accent)] no-underline hover:underline"
|
|
498
|
+
>
|
|
499
|
+
{a.data.title}
|
|
500
|
+
</a>
|
|
501
|
+
</li>
|
|
502
|
+
))}
|
|
503
|
+
</ul>
|
|
504
|
+
</aside>
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
size?: number;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { size = 12, class: className } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<svg
|
|
11
|
+
width={size}
|
|
12
|
+
height={size}
|
|
13
|
+
viewBox="0 0 24 24"
|
|
14
|
+
fill="currentColor"
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
class:list={['inline-block', className]}
|
|
17
|
+
>
|
|
18
|
+
<path
|
|
19
|
+
d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.123 2.123 0 0 0-.611-1.878L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.595-1.16z"
|
|
20
|
+
></path>
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
|
+
import StackCard from './StackCard.astro';
|
|
4
|
+
import { getStacksByTag, slugOf } from '../lib/stacks';
|
|
5
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
lang: Lang;
|
|
9
|
+
tag: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { lang, tag } = Astro.props;
|
|
13
|
+
const t = useTranslations(lang);
|
|
14
|
+
const entries = (await getStacksByTag(lang, tag)).sort((a, b) =>
|
|
15
|
+
a.data.name.localeCompare(b.data.name),
|
|
16
|
+
);
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<section class="py-6">
|
|
20
|
+
<a href={getRelativeLocaleUrl(lang, '')} class="text-sm text-[var(--aas-accent)] no-underline"
|
|
21
|
+
>← {t('tag.allTools')}</a
|
|
22
|
+
>
|
|
23
|
+
<h1 class="mt-3 text-3xl font-bold tracking-tight">
|
|
24
|
+
<span class="text-[var(--aas-muted)]">{t('tag.heading')}</span> #{tag}
|
|
25
|
+
</h1>
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
|
29
|
+
{
|
|
30
|
+
entries.map((e) => (
|
|
31
|
+
<StackCard
|
|
32
|
+
slug={slugOf(e)}
|
|
33
|
+
lang={lang}
|
|
34
|
+
name={e.data.name}
|
|
35
|
+
formerNames={e.data.formerNames}
|
|
36
|
+
vendor={e.data.vendor}
|
|
37
|
+
description={e.data.description}
|
|
38
|
+
tags={e.data.tags}
|
|
39
|
+
pricing={e.data.pricing}
|
|
40
|
+
license={e.data.license}
|
|
41
|
+
language={e.data.language}
|
|
42
|
+
category={e.data.category}
|
|
43
|
+
logo={e.data.logo}
|
|
44
|
+
logoDark={e.data.logoDark}
|
|
45
|
+
repo={e.data.repo}
|
|
46
|
+
version={e.data.version}
|
|
47
|
+
deprecated={e.data.deprecated}
|
|
48
|
+
/>
|
|
49
|
+
))
|
|
50
|
+
}
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
3
|
+
import ChevronDown from './ChevronDown.astro';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
lang: Lang;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { lang } = Astro.props;
|
|
10
|
+
const t = useTranslations(lang);
|
|
11
|
+
|
|
12
|
+
const options = [
|
|
13
|
+
{ value: 'system', label: t('theme.system') },
|
|
14
|
+
{ value: 'light', label: t('theme.light') },
|
|
15
|
+
{ value: 'dark', label: t('theme.dark') },
|
|
16
|
+
];
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<details class="aas-theme">
|
|
20
|
+
<summary
|
|
21
|
+
class="flex cursor-pointer list-none items-center gap-1 rounded-lg border border-[var(--aas-border)] px-2.5 py-1 text-[var(--aas-text)] select-none"
|
|
22
|
+
aria-label={t('theme.label')}
|
|
23
|
+
title={t('theme.label')}
|
|
24
|
+
>
|
|
25
|
+
<span data-theme-icon aria-hidden="true">☀</span>
|
|
26
|
+
<ChevronDown class="text-[var(--aas-muted)]" />
|
|
27
|
+
</summary>
|
|
28
|
+
<ul
|
|
29
|
+
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"
|
|
30
|
+
>
|
|
31
|
+
{
|
|
32
|
+
options.map((o) => (
|
|
33
|
+
<li>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
data-theme-option={o.value}
|
|
37
|
+
aria-pressed="false"
|
|
38
|
+
class="flex w-full items-center justify-between px-3 py-1.5 text-left text-[var(--aas-muted)] hover:bg-[var(--aas-bg)] aria-pressed:font-semibold aria-pressed:text-[var(--aas-text)]"
|
|
39
|
+
>
|
|
40
|
+
{o.label}
|
|
41
|
+
<span data-check aria-hidden="true" class="opacity-0">
|
|
42
|
+
✓
|
|
43
|
+
</span>
|
|
44
|
+
</button>
|
|
45
|
+
</li>
|
|
46
|
+
))
|
|
47
|
+
}
|
|
48
|
+
</ul>
|
|
49
|
+
</details>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
.aas-theme {
|
|
53
|
+
position: relative;
|
|
54
|
+
}
|
|
55
|
+
.aas-theme > summary::-webkit-details-marker {
|
|
56
|
+
display: none;
|
|
57
|
+
}
|
|
58
|
+
.aas-theme [data-theme-option][aria-pressed='true'] [data-check] {
|
|
59
|
+
opacity: 1;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
62
|
+
|
|
63
|
+
<script is:inline>
|
|
64
|
+
(function () {
|
|
65
|
+
var root = document.documentElement;
|
|
66
|
+
var KEY = 'theme';
|
|
67
|
+
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
68
|
+
|
|
69
|
+
function getPref() {
|
|
70
|
+
try {
|
|
71
|
+
return localStorage.getItem(KEY) || 'system';
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return 'system';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function resolve(pref) {
|
|
77
|
+
return pref === 'dark' || (pref === 'system' && mq.matches) ? 'dark' : 'light';
|
|
78
|
+
}
|
|
79
|
+
function apply(pref) {
|
|
80
|
+
root.dataset.theme = resolve(pref);
|
|
81
|
+
document.querySelectorAll('[data-theme-option]').forEach(function (el) {
|
|
82
|
+
el.setAttribute('aria-pressed', el.dataset.themeOption === pref ? 'true' : 'false');
|
|
83
|
+
});
|
|
84
|
+
document.querySelectorAll('[data-theme-icon]').forEach(function (el) {
|
|
85
|
+
el.textContent = root.dataset.theme === 'dark' ? '☾' : '☀';
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
document.querySelectorAll('[data-theme-option]').forEach(function (el) {
|
|
90
|
+
el.addEventListener('click', function () {
|
|
91
|
+
var pref = el.dataset.themeOption;
|
|
92
|
+
try {
|
|
93
|
+
localStorage.setItem(KEY, pref);
|
|
94
|
+
} catch (e) {}
|
|
95
|
+
apply(pref);
|
|
96
|
+
var d = el.closest('details');
|
|
97
|
+
if (d) d.open = false;
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
mq.addEventListener('change', function () {
|
|
101
|
+
if (getPref() === 'system') apply('system');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
apply(getPref());
|
|
105
|
+
})();
|
|
106
|
+
</script>
|