astro-better-cards 0.2.5 → 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 +175 -70
- package/PageNav.astro +61 -29
- package/README.md +30 -16
- package/package.json +1 -1
package/ChildCards.astro
CHANGED
|
@@ -6,14 +6,18 @@ 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('/')) {
|
|
@@ -42,39 +46,47 @@ if (folder !== undefined) {
|
|
|
42
46
|
}
|
|
43
47
|
prefix = prefix.replace(/^\/+|\/+$/g, '');
|
|
44
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);
|
|
53
|
+
|
|
45
54
|
function stripExt(id: string): string {
|
|
46
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,56 +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
|
-
|
|
98
|
-
|
|
99
|
-
href: `/${collection}/${stripExt(p.id).replace(/\/index$/, '')}`,
|
|
100
|
-
title: await md(p.data.title),
|
|
118
|
+
async function pageToItem(p: any): Promise<NavItem> {
|
|
119
|
+
return {
|
|
120
|
+
href: `/${stripExt(p.id).replace(/\/index$/, '')}`,
|
|
121
|
+
title: await md(p.data.title) as string,
|
|
101
122
|
description: p.data.description,
|
|
102
123
|
icon: p.data.icon ?? null,
|
|
103
124
|
darkIcon: p.data.darkIcon,
|
|
104
125
|
cardImage: p.data.cardImage,
|
|
105
126
|
version: p.data.version,
|
|
106
127
|
badges: p.data.badges,
|
|
107
|
-
}
|
|
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));
|
|
108
138
|
} else {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 };
|
|
140
199
|
}));
|
|
141
200
|
|
|
142
201
|
groups.sort((a, b) => a.order - b.order);
|
|
@@ -144,26 +203,72 @@ if (depth === 1) {
|
|
|
144
203
|
---
|
|
145
204
|
|
|
146
205
|
{depth === 1 ? (
|
|
147
|
-
|
|
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">
|
|
148
208
|
{items.map(item => (
|
|
149
209
|
<Card href={item.href} title={item.title} description={item.description}
|
|
150
210
|
icon={item.icon} darkIcon={item.darkIcon} cardImage={item.cardImage}
|
|
151
|
-
version={item.version} badges={item.badges} variant=
|
|
211
|
+
version={item.version} badges={item.badges} variant={variant} />
|
|
152
212
|
))}
|
|
153
213
|
</div>
|
|
154
|
-
) : (
|
|
214
|
+
) : depth === 2 ? (
|
|
155
215
|
<div class="mb-6">
|
|
156
216
|
{groups.map(group => (
|
|
157
217
|
<Fragment>
|
|
158
218
|
<h2 class="mt-10 mb-2 text-xl font-semibold not-prose" set:html={group.title} />
|
|
159
|
-
<div class=
|
|
219
|
+
<div class={`gap-6 grid ${colClass} not-prose`} xmlns="http://www.w3.org/1999/xhtml">
|
|
160
220
|
{group.items.map(item => (
|
|
161
221
|
<Card href={item.href} title={item.title} description={item.description}
|
|
162
222
|
icon={item.icon} darkIcon={item.darkIcon} cardImage={item.cardImage}
|
|
163
|
-
version={item.version} badges={item.badges} variant=
|
|
223
|
+
version={item.version} badges={item.badges} variant={variant} />
|
|
164
224
|
))}
|
|
165
225
|
</div>
|
|
166
226
|
</Fragment>
|
|
167
227
|
))}
|
|
168
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>
|
|
169
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 |
|