stack-site-builder 1.18.0 → 1.19.1
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 +32 -0
- package/index.mjs +7 -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/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 +4 -0
- package/src/layouts/BaseLayout.astro +105 -0
- package/src/lib/listing.ts +10 -5
- package/src/lib/private-client.ts +45 -1
- package/src/pages/aas-auth.json.ts +22 -0
- package/src/styles/global.css +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,36 @@ 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.1] - 2026-07-23
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Member-only entries are login-aware, and the Slides nav is back** —
|
|
19
|
+
1.18.0 dropped unlisted private entries from listings for everyone; the
|
|
20
|
+
intended behavior is per-VIEWER: logged-out visitors don't see them,
|
|
21
|
+
logged-in members do. Listings now render member-only cards with
|
|
22
|
+
`data-aas-member-only`; a pre-paint script stamps `html[data-aas-member]`
|
|
23
|
+
when a session exists and CSS does the rest — no flash either way. A
|
|
24
|
+
"shows after login" hint line replaces the invisible content for
|
|
25
|
+
logged-out visitors, and 1.19.0's listed-deck nav gating is reverted (the
|
|
26
|
+
Slides item follows the section toggle again).
|
|
27
|
+
|
|
28
|
+
## [1.19.0] - 2026-07-23
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Header member login/logout** — sites with private content get an account
|
|
33
|
+
control in the header: a login dropdown (id/password, same credential
|
|
34
|
+
machinery as the page gates) and a logout button once signed in. The
|
|
35
|
+
control reveals itself only when the new `/aas-auth.json` endpoint reports
|
|
36
|
+
configured login users (it serves the salted user table every private page
|
|
37
|
+
already embeds — nothing new is exposed); sites without `AAS_PRIVATE_*`
|
|
38
|
+
env see nothing. Credential verification moved into a shared
|
|
39
|
+
`loginWithCredentials` helper in `lib/private-client`.
|
|
40
|
+
- **Slides nav hides without listed decks** — a site whose decks are all
|
|
41
|
+
private+unlisted reaches them from course pages, so the header's Slides
|
|
42
|
+
item now appears only when the index actually lists at least one deck.
|
|
43
|
+
|
|
14
44
|
## [1.18.0] - 2026-07-23
|
|
15
45
|
|
|
16
46
|
### Added
|
|
@@ -303,6 +333,8 @@ catalog sites from a thin content-only repository.
|
|
|
303
333
|
- **Standalone development setup** — a devcontainer and a minimal `playground/`
|
|
304
334
|
consuming site for developing and previewing the theme on its own.
|
|
305
335
|
|
|
336
|
+
[1.19.1]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.19.0...v1.19.1
|
|
337
|
+
[1.19.0]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.18.0...v1.19.0
|
|
306
338
|
[1.18.0]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.4...v1.18.0
|
|
307
339
|
[1.17.4]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.3...v1.17.4
|
|
308
340
|
[1.17.3]: https://github.com/CodeCompose7/stack-site-builder/compare/v1.17.2...v1.17.3
|
package/index.mjs
CHANGED
|
@@ -129,6 +129,13 @@ export default function aasTheme({ glossary, sections = {} }) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
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
|
+
|
|
132
139
|
updateConfig({
|
|
133
140
|
markdown: aasMarkdown({ glossary, locales, defaultLocale }),
|
|
134
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.1",
|
|
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}
|
|
@@ -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
|
@@ -71,6 +71,8 @@ export const ui = {
|
|
|
71
71
|
'code.copy': 'Copy code',
|
|
72
72
|
'code.copied': 'Copied',
|
|
73
73
|
'private.badge': 'Private',
|
|
74
|
+
'private.login': 'Log in',
|
|
75
|
+
'private.memberHint': '\ud83d\udd12 Some content appears after logging in.',
|
|
74
76
|
'private.locked': 'This content is private. Log in to view it.',
|
|
75
77
|
'private.id': 'ID',
|
|
76
78
|
'private.password': 'Password',
|
|
@@ -247,6 +249,8 @@ export const ui = {
|
|
|
247
249
|
'code.copy': '코드 복사',
|
|
248
250
|
'code.copied': '복사됨',
|
|
249
251
|
'private.badge': '비공개',
|
|
252
|
+
'private.login': '로그인',
|
|
253
|
+
'private.memberHint': '\ud83d\udd12 일부 콘텐츠는 로그인 후에 표시됩니다.',
|
|
250
254
|
'private.locked': '비공개 콘텐츠입니다. 로그인 후 볼 수 있습니다.',
|
|
251
255
|
'private.id': '아이디',
|
|
252
256
|
'private.password': '비밀번호',
|
|
@@ -219,6 +219,15 @@ const navItems = allNavItems.filter((item) => !item.section || sectionEnabled(it
|
|
|
219
219
|
} catch (e) {}
|
|
220
220
|
})();
|
|
221
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>
|
|
222
231
|
{/* Listing density (standard / gallery), persisted in localStorage. Gallery
|
|
223
232
|
is the default; only an explicit 'grid' choice falls back to standard.
|
|
224
233
|
Set before paint so the layout doesn't flash. */}
|
|
@@ -263,6 +272,53 @@ const navItems = allNavItems.filter((item) => !item.section || sectionEnabled(it
|
|
|
263
272
|
))
|
|
264
273
|
}
|
|
265
274
|
</div>
|
|
275
|
+
{/* Member login/logout for private content — hidden until
|
|
276
|
+
/aas-auth.json reports the site has login users configured. */}
|
|
277
|
+
<details class="aas-menu relative" data-auth hidden>
|
|
278
|
+
<summary
|
|
279
|
+
class="flex cursor-pointer list-none items-center rounded-lg border border-[var(--aas-border)] px-2.5 py-1 text-[var(--aas-text)] select-none"
|
|
280
|
+
aria-label={t('private.login')}
|
|
281
|
+
title={t('private.login')}
|
|
282
|
+
>
|
|
283
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
284
|
+
</summary>
|
|
285
|
+
<div
|
|
286
|
+
class="absolute right-0 z-20 mt-1 w-64 rounded-lg border border-[var(--aas-border)] bg-[var(--aas-panel)] p-3 shadow-lg"
|
|
287
|
+
>
|
|
288
|
+
<form data-auth-form class="flex flex-col gap-2">
|
|
289
|
+
<input
|
|
290
|
+
name="id"
|
|
291
|
+
required
|
|
292
|
+
autocomplete="username"
|
|
293
|
+
placeholder={t('private.id')}
|
|
294
|
+
class="rounded-lg border border-[var(--aas-border)] bg-[var(--aas-bg)] px-3 py-1.5 text-sm text-[var(--aas-text)]"
|
|
295
|
+
/>
|
|
296
|
+
<input
|
|
297
|
+
name="password"
|
|
298
|
+
type="password"
|
|
299
|
+
required
|
|
300
|
+
autocomplete="current-password"
|
|
301
|
+
placeholder={t('private.password')}
|
|
302
|
+
class="rounded-lg border border-[var(--aas-border)] bg-[var(--aas-bg)] px-3 py-1.5 text-sm text-[var(--aas-text)]"
|
|
303
|
+
/>
|
|
304
|
+
<p data-auth-error hidden class="text-xs text-[var(--aas-accent)]">{t('private.error')}</p>
|
|
305
|
+
<button
|
|
306
|
+
type="submit"
|
|
307
|
+
class="rounded-lg bg-[var(--aas-accent)] px-3 py-1.5 text-sm font-semibold text-white"
|
|
308
|
+
>
|
|
309
|
+
{t('private.submit')}
|
|
310
|
+
</button>
|
|
311
|
+
</form>
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
data-auth-logout
|
|
315
|
+
hidden
|
|
316
|
+
class="w-full rounded-lg border border-[var(--aas-border)] px-3 py-1.5 text-sm text-[var(--aas-text)] hover:bg-[var(--aas-tint)]"
|
|
317
|
+
>
|
|
318
|
+
🔓 {t('private.logout')}
|
|
319
|
+
</button>
|
|
320
|
+
</div>
|
|
321
|
+
</details>
|
|
266
322
|
<ThemeToggle lang={lang} />
|
|
267
323
|
<LanguageSwitcher lang={lang} path={path} />
|
|
268
324
|
<details class="aas-menu relative sm:hidden">
|
|
@@ -328,6 +384,55 @@ const navItems = allNavItems.filter((item) => !item.section || sectionEnabled(it
|
|
|
328
384
|
<BackToTop lang={lang} />
|
|
329
385
|
|
|
330
386
|
<script>
|
|
387
|
+
import {
|
|
388
|
+
hasSession,
|
|
389
|
+
clearSession,
|
|
390
|
+
loginWithCredentials,
|
|
391
|
+
type AuthData,
|
|
392
|
+
} from '../lib/private-client';
|
|
393
|
+
|
|
394
|
+
// Header member login/logout: reveal the control only when the site has
|
|
395
|
+
// login users configured (/aas-auth.json — { enabled: false } otherwise).
|
|
396
|
+
const authCtrl = document.querySelector<HTMLDetailsElement>('[data-auth]');
|
|
397
|
+
if (authCtrl) {
|
|
398
|
+
(async () => {
|
|
399
|
+
try {
|
|
400
|
+
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
401
|
+
const res = await fetch(`${base}/aas-auth.json`);
|
|
402
|
+
const data = (await res.json()) as { enabled: boolean } & AuthData;
|
|
403
|
+
if (!data.enabled) return;
|
|
404
|
+
authCtrl.hidden = false;
|
|
405
|
+
const form = authCtrl.querySelector<HTMLFormElement>('[data-auth-form]')!;
|
|
406
|
+
const logoutBtn = authCtrl.querySelector<HTMLButtonElement>('[data-auth-logout]')!;
|
|
407
|
+
const errorEl = authCtrl.querySelector<HTMLElement>('[data-auth-error]')!;
|
|
408
|
+
const sync = () => {
|
|
409
|
+
const on = hasSession(data.days);
|
|
410
|
+
form.hidden = on;
|
|
411
|
+
logoutBtn.hidden = !on;
|
|
412
|
+
};
|
|
413
|
+
sync();
|
|
414
|
+
logoutBtn.addEventListener('click', () => {
|
|
415
|
+
clearSession();
|
|
416
|
+
location.reload();
|
|
417
|
+
});
|
|
418
|
+
form.addEventListener('submit', async (e) => {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
errorEl.hidden = true;
|
|
421
|
+
const fd = new FormData(form);
|
|
422
|
+
const ok = await loginWithCredentials(
|
|
423
|
+
data,
|
|
424
|
+
String(fd.get('id') ?? ''),
|
|
425
|
+
String(fd.get('password') ?? ''),
|
|
426
|
+
);
|
|
427
|
+
if (ok) location.reload();
|
|
428
|
+
else errorEl.hidden = false;
|
|
429
|
+
});
|
|
430
|
+
} catch {
|
|
431
|
+
/* endpoint unavailable — control stays hidden */
|
|
432
|
+
}
|
|
433
|
+
})();
|
|
434
|
+
}
|
|
435
|
+
|
|
331
436
|
import { initOnReady } from '../lib/reinit';
|
|
332
437
|
|
|
333
438
|
// Clicking a heading's "#" anchor copies that section's URL to the
|
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;
|
|
@@ -60,7 +60,7 @@ function storedKey(days: number): Uint8Array | null {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function clearSession(): void {
|
|
63
|
+
export function clearSession(): void {
|
|
64
64
|
try {
|
|
65
65
|
localStorage.removeItem(STORE);
|
|
66
66
|
} catch {
|
|
@@ -68,6 +68,50 @@ function clearSession(): void {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** The user table a login form needs — a GateData without the content. */
|
|
72
|
+
export interface AuthData {
|
|
73
|
+
users: { h: string; s: string; iv: string; w: string }[];
|
|
74
|
+
salt: string;
|
|
75
|
+
days: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whether this device holds a live session key. */
|
|
79
|
+
export function hasSession(days: number): boolean {
|
|
80
|
+
return storedKey(days) !== null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Verify credentials by unwrapping the site key (AES-GCM authentication
|
|
85
|
+
* fails on a wrong password) and store the session on success. Shared by
|
|
86
|
+
* the per-page gate form and the header login control.
|
|
87
|
+
*/
|
|
88
|
+
export async function loginWithCredentials(
|
|
89
|
+
data: AuthData,
|
|
90
|
+
id: string,
|
|
91
|
+
password: string,
|
|
92
|
+
): Promise<boolean> {
|
|
93
|
+
const salt = dec(data.salt);
|
|
94
|
+
const idBytes = new TextEncoder().encode(id.trim().toLowerCase());
|
|
95
|
+
const joined = new Uint8Array(salt.length + idBytes.length);
|
|
96
|
+
joined.set(salt);
|
|
97
|
+
joined.set(idBytes, salt.length);
|
|
98
|
+
const h = await sha256Hex(joined);
|
|
99
|
+
const user = data.users.find((u) => u.h === h);
|
|
100
|
+
if (!user) return false;
|
|
101
|
+
try {
|
|
102
|
+
const kek = await deriveKek(password, dec(user.s));
|
|
103
|
+
const k = new Uint8Array(await aesDecrypt(kek, dec(user.iv), dec(user.w)));
|
|
104
|
+
try {
|
|
105
|
+
localStorage.setItem(STORE, JSON.stringify({ k: enc(k), t: Date.now() }));
|
|
106
|
+
} catch {
|
|
107
|
+
/* private browsing — session just won't persist */
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false; // wrong password (unwrap failed)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
/** Swap the gate for the decrypted HTML; re-run inline scripts; notify re-init hooks. */
|
|
72
116
|
function inject(gate: HTMLElement, html: string, logoutLabel: string): void {
|
|
73
117
|
const host = document.createElement('div');
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { privateClientData } from '../lib/private';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The site's login user table (salted id hashes + per-user wrapped keys —
|
|
6
|
+
* the same data every private page already embeds in its gate payload, so
|
|
7
|
+
* this exposes nothing new). Powers the header login control on sites with
|
|
8
|
+
* private content; sites without the AAS_PRIVATE_* env render
|
|
9
|
+
* `{ enabled: false }` and the control stays hidden.
|
|
10
|
+
*/
|
|
11
|
+
export const GET: APIRoute = () => {
|
|
12
|
+
try {
|
|
13
|
+
const data = privateClientData();
|
|
14
|
+
return new Response(JSON.stringify({ enabled: true, ...data }), {
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
});
|
|
17
|
+
} catch {
|
|
18
|
+
return new Response(JSON.stringify({ enabled: false }), {
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
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
|
+
}
|