astro-better-cards 0.2.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ChildCards.astro +179 -76
- package/PageNav.astro +61 -29
- package/README.md +30 -16
- package/package.json +1 -1
package/ChildCards.astro
CHANGED
|
@@ -6,23 +6,27 @@ import Card from './Card.astro';
|
|
|
6
6
|
interface Props {
|
|
7
7
|
collection?: string;
|
|
8
8
|
folder?: string;
|
|
9
|
-
depth?: 1 | 2;
|
|
9
|
+
depth?: 1 | 2 | 3 | 4;
|
|
10
|
+
columns?: 1 | 2 | 3 | 4;
|
|
11
|
+
variant?: 'full' | 'compact' | 'quickstart';
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
const { collection = 'docs', folder,
|
|
14
|
+
const { collection = 'docs', folder, variant = 'full' } = Astro.props;
|
|
15
|
+
// coerce so depth={2} and depth="2" both work
|
|
16
|
+
const depth = Number(Astro.props.depth ?? 1) as 1 | 2 | 3 | 4;
|
|
17
|
+
const columns = Number(Astro.props.columns ?? 2) as 1 | 2 | 3 | 4;
|
|
13
18
|
|
|
14
19
|
const pages = await getCollection(collection as any);
|
|
15
20
|
|
|
16
|
-
// Compute collection-relative prefix (no leading/trailing slashes, no extension)
|
|
17
21
|
let prefix: string;
|
|
18
22
|
if (folder !== undefined) {
|
|
19
23
|
if (folder.startsWith('/')) {
|
|
20
24
|
const noSlash = folder.replace(/^\/+/, '');
|
|
21
25
|
prefix = noSlash.startsWith(`${collection}/`) ? noSlash.slice(collection.length + 1) : noSlash;
|
|
22
26
|
} else if (folder.startsWith('./') || folder.startsWith('../')) {
|
|
23
|
-
const pathname = Astro.url.pathname.replace(
|
|
27
|
+
const pathname = Astro.url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.html$/, '');
|
|
24
28
|
const pagePrefix = pathname.startsWith(`${collection}/`) ? pathname.slice(collection.length + 1) : pathname;
|
|
25
|
-
const pageFolder = pagePrefix.includes('/') ? pagePrefix.replace(/\/[^/]
|
|
29
|
+
const pageFolder = pagePrefix.includes('/') ? pagePrefix.replace(/\/[^/]+$/, '') : '';
|
|
26
30
|
const parts = pageFolder ? pageFolder.split('/') : [];
|
|
27
31
|
for (const seg of folder.split('/')) {
|
|
28
32
|
if (seg === '..') parts.pop();
|
|
@@ -33,48 +37,56 @@ if (folder !== undefined) {
|
|
|
33
37
|
prefix = folder;
|
|
34
38
|
}
|
|
35
39
|
} else {
|
|
36
|
-
const pathname = Astro.url.pathname.replace(
|
|
40
|
+
const pathname = Astro.url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.html$/, '');
|
|
37
41
|
if (pathname === '' || pathname === collection) {
|
|
38
42
|
prefix = '';
|
|
39
43
|
} else {
|
|
40
44
|
prefix = pathname.startsWith(`${collection}/`) ? pathname.slice(collection.length + 1) : pathname;
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
|
-
prefix = prefix.replace(
|
|
47
|
+
prefix = prefix.replace(/^\/+|\/+$/g, '');
|
|
48
|
+
|
|
49
|
+
const remark = new Marked();
|
|
50
|
+
const md = (s: string) => remark.parseInline(s) as Promise<string>;
|
|
51
|
+
|
|
52
|
+
const valid = (pages as any[]).filter(p => !p.data.excludeFromNav && p.data.route !== false);
|
|
44
53
|
|
|
45
54
|
function stripExt(id: string): string {
|
|
46
|
-
return id.replace(/\.(md|mdx)
|
|
55
|
+
return id.replace(/\.(md|mdx)$/i, '');
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
// returns path segments relative to prefix, or null if not under prefix
|
|
59
|
+
function getRem(id: string): string[] | null {
|
|
50
60
|
const cid = stripExt(id);
|
|
51
|
-
|
|
52
|
-
if (!
|
|
53
|
-
|
|
54
|
-
return rem.length > 0 && !rem.includes('/') && rem !== 'index';
|
|
61
|
+
const rem = prefix ? (cid.startsWith(`${prefix}/`) ? cid.slice(prefix.length + 1) : null) : cid;
|
|
62
|
+
if (!rem) return null;
|
|
63
|
+
return rem.split('/');
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
function
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
60
|
-
|
|
61
|
-
return parts.length === 2 && parts[1] !== 'index';
|
|
62
|
-
}
|
|
63
|
-
if (!cid.startsWith(`${prefix}/`)) return false;
|
|
64
|
-
const rem = cid.slice(prefix.length + 1);
|
|
65
|
-
const parts = rem.split('/');
|
|
66
|
-
return parts.length === 2 && parts[1] !== 'index';
|
|
66
|
+
function isAtDepth(id: string, d: number): boolean {
|
|
67
|
+
const parts = getRem(id);
|
|
68
|
+
if (!parts) return false;
|
|
69
|
+
return parts.length === d && parts[parts.length - 1] !== 'index';
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
function findIndexPage(relPath: string) {
|
|
73
|
+
return (pages as any[]).find(p => {
|
|
74
|
+
const cid = stripExt(p.id);
|
|
75
|
+
const target = prefix ? `${prefix}/${relPath}` : relPath;
|
|
76
|
+
return cid === target || cid === `${target}/index`;
|
|
77
|
+
});
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
const
|
|
80
|
+
async function getGroupTitle(relPath: string, fallback: string): Promise<string> {
|
|
81
|
+
const entry = findIndexPage(relPath);
|
|
82
|
+
return entry
|
|
83
|
+
? (await md(entry.data.title) as string)
|
|
84
|
+
: fallback.replace(/-/g, ' ').replace(/\b\w/g, (c: string) => c.toUpperCase());
|
|
85
|
+
}
|
|
76
86
|
|
|
77
|
-
|
|
87
|
+
function getGroupOrder(relPath: string): number {
|
|
88
|
+
return findIndexPage(relPath)?.data.order ?? 1000;
|
|
89
|
+
}
|
|
78
90
|
|
|
79
91
|
type NavItem = {
|
|
80
92
|
href: string;
|
|
@@ -87,58 +99,103 @@ type NavItem = {
|
|
|
87
99
|
badges?: string[];
|
|
88
100
|
};
|
|
89
101
|
|
|
90
|
-
type
|
|
102
|
+
type Group = {
|
|
103
|
+
key: string;
|
|
104
|
+
title: string;
|
|
105
|
+
order: number;
|
|
106
|
+
items: NavItem[];
|
|
107
|
+
subgroups: Group[];
|
|
108
|
+
};
|
|
91
109
|
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
const colClasses: Record<number, string> = {
|
|
111
|
+
1: 'grid-cols-1',
|
|
112
|
+
2: 'grid-cols-1 lg:grid-cols-2',
|
|
113
|
+
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
114
|
+
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
|
|
115
|
+
};
|
|
116
|
+
const colClass = colClasses[columns] ?? colClasses[2];
|
|
94
117
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
children.sort((a, b) => (a.data.order ?? 1000) - (b.data.order ?? 1000) || a.data.title.localeCompare(b.data.title));
|
|
98
|
-
items = await Promise.all(children.map(async p => ({
|
|
99
|
-
// FIX: Replaced `/${collection}/` with a single `/`
|
|
118
|
+
async function pageToItem(p: any): Promise<NavItem> {
|
|
119
|
+
return {
|
|
100
120
|
href: `/${stripExt(p.id).replace(/\/index$/, '')}`,
|
|
101
|
-
title: await md(p.data.title),
|
|
121
|
+
title: await md(p.data.title) as string,
|
|
102
122
|
description: p.data.description,
|
|
103
123
|
icon: p.data.icon ?? null,
|
|
104
124
|
darkIcon: p.data.darkIcon,
|
|
105
125
|
cardImage: p.data.cardImage,
|
|
106
126
|
version: p.data.version,
|
|
107
127
|
badges: p.data.badges,
|
|
108
|
-
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let items: NavItem[] = [];
|
|
132
|
+
let groups: Group[] = [];
|
|
133
|
+
|
|
134
|
+
if (depth === 1) {
|
|
135
|
+
const children = valid.filter(p => isAtDepth(p.id, 1));
|
|
136
|
+
children.sort((a, b) => (a.data.order ?? 1000) - (b.data.order ?? 1000) || a.data.title.localeCompare(b.data.title));
|
|
137
|
+
items = await Promise.all(children.map(pageToItem));
|
|
109
138
|
} else {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
const pagesAtDepth = valid.filter(p => isAtDepth(p.id, depth));
|
|
140
|
+
|
|
141
|
+
const topMap = new Map<string, any[]>();
|
|
142
|
+
for (const p of pagesAtDepth) {
|
|
143
|
+
const topKey = getRem(p.id)![0];
|
|
144
|
+
if (!topMap.has(topKey)) topMap.set(topKey, []);
|
|
145
|
+
topMap.get(topKey)!.push(p);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
groups = await Promise.all([...topMap.keys()].map(async topKey => {
|
|
149
|
+
const topTitle = await getGroupTitle(topKey, topKey);
|
|
150
|
+
const topOrder = getGroupOrder(topKey);
|
|
151
|
+
const topPages = topMap.get(topKey)!;
|
|
152
|
+
|
|
153
|
+
if (depth === 2) {
|
|
154
|
+
topPages.sort((a, b) => (a.data.order ?? 1000) - (b.data.order ?? 1000) || a.data.title.localeCompare(b.data.title));
|
|
155
|
+
return { key: topKey, title: topTitle, order: topOrder, items: await Promise.all(topPages.map(pageToItem)), subgroups: [] };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const midMap = new Map<string, any[]>();
|
|
159
|
+
for (const p of topPages) {
|
|
160
|
+
const midKey = getRem(p.id)![1];
|
|
161
|
+
if (!midMap.has(midKey)) midMap.set(midKey, []);
|
|
162
|
+
midMap.get(midKey)!.push(p);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const subgroups: Group[] = await Promise.all([...midMap.keys()].map(async midKey => {
|
|
166
|
+
const midRelPath = `${topKey}/${midKey}`;
|
|
167
|
+
const midTitle = await getGroupTitle(midRelPath, midKey);
|
|
168
|
+
const midOrder = getGroupOrder(midRelPath);
|
|
169
|
+
const midPages = midMap.get(midKey)!;
|
|
170
|
+
|
|
171
|
+
if (depth === 3) {
|
|
172
|
+
midPages.sort((a, b) => (a.data.order ?? 1000) - (b.data.order ?? 1000) || a.data.title.localeCompare(b.data.title));
|
|
173
|
+
return { key: midKey, title: midTitle, order: midOrder, items: await Promise.all(midPages.map(pageToItem)), subgroups: [] };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// depth === 4
|
|
177
|
+
const botMap = new Map<string, any[]>();
|
|
178
|
+
for (const p of midPages) {
|
|
179
|
+
const botKey = getRem(p.id)![2];
|
|
180
|
+
if (!botMap.has(botKey)) botMap.set(botKey, []);
|
|
181
|
+
botMap.get(botKey)!.push(p);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const subsubgroups: Group[] = await Promise.all([...botMap.keys()].map(async botKey => {
|
|
185
|
+
const botRelPath = `${topKey}/${midKey}/${botKey}`;
|
|
186
|
+
const botTitle = await getGroupTitle(botRelPath, botKey);
|
|
187
|
+
const botOrder = getGroupOrder(botRelPath);
|
|
188
|
+
const botPages = botMap.get(botKey)!;
|
|
189
|
+
botPages.sort((a, b) => (a.data.order ?? 1000) - (b.data.order ?? 1000) || a.data.title.localeCompare(b.data.title));
|
|
190
|
+
return { key: botKey, title: botTitle, order: botOrder, items: await Promise.all(botPages.map(pageToItem)), subgroups: [] };
|
|
191
|
+
}));
|
|
192
|
+
subsubgroups.sort((a, b) => a.order - b.order);
|
|
193
|
+
|
|
194
|
+
return { key: midKey, title: midTitle, order: midOrder, items: [], subgroups: subsubgroups };
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
subgroups.sort((a, b) => a.order - b.order);
|
|
198
|
+
return { key: topKey, title: topTitle, order: topOrder, items: [], subgroups };
|
|
142
199
|
}));
|
|
143
200
|
|
|
144
201
|
groups.sort((a, b) => a.order - b.order);
|
|
@@ -146,26 +203,72 @@ if (depth === 1) {
|
|
|
146
203
|
---
|
|
147
204
|
|
|
148
205
|
{depth === 1 ? (
|
|
149
|
-
|
|
206
|
+
// xmlns prevents Astro's MDX renderer from treating children as markdown
|
|
207
|
+
<div class={`gap-6 mt-6 mb-6 grid ${colClass} not-prose`} xmlns="http://www.w3.org/1999/xhtml">
|
|
150
208
|
{items.map(item => (
|
|
151
209
|
<Card href={item.href} title={item.title} description={item.description}
|
|
152
210
|
icon={item.icon} darkIcon={item.darkIcon} cardImage={item.cardImage}
|
|
153
|
-
version={item.version} badges={item.badges} variant=
|
|
211
|
+
version={item.version} badges={item.badges} variant={variant} />
|
|
154
212
|
))}
|
|
155
213
|
</div>
|
|
156
|
-
) : (
|
|
214
|
+
) : depth === 2 ? (
|
|
157
215
|
<div class="mb-6">
|
|
158
216
|
{groups.map(group => (
|
|
159
217
|
<Fragment>
|
|
160
218
|
<h2 class="mt-10 mb-2 text-xl font-semibold not-prose" set:html={group.title} />
|
|
161
|
-
<div class=
|
|
219
|
+
<div class={`gap-6 grid ${colClass} not-prose`} xmlns="http://www.w3.org/1999/xhtml">
|
|
162
220
|
{group.items.map(item => (
|
|
163
221
|
<Card href={item.href} title={item.title} description={item.description}
|
|
164
222
|
icon={item.icon} darkIcon={item.darkIcon} cardImage={item.cardImage}
|
|
165
|
-
version={item.version} badges={item.badges} variant=
|
|
223
|
+
version={item.version} badges={item.badges} variant={variant} />
|
|
166
224
|
))}
|
|
167
225
|
</div>
|
|
168
226
|
</Fragment>
|
|
169
227
|
))}
|
|
170
228
|
</div>
|
|
229
|
+
) : depth === 3 ? (
|
|
230
|
+
<div class="mb-6">
|
|
231
|
+
{groups.map(group => (
|
|
232
|
+
<Fragment>
|
|
233
|
+
<h2 class="mt-10 mb-2 text-xl font-semibold not-prose" set:html={group.title} />
|
|
234
|
+
{group.subgroups.map(sub => (
|
|
235
|
+
<Fragment>
|
|
236
|
+
<h3 class="mt-6 mb-2 text-lg font-semibold not-prose" set:html={sub.title} />
|
|
237
|
+
<div class={`gap-6 grid ${colClass} not-prose`} xmlns="http://www.w3.org/1999/xhtml">
|
|
238
|
+
{sub.items.map(item => (
|
|
239
|
+
<Card href={item.href} title={item.title} description={item.description}
|
|
240
|
+
icon={item.icon} darkIcon={item.darkIcon} cardImage={item.cardImage}
|
|
241
|
+
version={item.version} badges={item.badges} variant={variant} />
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
</Fragment>
|
|
245
|
+
))}
|
|
246
|
+
</Fragment>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
) : (
|
|
250
|
+
<div class="mb-6">
|
|
251
|
+
{groups.map(group => (
|
|
252
|
+
<Fragment>
|
|
253
|
+
<h2 class="mt-10 mb-2 text-xl font-semibold not-prose" set:html={group.title} />
|
|
254
|
+
{group.subgroups.map(sub => (
|
|
255
|
+
<Fragment>
|
|
256
|
+
<h3 class="mt-6 mb-2 text-lg font-semibold not-prose" set:html={sub.title} />
|
|
257
|
+
{sub.subgroups.map(subsub => (
|
|
258
|
+
<Fragment>
|
|
259
|
+
<h4 class="mt-4 mb-2 text-base font-semibold not-prose" set:html={subsub.title} />
|
|
260
|
+
<div class={`gap-6 grid ${colClass} not-prose`} xmlns="http://www.w3.org/1999/xhtml">
|
|
261
|
+
{subsub.items.map(item => (
|
|
262
|
+
<Card href={item.href} title={item.title} description={item.description}
|
|
263
|
+
icon={item.icon} darkIcon={item.darkIcon} cardImage={item.cardImage}
|
|
264
|
+
version={item.version} badges={item.badges} variant={variant} />
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
</Fragment>
|
|
268
|
+
))}
|
|
269
|
+
</Fragment>
|
|
270
|
+
))}
|
|
271
|
+
</Fragment>
|
|
272
|
+
))}
|
|
273
|
+
</div>
|
|
171
274
|
)}
|
package/PageNav.astro
CHANGED
|
@@ -4,26 +4,33 @@ import { Marked } from 'marked';
|
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
collection?: string;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
lastPage?: string;
|
|
7
|
+
prev?: string;
|
|
8
|
+
next?: string;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
const { collection = 'docs',
|
|
11
|
+
const { collection = 'docs', prev: prevProp, next: nextProp } = Astro.props;
|
|
13
12
|
|
|
14
13
|
const pages = await getCollection(collection as any);
|
|
15
14
|
const remark = new Marked();
|
|
16
15
|
|
|
17
|
-
const currentClean = currentId.replace(/\.(md|mdx)$/i, '');
|
|
18
|
-
const currentFolder = currentClean.includes('/') ? currentClean.replace(/\/[^/]+$/, '') : '';
|
|
19
|
-
|
|
20
16
|
function stripExt(id: string): string {
|
|
21
17
|
return id.replace(/\.(md|mdx)$/i, '');
|
|
22
18
|
}
|
|
23
19
|
|
|
20
|
+
const pathname = Astro.url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.html$/, '');
|
|
21
|
+
const currentFolder = pathname.includes('/') ? pathname.replace(/\/[^/]+$/, '') : '';
|
|
22
|
+
|
|
23
|
+
const currentEntry = (pages as any[]).find(p => {
|
|
24
|
+
const cid = stripExt(p.id);
|
|
25
|
+
return cid === pathname || cid === `${pathname}/index`;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const prevPath = prevProp ?? currentEntry?.data.prev;
|
|
29
|
+
const nextPath = nextProp ?? currentEntry?.data.next;
|
|
30
|
+
|
|
24
31
|
function findPage(href: string) {
|
|
25
32
|
const target = href.startsWith('/')
|
|
26
|
-
? href.replace(new RegExp(`^\\/${collection}\\/`), '')
|
|
33
|
+
? href.replace(new RegExp(`^\\/${collection}\\/`), '').replace(/^\//, '')
|
|
27
34
|
: currentFolder ? `${currentFolder}/${href}` : href;
|
|
28
35
|
return (pages as any[]).find(p => {
|
|
29
36
|
const cid = stripExt(p.id);
|
|
@@ -33,39 +40,64 @@ function findPage(href: string) {
|
|
|
33
40
|
|
|
34
41
|
function makeHref(href: string): string {
|
|
35
42
|
if (href.startsWith('/')) return href;
|
|
36
|
-
return `/${
|
|
43
|
+
return `/${currentFolder ? `${currentFolder}/` : ''}${href}`;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
let prevEntry = prevPath ? findPage(prevPath) : null;
|
|
47
|
+
let prevHref: string | null = prevPath ? makeHref(prevPath) : null;
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
let nextEntry = nextPath ? findPage(nextPath) : null;
|
|
50
|
+
let nextHref: string | null = nextPath ? makeHref(nextPath) : null;
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
// fall back to sibling order when prev/next not specified via prop or frontmatter
|
|
53
|
+
if (!prevEntry || !nextEntry) {
|
|
54
|
+
const siblings = (pages as any[])
|
|
55
|
+
.filter(p => {
|
|
56
|
+
if (p.data.excludeFromNav || p.data.route === false) return false;
|
|
57
|
+
const cid = stripExt(p.id);
|
|
58
|
+
const pFolder = cid.includes('/') ? cid.replace(/\/[^/]+$/, '') : '';
|
|
59
|
+
const lastSeg = cid.split('/').pop()!;
|
|
60
|
+
return pFolder === currentFolder && lastSeg !== 'index';
|
|
61
|
+
})
|
|
62
|
+
.sort((a, b) => (a.data.order ?? 1000) - (b.data.order ?? 1000) || a.data.title.localeCompare(b.data.title));
|
|
63
|
+
|
|
64
|
+
const idx = siblings.findIndex(p => stripExt(p.id) === pathname);
|
|
65
|
+
if (idx > 0 && !prevEntry) {
|
|
66
|
+
prevEntry = siblings[idx - 1];
|
|
67
|
+
prevHref = `/${stripExt(prevEntry.id).replace(/\/index$/, '')}`;
|
|
68
|
+
}
|
|
69
|
+
if (idx >= 0 && idx < siblings.length - 1 && !nextEntry) {
|
|
70
|
+
nextEntry = siblings[idx + 1];
|
|
71
|
+
nextHref = `/${stripExt(nextEntry.id).replace(/\/index$/, '')}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const prevTitle: string | null = prevEntry ? await remark.parseInline(prevEntry.data.title) as string : null;
|
|
76
|
+
const nextTitle: string | null = nextEntry ? await remark.parseInline(nextEntry.data.title) as string : null;
|
|
47
77
|
---
|
|
48
78
|
|
|
49
79
|
<nav class="not-prose mt-10 pt-6 border-t border-slate-200 dark:border-slate-700" aria-label="Page navigation">
|
|
50
80
|
<div class="grid grid-cols-2 gap-4">
|
|
51
|
-
{
|
|
52
|
-
<a href={
|
|
53
|
-
class="group flex
|
|
54
|
-
<span class="
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
{prevEntry ? (
|
|
82
|
+
<a href={prevHref}
|
|
83
|
+
class="group flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-4 py-3 no-underline hover:border-indigo-500 dark:hover:border-indigo-400 transition-colors">
|
|
84
|
+
<span class="text-2xl leading-none text-slate-300 dark:text-slate-600 group-hover:text-indigo-500 dark:group-hover:text-indigo-400 transition-colors shrink-0" aria-hidden="true">←</span>
|
|
85
|
+
<div class="min-w-0">
|
|
86
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">Previous</div>
|
|
87
|
+
<div class="text-sm font-medium text-slate-800 dark:text-slate-200 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 truncate"
|
|
88
|
+
set:html={prevTitle} />
|
|
89
|
+
</div>
|
|
59
90
|
</a>
|
|
60
91
|
) : <div />}
|
|
61
92
|
{nextEntry ? (
|
|
62
93
|
<a href={nextHref}
|
|
63
|
-
class="group flex
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
class="group flex items-center justify-end gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-4 py-3 text-right no-underline hover:border-indigo-500 dark:hover:border-indigo-400 transition-colors">
|
|
95
|
+
<div class="min-w-0">
|
|
96
|
+
<div class="text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">Next</div>
|
|
97
|
+
<div class="text-sm font-medium text-slate-800 dark:text-slate-200 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 truncate"
|
|
98
|
+
set:html={nextTitle} />
|
|
99
|
+
</div>
|
|
100
|
+
<span class="text-2xl leading-none text-slate-300 dark:text-slate-600 group-hover:text-indigo-500 dark:group-hover:text-indigo-400 transition-colors shrink-0" aria-hidden="true">→</span>
|
|
69
101
|
</a>
|
|
70
102
|
) : <div />}
|
|
71
103
|
</div>
|
package/README.md
CHANGED
|
@@ -30,8 +30,10 @@ import Card from 'astro-better-cards/Card.astro';
|
|
|
30
30
|
| `cardImage` | `string` | — | Hero image URL |
|
|
31
31
|
| `variant` | `'full' \| 'compact' \| 'quickstart'` | `'full'` | Display style |
|
|
32
32
|
| `comingSoon` | `boolean` | `false` | Greys out the card |
|
|
33
|
-
| `label` | `string` | — | Small label text
|
|
33
|
+
| `label` | `string` | — | Small label text |
|
|
34
34
|
| `labelFirst` | `boolean` | `false` | Render label before title |
|
|
35
|
+
| `version` | `string` | — | Version string shown below the title |
|
|
36
|
+
| `badges` | `string[]` | — | Badge labels shown beside the version |
|
|
35
37
|
|
|
36
38
|
## ChildCards
|
|
37
39
|
|
|
@@ -46,46 +48,58 @@ import ChildCards from 'astro-better-cards/ChildCards.astro';
|
|
|
46
48
|
<!-- grandchildren grouped by subfolder with section headers -->
|
|
47
49
|
<ChildCards depth={2} />
|
|
48
50
|
|
|
49
|
-
<!--
|
|
50
|
-
<ChildCards
|
|
51
|
+
<!-- deeper nesting (up to 4) with h2/h3/h4 section headers -->
|
|
52
|
+
<ChildCards depth={4} />
|
|
53
|
+
|
|
54
|
+
<!-- explicit folder, 3 columns -->
|
|
55
|
+
<ChildCards folder="get-started/quickstarts" columns={3} />
|
|
51
56
|
|
|
52
57
|
<!-- children from a different collection -->
|
|
53
58
|
<ChildCards collection="articles" />
|
|
54
59
|
```
|
|
55
60
|
|
|
56
|
-
Typically used via the `sectionIndex: true` front matter field (rendered automatically by the layout), or imported directly in MDX for more control.
|
|
57
|
-
|
|
58
61
|
### Props
|
|
59
62
|
|
|
60
63
|
| Prop | Type | Default | Description |
|
|
61
64
|
|------|------|---------|-------------|
|
|
62
65
|
| `collection` | `string` | `'docs'` | Astro content collection name |
|
|
63
66
|
| `folder` | `string` | current page folder | Collection-relative path (`get-started/foo`), URL-absolute path (`/docs/get-started/foo`), or relative path (`./sub`, `../other`) |
|
|
64
|
-
| `depth` | `1 \| 2` | `1` | `1` = direct children; `2` =
|
|
67
|
+
| `depth` | `1 \| 2 \| 3 \| 4` | `1` | `1` = direct children; `2`-`4` = deeper pages grouped under section headers (h2, then h3, then h4) |
|
|
68
|
+
| `columns` | `1 \| 2 \| 3 \| 4` | `2` | Number of columns in the card grid |
|
|
65
69
|
|
|
66
70
|
Pages with `excludeFromNav: true` or `route: false` are excluded.
|
|
67
71
|
|
|
68
72
|
## PageNav
|
|
69
73
|
|
|
70
|
-
Renders previous/next navigation links at the bottom of a page
|
|
74
|
+
Renders previous/next navigation links at the bottom of a page. Reads `prev` and `next` from the
|
|
75
|
+
current page's frontmatter and resolves titles automatically from the collection. Prop values
|
|
76
|
+
override frontmatter when both are present.
|
|
77
|
+
|
|
78
|
+
Add `prev` and `next` to the page's frontmatter:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
prev: "step-1"
|
|
82
|
+
next: "step-3"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Then use the component with no props:
|
|
71
86
|
|
|
72
87
|
```astro
|
|
73
88
|
import PageNav from 'astro-better-cards/PageNav.astro';
|
|
74
89
|
|
|
75
|
-
<PageNav
|
|
76
|
-
currentId={entry.id}
|
|
77
|
-
lastPage="step-1"
|
|
78
|
-
nextPage="step-3"
|
|
79
|
-
/>
|
|
90
|
+
<PageNav />
|
|
80
91
|
```
|
|
81
92
|
|
|
82
|
-
Or
|
|
93
|
+
Or override frontmatter on a specific instance:
|
|
94
|
+
|
|
95
|
+
```astro
|
|
96
|
+
<PageNav prev="other-page" next="another-page" />
|
|
97
|
+
```
|
|
83
98
|
|
|
84
99
|
### Props
|
|
85
100
|
|
|
86
101
|
| Prop | Type | Default | Description |
|
|
87
102
|
|------|------|---------|-------------|
|
|
88
103
|
| `collection` | `string` | `'docs'` | Astro content collection name |
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `lastPage` | `string` | — | Relative href to the previous page |
|
|
104
|
+
| `prev` | `string` | frontmatter `prev` | Path to the previous page (relative filename, collection-relative, or absolute `/`) |
|
|
105
|
+
| `next` | `string` | frontmatter `next` | Path to the next page |
|