stack-site-builder 1.17.5 → 1.19.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 +38 -0
- package/README.md +17 -5
- package/index.d.ts +1 -0
- package/index.mjs +12 -1
- package/package.json +1 -1
- package/src/components/BlogIndex.astro +2 -1
- package/src/components/CategoryIndex.astro +9 -3
- package/src/components/ConceptIndex.astro +2 -1
- package/src/components/CourseIndex.astro +2 -1
- package/src/components/Home.astro +2 -1
- package/src/components/PaperCard.astro +85 -0
- package/src/components/PaperDetail.astro +123 -0
- package/src/components/PapersIndex.astro +144 -0
- package/src/components/ProductsIndex.astro +2 -1
- package/src/components/RelatedPapers.astro +59 -0
- package/src/components/SlidesIndex.astro +2 -1
- package/src/components/TagIndex.astro +2 -1
- package/src/components/VendorIndex.astro +2 -1
- package/src/content.ts +87 -1
- package/src/i18n/ui.ts +26 -0
- package/src/layouts/BaseLayout.astro +117 -6
- package/src/lib/listing.ts +9 -0
- package/src/lib/papers.ts +25 -0
- package/src/lib/private-client.ts +45 -1
- package/src/lib/sections.ts +2 -0
- package/src/pages/[...lang]/paper/[...id].astro +35 -0
- package/src/pages/[...lang]/paper/category/[id].astro +25 -0
- package/src/pages/[...lang]/paper/index.astro +18 -0
- package/src/pages/aas-auth.json.ts +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,42 @@ content schema, while a consuming site supplies only content, taxonomy data and
|
|
|
11
11
|
config. Sites track the theme with `pnpm up stack-site-builder`, so each release
|
|
12
12
|
here is a plain version bump they pull in.
|
|
13
13
|
|
|
14
|
+
## [1.19.0] - 2026-07-23
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Header member login/logout** — sites with private content get an account
|
|
19
|
+
control in the header: a login dropdown (id/password, same credential
|
|
20
|
+
machinery as the page gates) and a logout button once signed in. The
|
|
21
|
+
control reveals itself only when the new `/aas-auth.json` endpoint reports
|
|
22
|
+
configured login users (it serves the salted user table every private page
|
|
23
|
+
already embeds — nothing new is exposed); sites without `AAS_PRIVATE_*`
|
|
24
|
+
env see nothing. Credential verification moved into a shared
|
|
25
|
+
`loginWithCredentials` helper in `lib/private-client`.
|
|
26
|
+
- **Slides nav hides without listed decks** — a site whose decks are all
|
|
27
|
+
private+unlisted reaches them from course pages, so the header's Slides
|
|
28
|
+
item now appears only when the index actually lists at least one deck.
|
|
29
|
+
|
|
30
|
+
## [1.18.0] - 2026-07-23
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`papers` collection — the reading room** — an opt-in section
|
|
35
|
+
(`sections: { papers: true }` + `src/data/paper-categories.ts`, with an
|
|
36
|
+
optional `paperCategoryMap` for build-time category validation) for sites
|
|
37
|
+
that review the literature behind their stack. Frontmatter carries the
|
|
38
|
+
full author list (cards abbreviate to "et al."), venue/year, arXiv /
|
|
39
|
+
publisher / released-code links, and an **open-source availability badge**
|
|
40
|
+
(`openSource` + `repo`); `tools` cross-links catalog stacks on the detail
|
|
41
|
+
page. Routes mirror concepts: `/paper/`, `/paper/<id>/`,
|
|
42
|
+
`/paper/category/<id>/`, with a category-grouped index.
|
|
43
|
+
- **Private entries are unlisted by default** — index listings (catalog
|
|
44
|
+
home, categories, tags, vendors, blog, concepts, courses, papers,
|
|
45
|
+
products, slides) now skip `private` entries unless they opt in with
|
|
46
|
+
`listed: true`, which shows the familiar locked teaser card. Unlisted
|
|
47
|
+
private entries still build and stay reachable via direct links and the
|
|
48
|
+
related sections on detail pages — the discovery path for gated content.
|
|
49
|
+
|
|
14
50
|
## [1.17.4] - 2026-07-22
|
|
15
51
|
|
|
16
52
|
### Changed
|
|
@@ -283,6 +319,8 @@ catalog sites from a thin content-only repository.
|
|
|
283
319
|
- **Standalone development setup** — a devcontainer and a minimal `playground/`
|
|
284
320
|
consuming site for developing and previewing the theme on its own.
|
|
285
321
|
|
|
322
|
+
[1.19.0]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.18.0...v1.19.0
|
|
323
|
+
[1.18.0]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.4...v1.18.0
|
|
286
324
|
[1.17.4]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.3...v1.17.4
|
|
287
325
|
[1.17.3]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.2...v1.17.3
|
|
288
326
|
[1.17.2]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.1...v1.17.2
|
package/README.md
CHANGED
|
@@ -38,9 +38,9 @@ export const collections = defineAasCollections({ categoryMap });
|
|
|
38
38
|
| --- | --- |
|
|
39
39
|
| `src/data/site.ts` | Site identity: name, repo URL (`repoNav: false` hides the header's GitHub link), the `locales` it ships, optional `sections` toggles, browser icons (`icons: { favicon, appleTouch, manifest }`), the `home` template, per-locale UI string overrides |
|
|
40
40
|
| `src/data/categories.ts` | The tool-catalog category tree (validated against content) |
|
|
41
|
-
| `src/data/concept-categories.ts` · `article-categories.ts` · `course-categories.ts` · `product-categories.ts` (opt-in) | Taxonomies for concepts / articles / courses / products |
|
|
41
|
+
| `src/data/concept-categories.ts` · `article-categories.ts` · `course-categories.ts` · `product-categories.ts` · `paper-categories.ts` (opt-in) | Taxonomies for concepts / articles / courses / products / papers |
|
|
42
42
|
| `src/data/glossary.mjs` | `[[Term]]` wikilink targets |
|
|
43
|
-
| `src/content/{stacks,concepts,courses,products,articles,slides}/` | The content, one MDX file per locale |
|
|
43
|
+
| `src/content/{stacks,concepts,courses,products,papers,articles,slides}/` | The content, one MDX file per locale |
|
|
44
44
|
| `src/content/pages/` | Standalone top-level pages (e.g. an About/소개), rendered at `/<slug>/` and optionally linked in the header nav |
|
|
45
45
|
| `public/` · `samples/` | Logos/favicons and runnable sample projects |
|
|
46
46
|
|
|
@@ -72,7 +72,7 @@ The rest are opt-out — **concepts, articles, samples, slides, glossary** and t
|
|
|
72
72
|
standalone **pages** collection (About/소개, …) — so a site can ship only what it
|
|
73
73
|
needs. Turning one off removes both its routes and its header-nav item. (`pages`
|
|
74
74
|
also has finer control: each page's `nav` / `draft` frontmatter, or simply not
|
|
75
|
-
authoring it.) **courses** and **
|
|
75
|
+
authoring it.) **courses**, **products** and **papers** are the opt-IN sections — see below.
|
|
76
76
|
|
|
77
77
|
Declare the toggles once in `src/data/site.ts` and forward them to the theme in
|
|
78
78
|
astro.config (which needs them to skip route injection). Import `SectionKey` from
|
|
@@ -166,6 +166,16 @@ Localized values are either one string or a per-locale record with
|
|
|
166
166
|
default-locale fallback. On a cards home the header's Browse link (which
|
|
167
167
|
anchors into the catalog) hides itself.
|
|
168
168
|
|
|
169
|
+
## Papers (opt-in)
|
|
170
|
+
|
|
171
|
+
A reading room for the literature behind the stack: each entry is one paper
|
|
172
|
+
with its full author list, venue/year, arXiv / publisher / code links and an
|
|
173
|
+
open-source availability badge; the body is your reading of it. `tools`
|
|
174
|
+
cross-links catalog stacks. Enable with `sections: { papers: true }` plus
|
|
175
|
+
`src/data/paper-categories.ts` (exporting `paperTree` / `paperCatOf`), and
|
|
176
|
+
optionally pass `paperCategoryMap` to `defineAasCollections`. Routes mirror
|
|
177
|
+
concepts: `/paper/`, `/paper/<id>/`, `/paper/category/<id>/`.
|
|
178
|
+
|
|
169
179
|
## Body components
|
|
170
180
|
|
|
171
181
|
Reusable MDX-body components, importable from any collection's content:
|
|
@@ -196,8 +206,10 @@ teaser: A public one-liner shown on cards and above the login form. # optional
|
|
|
196
206
|
|
|
197
207
|
The body ships **encrypted** (AES-256-GCM, key wrapped per user with PBKDF2) and
|
|
198
208
|
is decrypted in the browser after login — no server needed, works on any static
|
|
199
|
-
host.
|
|
200
|
-
|
|
209
|
+
host. By default a private entry stays OUT of index listings (reachable via
|
|
210
|
+
direct links and the related sections on detail pages); set `listed: true` to
|
|
211
|
+
show it as a locked teaser card (title + 🔒 + teaser). Private URLs get
|
|
212
|
+
`noindex` and stay out of the sitemap. Users and keys come from env vars (see
|
|
201
213
|
`playground/.env.sample`): `AAS_PRIVATE_USERS` (`id:password,…`),
|
|
202
214
|
`AAS_PRIVATE_MASTER_SECRET` (rotate it to log every device out) and
|
|
203
215
|
`AAS_PRIVATE_SESSION_DAYS`. Set them in `.env` locally or as CI secrets.
|
package/index.d.ts
CHANGED
package/index.mjs
CHANGED
|
@@ -44,6 +44,9 @@ const PAGES = [
|
|
|
44
44
|
'concept/[...id].astro',
|
|
45
45
|
'concept/category/[id].astro',
|
|
46
46
|
'course/index.astro',
|
|
47
|
+
'paper/index.astro',
|
|
48
|
+
'paper/[...id].astro',
|
|
49
|
+
'paper/category/[id].astro',
|
|
47
50
|
'course/[...id].astro',
|
|
48
51
|
'course/category/[id].astro',
|
|
49
52
|
'glossary.astro',
|
|
@@ -72,6 +75,7 @@ function sectionOf(file) {
|
|
|
72
75
|
if (file.startsWith('concept/')) return 'concepts';
|
|
73
76
|
if (file.startsWith('article/')) return 'articles';
|
|
74
77
|
if (file.startsWith('course/')) return 'courses';
|
|
78
|
+
if (file.startsWith('paper/')) return 'papers';
|
|
75
79
|
if (file.startsWith('products/')) return 'products';
|
|
76
80
|
if (file.startsWith('sample/')) return 'samples';
|
|
77
81
|
if (file.startsWith('slides/')) return 'slides';
|
|
@@ -85,7 +89,7 @@ function sectionOf(file) {
|
|
|
85
89
|
// when the site passes `{ <key>: true }`. Both need site-side data
|
|
86
90
|
// (src/data/{course,product}-categories.ts), so a theme upgrade alone must
|
|
87
91
|
// not enable them.
|
|
88
|
-
const OPT_IN_SECTIONS = new Set(['courses', 'products']);
|
|
92
|
+
const OPT_IN_SECTIONS = new Set(['courses', 'products', 'papers']);
|
|
89
93
|
|
|
90
94
|
/**
|
|
91
95
|
* @param {object} opts
|
|
@@ -125,6 +129,13 @@ export default function aasTheme({ glossary, sections = {} }) {
|
|
|
125
129
|
});
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
// Global (locale-less) login user table for the header login control —
|
|
133
|
+
// `{ enabled: false }` on sites without private-content env config.
|
|
134
|
+
injectRoute({
|
|
135
|
+
pattern: '/aas-auth.json',
|
|
136
|
+
entrypoint: 'stack-site-builder/pages/aas-auth.json.ts',
|
|
137
|
+
});
|
|
138
|
+
|
|
128
139
|
updateConfig({
|
|
129
140
|
markdown: aasMarkdown({ glossary, locales, defaultLocale }),
|
|
130
141
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stack-site-builder",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.19.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "The engine behind the awesome-*-stack catalog sites: an Astro theme with the catalog/concepts/articles/slides/samples routes, components, styles and markdown pipeline. Sites provide content, taxonomy data and config.",
|
|
7
7
|
"repository": {
|
|
@@ -8,6 +8,7 @@ import ArrowUpRight from './ArrowUpRight.astro';
|
|
|
8
8
|
const PAGE = 12;
|
|
9
9
|
import { articleTree, articleCatOf } from '@aas-data/article-categories';
|
|
10
10
|
import { getArticles, articleSlugOf, type ArticleEntry } from '../lib/articles';
|
|
11
|
+
import { listedInIndex } from '../lib/listing';
|
|
11
12
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
13
|
|
|
13
14
|
// Doubles as the writing index (no `categoryId` → all articles, grouped by
|
|
@@ -20,7 +21,7 @@ interface Props {
|
|
|
20
21
|
|
|
21
22
|
const { lang, categoryId } = Astro.props;
|
|
22
23
|
const t = useTranslations(lang);
|
|
23
|
-
const all = await getArticles(lang);
|
|
24
|
+
const all = (await getArticles(lang)).filter((e) => listedInIndex(e.data));
|
|
24
25
|
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `article/category/${cid}/`);
|
|
25
26
|
|
|
26
27
|
const itemsIn = (cid: string): ArticleEntry[] => {
|
|
@@ -7,6 +7,7 @@ import ArrowUpRight from './ArrowUpRight.astro';
|
|
|
7
7
|
import { categoryMap, childrenOf, pathOf } from '@aas-data/categories';
|
|
8
8
|
import { getStacks, getStacksByCategory, slugOf, type StackEntry } from '../lib/stacks';
|
|
9
9
|
import { deriveFacets } from '../lib/facets';
|
|
10
|
+
import { listedInIndex } from '../lib/listing';
|
|
10
11
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
@@ -31,16 +32,21 @@ const crumbs = [
|
|
|
31
32
|
|
|
32
33
|
const children = childrenOf(id);
|
|
33
34
|
// Stacks attached directly to this node (vs. nested under a subcategory).
|
|
34
|
-
const direct = (await getStacksByCategory(lang, id)).filter(
|
|
35
|
+
const direct = (await getStacksByCategory(lang, id)).filter(
|
|
36
|
+
(s) => s.data.category === id && listedInIndex(s.data),
|
|
37
|
+
);
|
|
35
38
|
// Each child subcategory with its own rolled-up stacks.
|
|
36
39
|
const childGroups = await Promise.all(
|
|
37
|
-
children.map(async (c) => ({
|
|
40
|
+
children.map(async (c) => ({
|
|
41
|
+
cat: c,
|
|
42
|
+
items: (await getStacksByCategory(lang, c.id)).filter((s) => listedInIndex(s.data)),
|
|
43
|
+
})),
|
|
38
44
|
);
|
|
39
45
|
|
|
40
46
|
// Filter facets are built from the WHOLE catalog (not just this page's tools) so
|
|
41
47
|
// they're identical on every page — a sort/filter carried in from elsewhere can
|
|
42
48
|
// always be restored here, even if no tool in this category has that value.
|
|
43
|
-
const facets = deriveFacets(await getStacks(lang), lang);
|
|
49
|
+
const facets = deriveFacets((await getStacks(lang)).filter((e) => listedInIndex(e.data)), lang);
|
|
44
50
|
|
|
45
51
|
const cardProps = (e: StackEntry) => ({
|
|
46
52
|
slug: slugOf(e),
|
|
@@ -8,6 +8,7 @@ import ArrowUpRight from './ArrowUpRight.astro';
|
|
|
8
8
|
const PAGE = 12;
|
|
9
9
|
import { conceptTree, conceptCatOf } from '@aas-data/concept-categories';
|
|
10
10
|
import { getConcepts, conceptSlugOf, type ConceptEntry } from '../lib/concepts';
|
|
11
|
+
import { listedInIndex } from '../lib/listing';
|
|
11
12
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
13
|
|
|
13
14
|
// Doubles as the concept index (no `categoryId` → all concepts, grouped by
|
|
@@ -22,7 +23,7 @@ const { lang, categoryId } = Astro.props;
|
|
|
22
23
|
const t = useTranslations(lang);
|
|
23
24
|
// Default order is most-recently-updated first (matches the ListControls
|
|
24
25
|
// default), so the server render and the client sort agree — no reflow on load.
|
|
25
|
-
const all = (await getConcepts(lang)).sort(
|
|
26
|
+
const all = (await getConcepts(lang)).filter((e) => listedInIndex(e.data)).sort(
|
|
26
27
|
(a, b) =>
|
|
27
28
|
(b.data.updated?.getTime() ?? 0) - (a.data.updated?.getTime() ?? 0) ||
|
|
28
29
|
a.data.title.localeCompare(b.data.title),
|
|
@@ -8,6 +8,7 @@ import ArrowUpRight from './ArrowUpRight.astro';
|
|
|
8
8
|
const PAGE = 12;
|
|
9
9
|
import { courseTree, courseCatOf } from '@aas-data/course-categories';
|
|
10
10
|
import { getCourses, courseSlugOf, type CourseEntry } from '../lib/courses';
|
|
11
|
+
import { listedInIndex } from '../lib/listing';
|
|
11
12
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
13
|
|
|
13
14
|
// Doubles as the course index (no `categoryId` → all courses, grouped by
|
|
@@ -22,7 +23,7 @@ const { lang, categoryId } = Astro.props;
|
|
|
22
23
|
const t = useTranslations(lang);
|
|
23
24
|
// getCourses() order (manual `order` desc, then date) agrees with the client
|
|
24
25
|
// "recent" comparator over CourseCard's data-date keys — no reflow on load.
|
|
25
|
-
const all = await getCourses(lang);
|
|
26
|
+
const all = (await getCourses(lang)).filter((e) => listedInIndex(e.data));
|
|
26
27
|
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `course/category/${cid}/`);
|
|
27
28
|
|
|
28
29
|
const itemsIn = (cid: string): CourseEntry[] => {
|
|
@@ -7,6 +7,7 @@ import { rootCategories, descendantIds } from '@aas-data/categories';
|
|
|
7
7
|
import { getStacks, slugOf } from '../lib/stacks';
|
|
8
8
|
import { site } from '@aas-data/site';
|
|
9
9
|
import { deriveFacets } from '../lib/facets';
|
|
10
|
+
import { listedInIndex } from '../lib/listing';
|
|
10
11
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
@@ -15,7 +16,7 @@ interface Props {
|
|
|
15
16
|
|
|
16
17
|
const { lang } = Astro.props;
|
|
17
18
|
const t = useTranslations(lang);
|
|
18
|
-
const entries = await getStacks(lang);
|
|
19
|
+
const entries = (await getStacks(lang)).filter((e) => listedInIndex(e.data));
|
|
19
20
|
|
|
20
21
|
// Default order is alphabetical (matches the client's default sort, so no flash).
|
|
21
22
|
// Group by top-level category, rolling up every subcategory in its subtree.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Image } from 'astro:assets';
|
|
3
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
4
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
5
|
+
import { shortAuthors } from '../lib/papers';
|
|
6
|
+
import { inlineMd } from '../lib/inline-md';
|
|
7
|
+
import type { ImageMetadata } from 'astro';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
lang: Lang;
|
|
11
|
+
slug: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
authors: string[];
|
|
15
|
+
year?: number;
|
|
16
|
+
venue?: string;
|
|
17
|
+
openSource?: boolean;
|
|
18
|
+
tags: string[];
|
|
19
|
+
image?: ImageMetadata;
|
|
20
|
+
imageAlt?: string;
|
|
21
|
+
/** Private entry: show a lock and the (public) `teaser` instead of the description. */
|
|
22
|
+
isPrivate?: boolean;
|
|
23
|
+
teaser?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
lang,
|
|
28
|
+
slug,
|
|
29
|
+
title,
|
|
30
|
+
description,
|
|
31
|
+
authors,
|
|
32
|
+
year,
|
|
33
|
+
venue,
|
|
34
|
+
openSource = false,
|
|
35
|
+
tags,
|
|
36
|
+
image,
|
|
37
|
+
imageAlt,
|
|
38
|
+
isPrivate = false,
|
|
39
|
+
teaser,
|
|
40
|
+
} = Astro.props;
|
|
41
|
+
const t = useTranslations(lang);
|
|
42
|
+
const shownDesc = isPrivate ? (teaser ?? '') : description;
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<a
|
|
46
|
+
href={getRelativeLocaleUrl(lang, `paper/${slug}/`)}
|
|
47
|
+
data-name={title.toLowerCase()}
|
|
48
|
+
data-date={year ? String(year) : ''}
|
|
49
|
+
class="aas-lift block overflow-hidden rounded-3xl border border-[var(--aas-border)] bg-[var(--aas-panel)] no-underline"
|
|
50
|
+
>
|
|
51
|
+
{image && <Image src={image} alt={imageAlt ?? title} class="aspect-[16/9] w-full object-cover" />}
|
|
52
|
+
<div class="p-5">
|
|
53
|
+
<p class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-[var(--aas-muted)]">
|
|
54
|
+
{venue && <span>{venue}</span>}
|
|
55
|
+
{venue && year && <span>·</span>}
|
|
56
|
+
{year && <span>{year}</span>}
|
|
57
|
+
{
|
|
58
|
+
openSource && (
|
|
59
|
+
<span class="rounded-full border border-[var(--aas-accent)] px-2 py-0.5 text-[var(--aas-accent)]">
|
|
60
|
+
{t('paper.openSource')}
|
|
61
|
+
</span>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
</p>
|
|
65
|
+
<h3 class="mt-1 text-lg font-semibold text-[var(--aas-text)]">
|
|
66
|
+
{isPrivate && <span aria-label={t('private.badge')} title={t('private.badge')}>🔒 </span>}{title}
|
|
67
|
+
</h3>
|
|
68
|
+
{authors.length > 0 && <p class="mt-1 text-sm text-[var(--aas-muted)]">{shortAuthors(authors)}</p>}
|
|
69
|
+
<p
|
|
70
|
+
class="aas-md mt-2 text-sm leading-relaxed text-[var(--aas-muted)]"
|
|
71
|
+
set:html={inlineMd(shownDesc)}
|
|
72
|
+
/>
|
|
73
|
+
{
|
|
74
|
+
tags.length > 0 && (
|
|
75
|
+
<div class="mt-3 flex flex-wrap gap-1.5">
|
|
76
|
+
{tags.slice(0, 4).map((tag) => (
|
|
77
|
+
<span class="rounded-full border border-[var(--aas-border)] px-2 py-0.5 text-xs text-[var(--aas-muted)]">
|
|
78
|
+
{tag}
|
|
79
|
+
</span>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
</div>
|
|
85
|
+
</a>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { render } from 'astro:content';
|
|
3
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
4
|
+
import { getPapers, paperSlugOf, type PaperEntry } from '../lib/papers';
|
|
5
|
+
import { inlineMd } from '../lib/inline-md';
|
|
6
|
+
import { Image } from 'astro:assets';
|
|
7
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
8
|
+
import { formatDate } from '../lib/dates';
|
|
9
|
+
import MermaidLoader from './MermaidLoader.astro';
|
|
10
|
+
import TocRail from './TocRail.astro';
|
|
11
|
+
import Breadcrumb from './Breadcrumb.astro';
|
|
12
|
+
import ArrowUpRight from './ArrowUpRight.astro';
|
|
13
|
+
import { paperTree, paperCatOf } from '@aas-data/paper-categories';
|
|
14
|
+
import RelatedPapers from './RelatedPapers.astro';
|
|
15
|
+
import RelatedTools from './RelatedTools.astro';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
entry: PaperEntry;
|
|
19
|
+
lang: Lang;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { entry, lang } = Astro.props;
|
|
23
|
+
const t = useTranslations(lang);
|
|
24
|
+
const { Content, headings } = await render(entry);
|
|
25
|
+
const data = entry.data;
|
|
26
|
+
const tocItems = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
|
|
27
|
+
const bodyHasMermaid = (entry.body ?? '').includes('```mermaid');
|
|
28
|
+
|
|
29
|
+
// Sibling papers linked from frontmatter (mutual cross-links, like concepts).
|
|
30
|
+
const papers = await getPapers(lang);
|
|
31
|
+
const paperBySlug = new Map(papers.map((c) => [paperSlugOf(c), c]));
|
|
32
|
+
const relatedPapers = data.related
|
|
33
|
+
.map((sl) => paperBySlug.get(sl))
|
|
34
|
+
.filter(Boolean) as PaperEntry[];
|
|
35
|
+
|
|
36
|
+
// External resource buttons: arXiv, the publisher page, and released code.
|
|
37
|
+
const links = [
|
|
38
|
+
data.arxiv && { label: t('paper.arxiv'), href: data.arxiv },
|
|
39
|
+
data.paperUrl && { label: t('paper.paper'), href: data.paperUrl },
|
|
40
|
+
data.repo && { label: t('paper.code'), href: data.repo },
|
|
41
|
+
].filter(Boolean) as { label: string; href: string }[];
|
|
42
|
+
|
|
43
|
+
// Breadcrumb: All papers › category › subcategory… (each segment linked).
|
|
44
|
+
const crumbs = [
|
|
45
|
+
{ label: t('paper.backToPapers'), href: getRelativeLocaleUrl(lang, 'paper/') },
|
|
46
|
+
...paperTree.pathOf(paperCatOf(data.category)).map((c) => ({
|
|
47
|
+
label: c.label[lang],
|
|
48
|
+
href: getRelativeLocaleUrl(lang, `paper/category/${c.id}/`),
|
|
49
|
+
})),
|
|
50
|
+
];
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
<Breadcrumb segments={crumbs} />
|
|
54
|
+
|
|
55
|
+
<header class="mt-4 pb-6">
|
|
56
|
+
<h1 class="text-3xl font-bold tracking-tight">{data.title}</h1>
|
|
57
|
+
{
|
|
58
|
+
data.authors.length > 0 && (
|
|
59
|
+
<p class="mt-2 text-sm text-[var(--aas-muted)]">
|
|
60
|
+
<span class="font-semibold">{t('paper.authors')}:</span> {data.authors.join(', ')}
|
|
61
|
+
</p>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
<p class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[var(--aas-muted)]">
|
|
65
|
+
{data.venue && <span>{data.venue}</span>}
|
|
66
|
+
{data.year && <span>{data.year}</span>}
|
|
67
|
+
{
|
|
68
|
+
data.openSource && (
|
|
69
|
+
<span class="rounded-full border border-[var(--aas-accent)] px-2 py-0.5 text-xs text-[var(--aas-accent)]">
|
|
70
|
+
{t('paper.openSource')}
|
|
71
|
+
</span>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
{data.version && <span>{t('meta.docVersion')} {data.version}</span>}
|
|
75
|
+
{
|
|
76
|
+
data.updated && (
|
|
77
|
+
<span>
|
|
78
|
+
{t('paper.updated')} {formatDate(data.updated, lang)}
|
|
79
|
+
</span>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
</p>
|
|
83
|
+
{
|
|
84
|
+
links.length > 0 && (
|
|
85
|
+
<p class="mt-3 flex flex-wrap items-center gap-2">
|
|
86
|
+
{links.map((l) => (
|
|
87
|
+
<a
|
|
88
|
+
href={l.href}
|
|
89
|
+
target="_blank"
|
|
90
|
+
rel="noopener noreferrer"
|
|
91
|
+
class="rounded-full border border-[var(--aas-border)] bg-[var(--aas-panel)] px-3 py-1 text-sm text-[var(--aas-text)] no-underline hover:bg-[var(--aas-tint)]"
|
|
92
|
+
>
|
|
93
|
+
{l.label}
|
|
94
|
+
<ArrowUpRight size={14} class="ml-0.5 text-[var(--aas-muted)]" />
|
|
95
|
+
</a>
|
|
96
|
+
))}
|
|
97
|
+
</p>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
<p class="mt-2 aas-md text-lg whitespace-pre-line text-[var(--aas-muted)]" set:html={inlineMd(data.description)} />
|
|
101
|
+
{
|
|
102
|
+
data.image && (
|
|
103
|
+
<Image
|
|
104
|
+
src={data.image}
|
|
105
|
+
alt={data.imageAlt ?? data.title}
|
|
106
|
+
class="mt-5 aspect-[2/1] w-full rounded-2xl border border-[var(--aas-border)] object-cover"
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
</header>
|
|
111
|
+
|
|
112
|
+
<RelatedPapers lang={lang} papers={relatedPapers} />
|
|
113
|
+
|
|
114
|
+
<div class="mt-8 flex items-start gap-8">
|
|
115
|
+
<article class="prose max-w-none aas-reading min-w-0 flex-1">
|
|
116
|
+
<Content />
|
|
117
|
+
</article>
|
|
118
|
+
<TocRail lang={lang} items={tocItems} />
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<RelatedTools lang={lang} slugs={data.tools} />
|
|
122
|
+
|
|
123
|
+
{bodyHasMermaid && <MermaidLoader />}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
|
+
import PaperCard from './PaperCard.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 { paperTree, paperCatOf } from '@aas-data/paper-categories';
|
|
10
|
+
import { getPapers, paperSlugOf, type PaperEntry } from '../lib/papers';
|
|
11
|
+
import { listedInIndex } from '../lib/listing';
|
|
12
|
+
import { useTranslations, type Lang } from '../i18n/ui';
|
|
13
|
+
|
|
14
|
+
// Doubles as the papers index (no `categoryId` → all papers, grouped by
|
|
15
|
+
// category) and a category browse page (`categoryId` → that category, with a
|
|
16
|
+
// breadcrumb, its direct papers, and a section per subcategory).
|
|
17
|
+
interface Props {
|
|
18
|
+
lang: Lang;
|
|
19
|
+
categoryId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { lang, categoryId } = Astro.props;
|
|
23
|
+
const t = useTranslations(lang);
|
|
24
|
+
// getPapers() sorts newest publication year first, matching the client-side
|
|
25
|
+
// "recent" comparator over PaperCard's data-date (= year) — no reflow on load.
|
|
26
|
+
const all = (await getPapers(lang)).filter((e) => listedInIndex(e.data));
|
|
27
|
+
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `paper/category/${cid}/`);
|
|
28
|
+
|
|
29
|
+
const itemsIn = (cid: string): PaperEntry[] => {
|
|
30
|
+
const ids = new Set(paperTree.descendantIds(cid));
|
|
31
|
+
return all.filter((c) => ids.has(paperCatOf(c.data.category)));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const node = categoryId ? paperTree.map.get(categoryId) : undefined;
|
|
35
|
+
const sections = node ? paperTree.childrenOf(node.id) : paperTree.roots;
|
|
36
|
+
const direct = node ? all.filter((c) => paperCatOf(c.data.category) === node.id) : [];
|
|
37
|
+
|
|
38
|
+
// A leaf category page shows one flat list — that grid gets ?page= pagination.
|
|
39
|
+
// Index and parent-category pages stay grouped overviews: each section is
|
|
40
|
+
// capped to PAGE cards with a "see all" link to the fuller list.
|
|
41
|
+
const isLeaf = !!node && sections.length === 0;
|
|
42
|
+
|
|
43
|
+
const path = node ? paperTree.pathOf(node.id) : [];
|
|
44
|
+
const crumbs = node
|
|
45
|
+
? [
|
|
46
|
+
{ label: t('paper.backToPapers'), href: getRelativeLocaleUrl(lang, 'paper/') },
|
|
47
|
+
...path.map((c, i) => ({
|
|
48
|
+
label: c.label[lang],
|
|
49
|
+
href: i === path.length - 1 ? undefined : catUrl(c.id),
|
|
50
|
+
})),
|
|
51
|
+
]
|
|
52
|
+
: [];
|
|
53
|
+
|
|
54
|
+
const card = (c: PaperEntry) => ({
|
|
55
|
+
lang,
|
|
56
|
+
slug: paperSlugOf(c),
|
|
57
|
+
title: c.data.title,
|
|
58
|
+
description: c.data.description,
|
|
59
|
+
authors: c.data.authors,
|
|
60
|
+
year: c.data.year,
|
|
61
|
+
venue: c.data.venue,
|
|
62
|
+
openSource: c.data.openSource,
|
|
63
|
+
tags: c.data.tags,
|
|
64
|
+
image: c.data.image,
|
|
65
|
+
imageAlt: c.data.imageAlt,
|
|
66
|
+
isPrivate: c.data.private,
|
|
67
|
+
teaser: c.data.teaser,
|
|
68
|
+
});
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
node ? (
|
|
73
|
+
<>
|
|
74
|
+
<Breadcrumb segments={crumbs} />
|
|
75
|
+
<header class="mt-4">
|
|
76
|
+
<h1 class="text-3xl font-bold tracking-tight">{node.label[lang]}</h1>
|
|
77
|
+
<p class="mt-2 max-w-2xl text-lg text-[var(--aas-muted)]">
|
|
78
|
+
{node.detail?.[lang] ?? node.description[lang]}
|
|
79
|
+
</p>
|
|
80
|
+
</header>
|
|
81
|
+
</>
|
|
82
|
+
) : (
|
|
83
|
+
<section class="py-6">
|
|
84
|
+
<h1 class="text-4xl font-bold tracking-tight">{t('paper.title')}</h1>
|
|
85
|
+
<p class="mt-3 max-w-2xl text-lg text-[var(--aas-muted)]">{t('paper.tagline')}</p>
|
|
86
|
+
</section>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
{all.length === 0 && <p class="mt-8 text-[var(--aas-muted)]">{t('paper.empty')}</p>}
|
|
91
|
+
|
|
92
|
+
{all.length > 0 && <div class="mt-6"><ListControls lang={lang} /></div>}
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
direct.length > 0 && (
|
|
96
|
+
<div
|
|
97
|
+
class="mt-4 grid gap-5 sm:grid-cols-2 lg:grid-cols-3"
|
|
98
|
+
data-sortgrid
|
|
99
|
+
data-paginate={isLeaf ? true : undefined}
|
|
100
|
+
data-page-size={isLeaf ? PAGE : undefined}
|
|
101
|
+
>
|
|
102
|
+
{direct.map((c) => (
|
|
103
|
+
<PaperCard {...card(c)} />
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
{
|
|
110
|
+
sections.map((cat) => {
|
|
111
|
+
const items = itemsIn(cat.id);
|
|
112
|
+
return (
|
|
113
|
+
items.length > 0 && (
|
|
114
|
+
<section class="mt-8">
|
|
115
|
+
<h2 class="text-2xl font-semibold">
|
|
116
|
+
<a
|
|
117
|
+
href={catUrl(cat.id)}
|
|
118
|
+
class="text-[var(--aas-text)] no-underline hover:text-[var(--aas-accent)]"
|
|
119
|
+
>
|
|
120
|
+
{cat.label[lang]}
|
|
121
|
+
<ArrowUpRight size={20} class="ml-1 text-[var(--aas-muted)]" />
|
|
122
|
+
</a>
|
|
123
|
+
</h2>
|
|
124
|
+
<p class="mt-1 text-sm text-[var(--aas-muted)]">{cat.description[lang]}</p>
|
|
125
|
+
<div class="mt-4 grid gap-5 sm:grid-cols-2 lg:grid-cols-3" data-sortgrid>
|
|
126
|
+
{items.slice(0, PAGE).map((c) => (
|
|
127
|
+
<PaperCard {...card(c)} />
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
{items.length > PAGE && (
|
|
131
|
+
<a
|
|
132
|
+
href={catUrl(cat.id)}
|
|
133
|
+
class="mt-3 inline-block text-sm font-medium text-[var(--aas-accent)] no-underline"
|
|
134
|
+
>
|
|
135
|
+
{t('list.seeAll').replace('{n}', String(items.length))} →
|
|
136
|
+
</a>
|
|
137
|
+
)}
|
|
138
|
+
</section>
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
{isLeaf && <nav data-pager aria-label="Pagination" class="mt-8 flex flex-wrap items-center justify-center gap-1.5" />}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
8
8
|
import { productTree, productCatOf } from '@aas-data/product-categories';
|
|
9
9
|
import { getTopProducts, productSlugOf, type ProductEntry } from '../lib/products';
|
|
10
|
+
import { listedInIndex } from '../lib/listing';
|
|
10
11
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
@@ -15,7 +16,7 @@ interface Props {
|
|
|
15
16
|
|
|
16
17
|
const { lang } = Astro.props;
|
|
17
18
|
const t = useTranslations(lang);
|
|
18
|
-
const all = await getTopProducts(lang);
|
|
19
|
+
const all = (await getTopProducts(lang)).filter((e) => listedInIndex(e.data));
|
|
19
20
|
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
20
21
|
const withBase = (p?: string) => (p && p.startsWith('/') ? base + p : p);
|
|
21
22
|
|