stack-site-builder 1.19.0 → 1.19.2
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 +28 -0
- package/package.json +1 -1
- package/src/components/ArticleCard.astro +4 -1
- package/src/components/BlogIndex.astro +7 -2
- package/src/components/CategoryIndex.astro +5 -9
- package/src/components/ConceptCard.astro +4 -1
- package/src/components/ConceptIndex.astro +7 -2
- package/src/components/CourseCard.astro +4 -0
- package/src/components/CourseIndex.astro +7 -2
- package/src/components/Home.astro +3 -2
- package/src/components/MermaidLoader.astro +14 -1
- package/src/components/PaperCard.astro +4 -0
- package/src/components/PapersIndex.astro +7 -2
- package/src/components/ProductsIndex.astro +7 -2
- package/src/components/SlidesIndex.astro +10 -3
- package/src/components/StackCard.astro +4 -1
- package/src/components/TagIndex.astro +3 -2
- package/src/components/VendorIndex.astro +3 -2
- package/src/i18n/ui.ts +2 -0
- package/src/layouts/BaseLayout.astro +15 -15
- package/src/lib/listing.ts +10 -5
- package/src/styles/global.css +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,32 @@ 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.2] - 2026-07-23
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Dark mode made custom-styled diagrams unreadable** — authored
|
|
19
|
+
`style X fill:#…` lines carry light-mode colors, while Mermaid's HTML
|
|
20
|
+
labels take their text color from the theme variables; in dark mode that
|
|
21
|
+
paired light text with the hardcoded light fills. The loader now drops
|
|
22
|
+
fill styles when rendering in dark mode, so those diagrams follow the
|
|
23
|
+
theme palette there — light mode keeps the authored colors, and toggling
|
|
24
|
+
the theme re-renders accordingly.
|
|
25
|
+
|
|
26
|
+
## [1.19.1] - 2026-07-23
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **Member-only entries are login-aware, and the Slides nav is back** —
|
|
31
|
+
1.18.0 dropped unlisted private entries from listings for everyone; the
|
|
32
|
+
intended behavior is per-VIEWER: logged-out visitors don't see them,
|
|
33
|
+
logged-in members do. Listings now render member-only cards with
|
|
34
|
+
`data-aas-member-only`; a pre-paint script stamps `html[data-aas-member]`
|
|
35
|
+
when a session exists and CSS does the rest — no flash either way. A
|
|
36
|
+
"shows after login" hint line replaces the invisible content for
|
|
37
|
+
logged-out visitors, and 1.19.0's listed-deck nav gating is reverted (the
|
|
38
|
+
Slides item follows the section toggle again).
|
|
39
|
+
|
|
14
40
|
## [1.19.0] - 2026-07-23
|
|
15
41
|
|
|
16
42
|
### Added
|
|
@@ -319,6 +345,8 @@ catalog sites from a thin content-only repository.
|
|
|
319
345
|
- **Standalone development setup** — a devcontainer and a minimal `playground/`
|
|
320
346
|
consuming site for developing and previewing the theme on its own.
|
|
321
347
|
|
|
348
|
+
[1.19.2]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.19.1...v1.19.2
|
|
349
|
+
[1.19.1]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.19.0...v1.19.1
|
|
322
350
|
[1.19.0]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.18.0...v1.19.0
|
|
323
351
|
[1.18.0]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.4...v1.18.0
|
|
324
352
|
[1.17.4]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.3...v1.17.4
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stack-site-builder",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.19.
|
|
4
|
+
"version": "1.19.2",
|
|
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": {
|
|
@@ -19,9 +19,11 @@ interface Props {
|
|
|
19
19
|
/** Private entry: show a lock and the (public) `teaser` instead of the description. */
|
|
20
20
|
isPrivate?: boolean;
|
|
21
21
|
teaser?: string;
|
|
22
|
+
/** Hidden from logged-out visitors (private without listed). */
|
|
23
|
+
memberOnly?: boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
const { slug, lang, title, description, date, tags, image, imageAlt, version, isPrivate = false, teaser } = Astro.props;
|
|
26
|
+
const { slug, lang, title, description, date, tags, image, imageAlt, version, isPrivate = false, teaser, memberOnly = false } = Astro.props;
|
|
25
27
|
const t = useTranslations(lang);
|
|
26
28
|
const shownDesc = isPrivate ? (teaser ?? '') : description;
|
|
27
29
|
const href = getRelativeLocaleUrl(lang, `article/${slug}/`);
|
|
@@ -31,6 +33,7 @@ const dateStr = date.toISOString().slice(0, 10);
|
|
|
31
33
|
<a
|
|
32
34
|
href={href}
|
|
33
35
|
data-name={title.toLowerCase()}
|
|
36
|
+
data-aas-member-only={memberOnly ? '' : undefined}
|
|
34
37
|
data-date={date.toISOString()}
|
|
35
38
|
class="aas-lift flex items-stretch overflow-hidden rounded-2xl border border-[var(--aas-border)] bg-[var(--aas-panel)] no-underline"
|
|
36
39
|
>
|
|
@@ -8,7 +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 {
|
|
11
|
+
import { memberOnlyInIndex } from '../lib/listing';
|
|
12
12
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
13
13
|
|
|
14
14
|
// Doubles as the writing index (no `categoryId` → all articles, grouped by
|
|
@@ -21,7 +21,8 @@ interface Props {
|
|
|
21
21
|
|
|
22
22
|
const { lang, categoryId } = Astro.props;
|
|
23
23
|
const t = useTranslations(lang);
|
|
24
|
-
const all =
|
|
24
|
+
const all = await getArticles(lang);
|
|
25
|
+
const hasMemberOnly = all.some((e) => memberOnlyInIndex(e.data));
|
|
25
26
|
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `article/category/${cid}/`);
|
|
26
27
|
|
|
27
28
|
const itemsIn = (cid: string): ArticleEntry[] => {
|
|
@@ -61,6 +62,7 @@ const card = (a: ArticleEntry) => ({
|
|
|
61
62
|
imageAlt: a.data.imageAlt,
|
|
62
63
|
isPrivate: a.data.private,
|
|
63
64
|
teaser: a.data.teaser,
|
|
65
|
+
memberOnly: memberOnlyInIndex(a.data),
|
|
64
66
|
});
|
|
65
67
|
---
|
|
66
68
|
|
|
@@ -84,6 +86,9 @@ const card = (a: ArticleEntry) => ({
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
{all.length === 0 && <p class="mt-8 text-[var(--aas-muted)]">{t('blog.empty')}</p>}
|
|
89
|
+
{hasMemberOnly && (
|
|
90
|
+
<p data-aas-member-hint class="mt-4 text-sm text-[var(--aas-muted)]">{t('private.memberHint')}</p>
|
|
91
|
+
)}
|
|
87
92
|
|
|
88
93
|
{all.length > 0 && <div class="mt-6"><ListControls lang={lang} /></div>}
|
|
89
94
|
|
|
@@ -7,7 +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 {
|
|
10
|
+
import { memberOnlyInIndex } from '../lib/listing';
|
|
11
11
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
@@ -32,23 +32,19 @@ const crumbs = [
|
|
|
32
32
|
|
|
33
33
|
const children = childrenOf(id);
|
|
34
34
|
// Stacks attached directly to this node (vs. nested under a subcategory).
|
|
35
|
-
const direct = (await getStacksByCategory(lang, id)).filter(
|
|
36
|
-
(s) => s.data.category === id && listedInIndex(s.data),
|
|
37
|
-
);
|
|
35
|
+
const direct = (await getStacksByCategory(lang, id)).filter((s) => s.data.category === id);
|
|
38
36
|
// Each child subcategory with its own rolled-up stacks.
|
|
39
37
|
const childGroups = await Promise.all(
|
|
40
|
-
children.map(async (c) => ({
|
|
41
|
-
cat: c,
|
|
42
|
-
items: (await getStacksByCategory(lang, c.id)).filter((s) => listedInIndex(s.data)),
|
|
43
|
-
})),
|
|
38
|
+
children.map(async (c) => ({ cat: c, items: await getStacksByCategory(lang, c.id) })),
|
|
44
39
|
);
|
|
45
40
|
|
|
46
41
|
// Filter facets are built from the WHOLE catalog (not just this page's tools) so
|
|
47
42
|
// they're identical on every page — a sort/filter carried in from elsewhere can
|
|
48
43
|
// always be restored here, even if no tool in this category has that value.
|
|
49
|
-
const facets = deriveFacets(
|
|
44
|
+
const facets = deriveFacets(await getStacks(lang), lang);
|
|
50
45
|
|
|
51
46
|
const cardProps = (e: StackEntry) => ({
|
|
47
|
+
memberOnly: memberOnlyInIndex(e.data),
|
|
52
48
|
slug: slugOf(e),
|
|
53
49
|
lang,
|
|
54
50
|
name: e.data.name,
|
|
@@ -19,9 +19,11 @@ interface Props {
|
|
|
19
19
|
/** Private entry: show a lock and the (public) `teaser` instead of the description. */
|
|
20
20
|
isPrivate?: boolean;
|
|
21
21
|
teaser?: string;
|
|
22
|
+
/** Hidden from logged-out visitors (private without listed). */
|
|
23
|
+
memberOnly?: boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
const { lang, slug, title, description, tags, image, imageAlt, version, updated, isPrivate = false, teaser } = Astro.props;
|
|
26
|
+
const { lang, slug, title, description, tags, image, imageAlt, version, updated, isPrivate = false, teaser, memberOnly = false } = Astro.props;
|
|
25
27
|
const t = useTranslations(lang);
|
|
26
28
|
const shownDesc = isPrivate ? (teaser ?? '') : description;
|
|
27
29
|
---
|
|
@@ -29,6 +31,7 @@ const shownDesc = isPrivate ? (teaser ?? '') : description;
|
|
|
29
31
|
<a
|
|
30
32
|
href={getRelativeLocaleUrl(lang, `concept/${slug}/`)}
|
|
31
33
|
data-name={title.toLowerCase()}
|
|
34
|
+
data-aas-member-only={memberOnly ? '' : undefined}
|
|
32
35
|
data-date={updated ? updated.toISOString() : ''}
|
|
33
36
|
class="aas-lift block overflow-hidden rounded-3xl border border-[var(--aas-border)] bg-[var(--aas-panel)] no-underline"
|
|
34
37
|
>
|
|
@@ -8,7 +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 {
|
|
11
|
+
import { memberOnlyInIndex } from '../lib/listing';
|
|
12
12
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
13
13
|
|
|
14
14
|
// Doubles as the concept index (no `categoryId` → all concepts, grouped by
|
|
@@ -23,11 +23,12 @@ const { lang, categoryId } = Astro.props;
|
|
|
23
23
|
const t = useTranslations(lang);
|
|
24
24
|
// Default order is most-recently-updated first (matches the ListControls
|
|
25
25
|
// default), so the server render and the client sort agree — no reflow on load.
|
|
26
|
-
const all = (await getConcepts(lang)).
|
|
26
|
+
const all = (await getConcepts(lang)).sort(
|
|
27
27
|
(a, b) =>
|
|
28
28
|
(b.data.updated?.getTime() ?? 0) - (a.data.updated?.getTime() ?? 0) ||
|
|
29
29
|
a.data.title.localeCompare(b.data.title),
|
|
30
30
|
);
|
|
31
|
+
const hasMemberOnly = all.some((e) => memberOnlyInIndex(e.data));
|
|
31
32
|
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `concept/category/${cid}/`);
|
|
32
33
|
|
|
33
34
|
const itemsIn = (cid: string): ConceptEntry[] => {
|
|
@@ -67,6 +68,7 @@ const card = (c: ConceptEntry) => ({
|
|
|
67
68
|
updated: c.data.updated,
|
|
68
69
|
isPrivate: c.data.private,
|
|
69
70
|
teaser: c.data.teaser,
|
|
71
|
+
memberOnly: memberOnlyInIndex(c.data),
|
|
70
72
|
});
|
|
71
73
|
---
|
|
72
74
|
|
|
@@ -90,6 +92,9 @@ const card = (c: ConceptEntry) => ({
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
{all.length === 0 && <p class="mt-8 text-[var(--aas-muted)]">{t('concept.empty')}</p>}
|
|
95
|
+
{hasMemberOnly && (
|
|
96
|
+
<p data-aas-member-hint class="mt-4 text-sm text-[var(--aas-muted)]">{t('private.memberHint')}</p>
|
|
97
|
+
)}
|
|
93
98
|
|
|
94
99
|
{all.length > 0 && <div class="mt-6"><ListControls lang={lang} /></div>}
|
|
95
100
|
|
|
@@ -25,6 +25,8 @@ interface Props {
|
|
|
25
25
|
/** Private entry: show a lock and the (public) `teaser` instead of the description. */
|
|
26
26
|
isPrivate?: boolean;
|
|
27
27
|
teaser?: string;
|
|
28
|
+
/** Hidden from logged-out visitors (private without listed). */
|
|
29
|
+
memberOnly?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const {
|
|
@@ -43,6 +45,7 @@ const {
|
|
|
43
45
|
order,
|
|
44
46
|
isPrivate = false,
|
|
45
47
|
teaser,
|
|
48
|
+
memberOnly = false,
|
|
46
49
|
} = Astro.props;
|
|
47
50
|
const t = useTranslations(lang);
|
|
48
51
|
const shownDesc = isPrivate ? (teaser ?? '') : description;
|
|
@@ -57,6 +60,7 @@ const shownDate = updated ?? date;
|
|
|
57
60
|
<a
|
|
58
61
|
href={getRelativeLocaleUrl(lang, `course/${slug}/`)}
|
|
59
62
|
data-name={title.toLowerCase()}
|
|
63
|
+
data-aas-member-only={memberOnly ? '' : undefined}
|
|
60
64
|
data-date={sortKey}
|
|
61
65
|
class="aas-lift block overflow-hidden rounded-3xl border border-[var(--aas-border)] bg-[var(--aas-panel)] no-underline"
|
|
62
66
|
>
|
|
@@ -8,7 +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 {
|
|
11
|
+
import { memberOnlyInIndex } from '../lib/listing';
|
|
12
12
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
13
13
|
|
|
14
14
|
// Doubles as the course index (no `categoryId` → all courses, grouped by
|
|
@@ -23,7 +23,8 @@ const { lang, categoryId } = Astro.props;
|
|
|
23
23
|
const t = useTranslations(lang);
|
|
24
24
|
// getCourses() order (manual `order` desc, then date) agrees with the client
|
|
25
25
|
// "recent" comparator over CourseCard's data-date keys — no reflow on load.
|
|
26
|
-
const all =
|
|
26
|
+
const all = await getCourses(lang);
|
|
27
|
+
const hasMemberOnly = all.some((e) => memberOnlyInIndex(e.data));
|
|
27
28
|
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `course/category/${cid}/`);
|
|
28
29
|
|
|
29
30
|
const itemsIn = (cid: string): CourseEntry[] => {
|
|
@@ -67,6 +68,7 @@ const card = (c: CourseEntry) => ({
|
|
|
67
68
|
order: c.data.order,
|
|
68
69
|
isPrivate: c.data.private,
|
|
69
70
|
teaser: c.data.teaser,
|
|
71
|
+
memberOnly: memberOnlyInIndex(c.data),
|
|
70
72
|
});
|
|
71
73
|
---
|
|
72
74
|
|
|
@@ -90,6 +92,9 @@ const card = (c: CourseEntry) => ({
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
{all.length === 0 && <p class="mt-8 text-[var(--aas-muted)]">{t('course.empty')}</p>}
|
|
95
|
+
{hasMemberOnly && (
|
|
96
|
+
<p data-aas-member-hint class="mt-4 text-sm text-[var(--aas-muted)]">{t('private.memberHint')}</p>
|
|
97
|
+
)}
|
|
93
98
|
|
|
94
99
|
{all.length > 0 && <div class="mt-6"><ListControls lang={lang} /></div>}
|
|
95
100
|
|
|
@@ -7,7 +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 {
|
|
10
|
+
import { memberOnlyInIndex } from '../lib/listing';
|
|
11
11
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
@@ -16,7 +16,7 @@ interface Props {
|
|
|
16
16
|
|
|
17
17
|
const { lang } = Astro.props;
|
|
18
18
|
const t = useTranslations(lang);
|
|
19
|
-
const entries =
|
|
19
|
+
const entries = await getStacks(lang);
|
|
20
20
|
|
|
21
21
|
// Default order is alphabetical (matches the client's default sort, so no flash).
|
|
22
22
|
// Group by top-level category, rolling up every subcategory in its subtree.
|
|
@@ -65,6 +65,7 @@ const facets = deriveFacets(entries, lang);
|
|
|
65
65
|
<div class="mt-5 grid gap-4 sm:grid-cols-2" data-grid>
|
|
66
66
|
{items.map((e) => (
|
|
67
67
|
<StackCard
|
|
68
|
+
memberOnly={memberOnlyInIndex(e.data)}
|
|
68
69
|
slug={slugOf(e)}
|
|
69
70
|
lang={lang}
|
|
70
71
|
name={e.data.name}
|
|
@@ -56,6 +56,19 @@
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Authored `style X fill:#…` lines carry light-mode colors, and Mermaid's
|
|
60
|
+
// HTML labels take their text color from the THEME variables — in dark mode
|
|
61
|
+
// that pairs light text with the hardcoded light fills, unreadable. So in
|
|
62
|
+
// dark mode we drop those fill styles and let the diagram render entirely
|
|
63
|
+
// from the theme palette; light mode keeps the authored colors.
|
|
64
|
+
function srcForTheme(src: string): string {
|
|
65
|
+
if (document.documentElement.dataset.theme !== 'dark') return src;
|
|
66
|
+
return src
|
|
67
|
+
.split('\n')
|
|
68
|
+
.filter((l) => !/^\s*style\s+\S+.*fill:#/.test(l))
|
|
69
|
+
.join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
async function renderAll() {
|
|
60
73
|
const nodes = Array.from(document.querySelectorAll<HTMLElement>('pre.mermaid'));
|
|
61
74
|
if (nodes.length === 0) return;
|
|
@@ -66,7 +79,7 @@
|
|
|
66
79
|
// injecting it as HTML would turn `<…>` in labels into live DOM (the
|
|
67
80
|
// server-side escaping exists precisely to prevent that). Mermaid reads
|
|
68
81
|
// the element's innerHTML and decodes entities itself.
|
|
69
|
-
n.textContent = n.dataset.src ?? '';
|
|
82
|
+
n.textContent = srcForTheme(n.dataset.src ?? '');
|
|
70
83
|
}
|
|
71
84
|
init();
|
|
72
85
|
try {
|
|
@@ -21,6 +21,8 @@ interface Props {
|
|
|
21
21
|
/** Private entry: show a lock and the (public) `teaser` instead of the description. */
|
|
22
22
|
isPrivate?: boolean;
|
|
23
23
|
teaser?: string;
|
|
24
|
+
/** Hidden from logged-out visitors (private without listed). */
|
|
25
|
+
memberOnly?: boolean;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const {
|
|
@@ -37,6 +39,7 @@ const {
|
|
|
37
39
|
imageAlt,
|
|
38
40
|
isPrivate = false,
|
|
39
41
|
teaser,
|
|
42
|
+
memberOnly = false,
|
|
40
43
|
} = Astro.props;
|
|
41
44
|
const t = useTranslations(lang);
|
|
42
45
|
const shownDesc = isPrivate ? (teaser ?? '') : description;
|
|
@@ -45,6 +48,7 @@ const shownDesc = isPrivate ? (teaser ?? '') : description;
|
|
|
45
48
|
<a
|
|
46
49
|
href={getRelativeLocaleUrl(lang, `paper/${slug}/`)}
|
|
47
50
|
data-name={title.toLowerCase()}
|
|
51
|
+
data-aas-member-only={memberOnly ? '' : undefined}
|
|
48
52
|
data-date={year ? String(year) : ''}
|
|
49
53
|
class="aas-lift block overflow-hidden rounded-3xl border border-[var(--aas-border)] bg-[var(--aas-panel)] no-underline"
|
|
50
54
|
>
|
|
@@ -8,7 +8,7 @@ import ArrowUpRight from './ArrowUpRight.astro';
|
|
|
8
8
|
const PAGE = 12;
|
|
9
9
|
import { paperTree, paperCatOf } from '@aas-data/paper-categories';
|
|
10
10
|
import { getPapers, paperSlugOf, type PaperEntry } from '../lib/papers';
|
|
11
|
-
import {
|
|
11
|
+
import { memberOnlyInIndex } from '../lib/listing';
|
|
12
12
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
13
13
|
|
|
14
14
|
// Doubles as the papers index (no `categoryId` → all papers, grouped by
|
|
@@ -23,7 +23,8 @@ const { lang, categoryId } = Astro.props;
|
|
|
23
23
|
const t = useTranslations(lang);
|
|
24
24
|
// getPapers() sorts newest publication year first, matching the client-side
|
|
25
25
|
// "recent" comparator over PaperCard's data-date (= year) — no reflow on load.
|
|
26
|
-
const all =
|
|
26
|
+
const all = await getPapers(lang);
|
|
27
|
+
const hasMemberOnly = all.some((e) => memberOnlyInIndex(e.data));
|
|
27
28
|
const catUrl = (cid: string) => getRelativeLocaleUrl(lang, `paper/category/${cid}/`);
|
|
28
29
|
|
|
29
30
|
const itemsIn = (cid: string): PaperEntry[] => {
|
|
@@ -65,6 +66,7 @@ const card = (c: PaperEntry) => ({
|
|
|
65
66
|
imageAlt: c.data.imageAlt,
|
|
66
67
|
isPrivate: c.data.private,
|
|
67
68
|
teaser: c.data.teaser,
|
|
69
|
+
memberOnly: memberOnlyInIndex(c.data),
|
|
68
70
|
});
|
|
69
71
|
---
|
|
70
72
|
|
|
@@ -88,6 +90,9 @@ const card = (c: PaperEntry) => ({
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
{all.length === 0 && <p class="mt-8 text-[var(--aas-muted)]">{t('paper.empty')}</p>}
|
|
93
|
+
{hasMemberOnly && (
|
|
94
|
+
<p data-aas-member-hint class="mt-4 text-sm text-[var(--aas-muted)]">{t('private.memberHint')}</p>
|
|
95
|
+
)}
|
|
91
96
|
|
|
92
97
|
{all.length > 0 && <div class="mt-6"><ListControls lang={lang} /></div>}
|
|
93
98
|
|
|
@@ -7,7 +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 {
|
|
10
|
+
import { memberOnlyInIndex } from '../lib/listing';
|
|
11
11
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
@@ -16,7 +16,8 @@ interface Props {
|
|
|
16
16
|
|
|
17
17
|
const { lang } = Astro.props;
|
|
18
18
|
const t = useTranslations(lang);
|
|
19
|
-
const all =
|
|
19
|
+
const all = await getTopProducts(lang);
|
|
20
|
+
const hasMemberOnly = all.some((e) => memberOnlyInIndex(e.data));
|
|
20
21
|
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
21
22
|
const withBase = (p?: string) => (p && p.startsWith('/') ? base + p : p);
|
|
22
23
|
|
|
@@ -36,6 +37,9 @@ const sections = productTree.roots
|
|
|
36
37
|
</section>
|
|
37
38
|
|
|
38
39
|
{all.length === 0 && <p class="mt-8 text-[var(--aas-muted)]">{t('products.empty')}</p>}
|
|
40
|
+
{hasMemberOnly && (
|
|
41
|
+
<p data-aas-member-hint class="mt-4 text-sm text-[var(--aas-muted)]">{t('private.memberHint')}</p>
|
|
42
|
+
)}
|
|
39
43
|
|
|
40
44
|
{
|
|
41
45
|
sections.map(({ cat, items }) => (
|
|
@@ -46,6 +50,7 @@ const sections = productTree.roots
|
|
|
46
50
|
{items.map((a: ProductEntry) => (
|
|
47
51
|
<a
|
|
48
52
|
href={getRelativeLocaleUrl(lang, `products/${productSlugOf(a)}/`)}
|
|
53
|
+
data-aas-member-only={memberOnlyInIndex(a.data) ? '' : undefined}
|
|
49
54
|
class="aas-lift flex items-center gap-6 rounded-3xl border border-[var(--aas-border)] bg-[var(--aas-panel)] p-6 no-underline"
|
|
50
55
|
>
|
|
51
56
|
{a.data.icon && (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
|
-
import {
|
|
3
|
+
import { memberOnlyInIndex } from '../lib/listing';
|
|
4
4
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
5
5
|
import { getDecks, deckSlugOf } from '../lib/slides';
|
|
6
6
|
|
|
@@ -13,18 +13,25 @@ const t = useTranslations(lang);
|
|
|
13
13
|
|
|
14
14
|
// Decks are locale-partitioned (content/slides/<lang>/); list this locale's and
|
|
15
15
|
// link to its localized deck page. `related` points at the concept it summarizes.
|
|
16
|
-
const decks =
|
|
16
|
+
const decks = await getDecks(lang);
|
|
17
|
+
const hasMemberOnly = decks.some((e) => memberOnlyInIndex(e.data));
|
|
17
18
|
---
|
|
18
19
|
|
|
19
20
|
<section class="py-6">
|
|
20
21
|
<h1 class="text-4xl font-bold tracking-tight">{t('slides.title')}</h1>
|
|
21
22
|
<p class="mt-3 max-w-2xl text-lg text-[var(--aas-muted)]">{t('slides.tagline')}</p>
|
|
22
23
|
</section>
|
|
24
|
+
{hasMemberOnly && (
|
|
25
|
+
<p data-aas-member-hint class="mt-4 text-sm text-[var(--aas-muted)]">{t('private.memberHint')}</p>
|
|
26
|
+
)}
|
|
23
27
|
|
|
24
28
|
<div class="mt-6 grid gap-5 sm:grid-cols-2">
|
|
25
29
|
{
|
|
26
30
|
decks.map((deck) => (
|
|
27
|
-
<div
|
|
31
|
+
<div
|
|
32
|
+
class="overflow-hidden rounded-3xl border border-[var(--aas-border)] bg-[var(--aas-panel)]"
|
|
33
|
+
data-aas-member-only={memberOnlyInIndex(deck.data) ? '' : undefined}
|
|
34
|
+
>
|
|
28
35
|
<a
|
|
29
36
|
href={getRelativeLocaleUrl(lang, `slides/${deckSlugOf(deck)}/`)}
|
|
30
37
|
class="aas-lift block p-5 no-underline"
|
|
@@ -29,9 +29,11 @@ interface Props {
|
|
|
29
29
|
/** Private entry: show a lock and the (public) `teaser` instead of the description. */
|
|
30
30
|
isPrivate?: boolean;
|
|
31
31
|
teaser?: string;
|
|
32
|
+
/** Hidden from logged-out visitors (private without listed). */
|
|
33
|
+
memberOnly?: boolean;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
const { slug, lang, name, formerNames = [], vendor, description, tags, pricing, license, language, category, logo, logoDark, repo, version, deprecated, isPrivate = false, teaser } =
|
|
36
|
+
const { slug, lang, name, formerNames = [], vendor, description, tags, pricing, license, language, category, logo, logoDark, repo, version, deprecated, isPrivate = false, teaser, memberOnly = false } =
|
|
35
37
|
Astro.props;
|
|
36
38
|
const t = useTranslations(lang);
|
|
37
39
|
const href = getRelativeLocaleUrl(lang, `stack/${slug}/`);
|
|
@@ -70,6 +72,7 @@ const searchText = `${name} ${shownDesc} ${tags.join(' ')}`.toLowerCase();
|
|
|
70
72
|
<div
|
|
71
73
|
class="aas-lift relative isolate flex flex-col gap-3 overflow-hidden rounded-3xl border border-[var(--aas-border)] bg-[var(--aas-panel)] p-5 min-[1000px]:flex-row"
|
|
72
74
|
data-tool
|
|
75
|
+
data-aas-member-only={memberOnly ? '' : undefined}
|
|
73
76
|
data-name={name}
|
|
74
77
|
data-search={searchText}
|
|
75
78
|
data-pricing={pricingTags(pricing).join('|')}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
3
|
import StackCard from './StackCard.astro';
|
|
4
4
|
import { getStacksByTag, slugOf } from '../lib/stacks';
|
|
5
|
-
import {
|
|
5
|
+
import { memberOnlyInIndex } from '../lib/listing';
|
|
6
6
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
@@ -12,7 +12,7 @@ interface Props {
|
|
|
12
12
|
|
|
13
13
|
const { lang, tag } = Astro.props;
|
|
14
14
|
const t = useTranslations(lang);
|
|
15
|
-
const entries = (await getStacksByTag(lang, tag)).
|
|
15
|
+
const entries = (await getStacksByTag(lang, tag)).sort((a, b) =>
|
|
16
16
|
a.data.name.localeCompare(b.data.name),
|
|
17
17
|
);
|
|
18
18
|
---
|
|
@@ -30,6 +30,7 @@ const entries = (await getStacksByTag(lang, tag)).filter((e) => listedInIndex(e.
|
|
|
30
30
|
{
|
|
31
31
|
entries.map((e) => (
|
|
32
32
|
<StackCard
|
|
33
|
+
memberOnly={memberOnlyInIndex(e.data)}
|
|
33
34
|
slug={slugOf(e)}
|
|
34
35
|
lang={lang}
|
|
35
36
|
name={e.data.name}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
3
3
|
import StackCard from './StackCard.astro';
|
|
4
4
|
import { getStacksByVendor, slugOf } from '../lib/stacks';
|
|
5
|
-
import {
|
|
5
|
+
import { memberOnlyInIndex } from '../lib/listing';
|
|
6
6
|
import { useTranslations, type Lang } from '../i18n/ui';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
@@ -12,7 +12,7 @@ interface Props {
|
|
|
12
12
|
|
|
13
13
|
const { lang, vendor } = Astro.props;
|
|
14
14
|
const t = useTranslations(lang);
|
|
15
|
-
const entries =
|
|
15
|
+
const entries = await getStacksByVendor(lang, vendor);
|
|
16
16
|
// The display name is whatever the entries wrote (e.g. "Hugging Face").
|
|
17
17
|
const vendorName = entries[0]?.data.vendor ?? vendor;
|
|
18
18
|
---
|
|
@@ -30,6 +30,7 @@ const vendorName = entries[0]?.data.vendor ?? vendor;
|
|
|
30
30
|
{
|
|
31
31
|
entries.map((e) => (
|
|
32
32
|
<StackCard
|
|
33
|
+
memberOnly={memberOnlyInIndex(e.data)}
|
|
33
34
|
slug={slugOf(e)}
|
|
34
35
|
lang={lang}
|
|
35
36
|
name={e.data.name}
|
package/src/i18n/ui.ts
CHANGED
|
@@ -72,6 +72,7 @@ export const ui = {
|
|
|
72
72
|
'code.copied': 'Copied',
|
|
73
73
|
'private.badge': 'Private',
|
|
74
74
|
'private.login': 'Log in',
|
|
75
|
+
'private.memberHint': '\ud83d\udd12 Some content appears after logging in.',
|
|
75
76
|
'private.locked': 'This content is private. Log in to view it.',
|
|
76
77
|
'private.id': 'ID',
|
|
77
78
|
'private.password': 'Password',
|
|
@@ -249,6 +250,7 @@ export const ui = {
|
|
|
249
250
|
'code.copied': '복사됨',
|
|
250
251
|
'private.badge': '비공개',
|
|
251
252
|
'private.login': '로그인',
|
|
253
|
+
'private.memberHint': '\ud83d\udd12 일부 콘텐츠는 로그인 후에 표시됩니다.',
|
|
252
254
|
'private.locked': '비공개 콘텐츠입니다. 로그인 후 볼 수 있습니다.',
|
|
253
255
|
'private.id': '아이디',
|
|
254
256
|
'private.password': '비밀번호',
|
|
@@ -8,8 +8,6 @@ import ThemeToggle from '../components/ThemeToggle.astro';
|
|
|
8
8
|
import BackToTop from '../components/BackToTop.astro';
|
|
9
9
|
import { getNavPages, pageSlugOf } from '../lib/pages';
|
|
10
10
|
import { getNavProducts, getTopProducts, productSlugOf } from '../lib/products';
|
|
11
|
-
import { getDecks } from '../lib/slides';
|
|
12
|
-
import { listedInIndex } from '../lib/listing';
|
|
13
11
|
import { homeTemplate } from '../lib/home';
|
|
14
12
|
import { sectionEnabled, type SectionKey } from '../lib/sections';
|
|
15
13
|
import { siteIcons } from '../lib/icons';
|
|
@@ -52,9 +50,6 @@ const navPages = await getNavPages(lang);
|
|
|
52
50
|
// the Products-index link, which appears once the locale has any product.
|
|
53
51
|
const navProducts = await getNavProducts(lang);
|
|
54
52
|
const hasProducts = (await getTopProducts(lang)).length > 0;
|
|
55
|
-
// The slides nav item only makes sense when the index actually lists decks —
|
|
56
|
-
// a site whose decks are all private+unlisted reaches them from course pages.
|
|
57
|
-
const hasListedDecks = (await getDecks(lang)).some((d) => listedInIndex(d.data));
|
|
58
53
|
// The header's GitHub link — hidden for sites whose repo is private
|
|
59
54
|
// (`repoNav: false` in site.ts) or that declare no repoUrl at all.
|
|
60
55
|
const showRepoLink =
|
|
@@ -124,16 +119,12 @@ const allNavItems: {
|
|
|
124
119
|
section: 'samples',
|
|
125
120
|
svg: '<polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/>',
|
|
126
121
|
},
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
svg: '<path d="M2 3h20"/><path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"/><path d="m7 21 5-5 5 5"/><path d="M12 12v9"/>',
|
|
134
|
-
},
|
|
135
|
-
]
|
|
136
|
-
: []),
|
|
122
|
+
{
|
|
123
|
+
href: getRelativeLocaleUrl(lang, 'slides/'),
|
|
124
|
+
label: t('nav.slides'),
|
|
125
|
+
section: 'slides',
|
|
126
|
+
svg: '<path d="M2 3h20"/><path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"/><path d="m7 21 5-5 5 5"/><path d="M12 12v9"/>',
|
|
127
|
+
},
|
|
137
128
|
{
|
|
138
129
|
href: getRelativeLocaleUrl(lang, 'glossary/'),
|
|
139
130
|
label: t('nav.glossary'),
|
|
@@ -228,6 +219,15 @@ const navItems = allNavItems.filter((item) => !item.section || sectionEnabled(it
|
|
|
228
219
|
} catch (e) {}
|
|
229
220
|
})();
|
|
230
221
|
</script>
|
|
222
|
+
{/* Stamp html[data-aas-member] before paint when a login session exists,
|
|
223
|
+
so member-only listing cards render without a flash (CSS keys off it). */}
|
|
224
|
+
<script is:inline>
|
|
225
|
+
(function () {
|
|
226
|
+
try {
|
|
227
|
+
if (localStorage.getItem('aas:pk')) document.documentElement.dataset.aasMember = '1';
|
|
228
|
+
} catch (e) {}
|
|
229
|
+
})();
|
|
230
|
+
</script>
|
|
231
231
|
{/* Listing density (standard / gallery), persisted in localStorage. Gallery
|
|
232
232
|
is the default; only an explicit 'grid' choice falls back to standard.
|
|
233
233
|
Set before paint so the layout doesn't flash. */}
|
package/src/lib/listing.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Listing visibility for private entries. A private entry without
|
|
3
|
+
* `listed: true` is "member-only": it still renders in index listings but
|
|
4
|
+
* carries `data-aas-member-only`, and CSS hides it unless the pre-paint
|
|
5
|
+
* script stamped `html[data-aas-member]` (a login session exists). So
|
|
6
|
+
* logged-out visitors don't see it, members do — with no flash either way.
|
|
7
|
+
* `listed: true` shows the locked teaser card to everyone.
|
|
7
8
|
*/
|
|
9
|
+
export const memberOnlyInIndex = (d: { private?: boolean; listed?: boolean }): boolean =>
|
|
10
|
+
d.private === true && d.listed !== true;
|
|
11
|
+
|
|
12
|
+
/** Kept for compatibility: whether an entry is visible to logged-out visitors. */
|
|
8
13
|
export const listedInIndex = (d: { private?: boolean; listed?: boolean }): boolean =>
|
|
9
14
|
!d.private || d.listed === true;
|
package/src/styles/global.css
CHANGED
|
@@ -2392,3 +2392,13 @@ summary {
|
|
|
2392
2392
|
.aas-slide :where(table) :where(th, td):first-child {
|
|
2393
2393
|
white-space: nowrap;
|
|
2394
2394
|
}
|
|
2395
|
+
|
|
2396
|
+
/* Member-only listing cards (private without `listed: true`): rendered for
|
|
2397
|
+
everyone, hidden unless the pre-paint script stamped html[data-aas-member]
|
|
2398
|
+
(a login session exists). The hint line inverts: visible only logged-out. */
|
|
2399
|
+
html:not([data-aas-member]) [data-aas-member-only] {
|
|
2400
|
+
display: none;
|
|
2401
|
+
}
|
|
2402
|
+
html[data-aas-member] [data-aas-member-hint] {
|
|
2403
|
+
display: none;
|
|
2404
|
+
}
|