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 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.0",
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 { listedInIndex } from '../lib/listing';
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 = (await getArticles(lang)).filter((e) => listedInIndex(e.data));
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 { listedInIndex } from '../lib/listing';
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((await getStacks(lang)).filter((e) => listedInIndex(e.data)), lang);
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 { listedInIndex } from '../lib/listing';
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)).filter((e) => listedInIndex(e.data)).sort(
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 { listedInIndex } from '../lib/listing';
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 = (await getCourses(lang)).filter((e) => listedInIndex(e.data));
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 { listedInIndex } from '../lib/listing';
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 = (await getStacks(lang)).filter((e) => listedInIndex(e.data));
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 { listedInIndex } from '../lib/listing';
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 = (await getPapers(lang)).filter((e) => listedInIndex(e.data));
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 { listedInIndex } from '../lib/listing';
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 = (await getTopProducts(lang)).filter((e) => listedInIndex(e.data));
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 { listedInIndex } from '../lib/listing';
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 = (await getDecks(lang)).filter((e) => listedInIndex(e.data));
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 class="overflow-hidden rounded-3xl border border-[var(--aas-border)] bg-[var(--aas-panel)]">
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 { listedInIndex } from '../lib/listing';
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)).filter((e) => listedInIndex(e.data)).sort((a, b) =>
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 { listedInIndex } from '../lib/listing';
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 = (await getStacksByVendor(lang, vendor)).filter((e) => listedInIndex(e.data));
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
- ...(hasListedDecks
128
- ? [
129
- {
130
- href: getRelativeLocaleUrl(lang, 'slides/'),
131
- label: t('nav.slides'),
132
- section: 'slides' as SectionKey,
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. */}
@@ -1,9 +1,14 @@
1
1
  /**
2
- * Whether an entry belongs on index listings. Public entries always do;
3
- * PRIVATE entries only when they opt in with `listed: true` (shown as a
4
- * locked teaser card). Unlisted private entries still build and stay
5
- * reachable via direct links and the related-entry sections on detail pages
6
- * that's the discovery path for gated content.
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;
@@ -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
+ }