astro-better-cards 0.2.6 → 1.0.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/CardGrid.astro ADDED
@@ -0,0 +1,17 @@
1
+ ---
2
+ interface Props {
3
+ columns?: 1 | 2 | 3 | 4;
4
+ }
5
+
6
+ const { columns = 2 } = Astro.props;
7
+ const colClasses: Record<number, string> = {
8
+ 1: 'grid-cols-1',
9
+ 2: 'grid-cols-1 lg:grid-cols-2',
10
+ 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
11
+ 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
12
+ };
13
+ const colClass = colClasses[columns] ?? colClasses[2];
14
+ ---
15
+ <div class={`gap-6 mt-6 mb-6 grid ${colClass} not-prose`} xmlns="http://www.w3.org/1999/xhtml">
16
+ <slot />
17
+ </div>
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, depth = 1 } = Astro.props;
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(/^\/+|\/+\(/g, '').replace(/\.html\)/, '');
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(/^\/+|\/+\(/g, '').replace(/\.html\)/, '');
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(/^\/+|\/+\$/g, '');
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)\$/i, '');
55
+ return id.replace(/\.(md|mdx)$/i, '');
47
56
  }
48
57
 
49
- function isDirectChild(id: string): boolean {
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
- if (!prefix) return !cid.includes('/') && cid !== 'index';
52
- if (!cid.startsWith(`${prefix}/`)) return false;
53
- const rem = cid.slice(prefix.length + 1);
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 isGrandchildPage(id: string): boolean {
58
- const cid = stripExt(id);
59
- if (!prefix) {
60
- const parts = cid.split('/');
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 getSubfolder(id: string): string {
70
- if (!prefix) return stripExt(id).split('/')[0];
71
- return stripExt(id).slice(prefix.length + 1).split('/')[0];
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
- const remark = new Marked();
75
- const md = (s: string) => remark.parseInline(s) as Promise<string>;
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
- const valid = (pages as any[]).filter(p => !p.data.excludeFromNav && p.data.route !== false);
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 NavGroup = { title: string; order: number; items: NavItem[] };
102
+ type Group = {
103
+ key: string;
104
+ title: string;
105
+ order: number;
106
+ items: NavItem[];
107
+ subgroups: Group[];
108
+ };
91
109
 
92
- let items: NavItem[] = [];
93
- let groups: NavGroup[] = [];
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
- if (depth === 1) {
96
- const children = valid.filter(p => isDirectChild(p.id));
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 `/`
100
- href: `/${stripExt(p.id).replace(/\/index$/, '')}`,
101
- title: await md(p.data.title),
118
+ async function pageToItem(p: any): Promise<NavItem> {
119
+ return {
120
+ href: `/${collection}/${stripExt(p.id).replace(/\/index$/, '')}`,
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 grandchildren = valid.filter(p => isGrandchildPage(p.id));
111
- const subfolderNames = [...new Set(grandchildren.map(p => getSubfolder(p.id)))];
112
-
113
- groups = await Promise.all(subfolderNames.map(async sf => {
114
- const indexId = prefix ? `${prefix}/${sf}` : sf;
115
- const indexEntry = (pages as any[]).find(p => {
116
- const cid = stripExt(p.id);
117
- return cid === indexId || cid === `${indexId}/index`;
118
- });
119
- const sectionTitle: string = indexEntry
120
- ? await md(indexEntry.data.title)
121
- : sf.replace(/-/g, ' ').replace(/\b\w/g, (c: string) => c.toUpperCase());
122
- const sectionOrder: number = indexEntry?.data.order ?? 1000;
123
-
124
- const sfItems = grandchildren.filter(p => getSubfolder(p.id) === sf);
125
- sfItems.sort((a, b) => (a.data.order ?? 1000) - (b.data.order ?? 1000) || a.data.title.localeCompare(b.data.title));
126
-
127
- return {
128
- title: sectionTitle,
129
- order: sectionOrder,
130
- items: await Promise.all(sfItems.map(async p => ({
131
- // FIX: Replaced `/${collection}/` with a single `/`
132
- href: `/${stripExt(p.id)}`,
133
- title: await md(p.data.title),
134
- description: p.data.description,
135
- icon: p.data.icon ?? null,
136
- darkIcon: p.data.darkIcon,
137
- cardImage: p.data.cardImage,
138
- version: p.data.version,
139
- badges: p.data.badges,
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
- <div class="gap-6 mt-6 mb-6 grid grid-cols-1 not-prose lg:grid-cols-2" xmlns="http://www.w3.org/1999/xhtml">
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="full" />
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="gap-6 grid grid-cols-1 not-prose lg:grid-cols-2" xmlns="http://www.w3.org/1999/xhtml">
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="full" />
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,39 @@ import { Marked } from 'marked';
4
4
 
5
5
  interface Props {
6
6
  collection?: string;
7
- currentId: string;
8
- nextPage?: string;
9
- lastPage?: string;
7
+ prev?: string;
8
+ next?: string;
10
9
  }
11
10
 
12
- const { collection = 'docs', currentId, nextPage, lastPage } = Astro.props;
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
+ // strip the collection URL prefix so the remainder matches bare entry IDs
22
+ const pathnameRelative = pathname.startsWith(`${collection}/`)
23
+ ? pathname.slice(collection.length + 1)
24
+ : pathname;
25
+ const currentFolder = pathnameRelative.includes('/')
26
+ ? pathnameRelative.replace(/\/[^/]+$/, '')
27
+ : '';
28
+
29
+ const currentEntry = (pages as any[]).find(p => {
30
+ const cid = stripExt(p.id);
31
+ return cid === pathnameRelative || cid === `${pathnameRelative}/index`;
32
+ });
33
+
34
+ const prevPath = prevProp ?? currentEntry?.data.prev;
35
+ const nextPath = nextProp ?? currentEntry?.data.next;
36
+
24
37
  function findPage(href: string) {
25
38
  const target = href.startsWith('/')
26
- ? href.replace(new RegExp(`^\\/${collection}\\/`), '')
39
+ ? href.replace(new RegExp(`^\\/${collection}\\/`), '').replace(/^\//, '')
27
40
  : currentFolder ? `${currentFolder}/${href}` : href;
28
41
  return (pages as any[]).find(p => {
29
42
  const cid = stripExt(p.id);
@@ -36,36 +49,61 @@ function makeHref(href: string): string {
36
49
  return `/${collection}/${currentFolder ? `${currentFolder}/` : ''}${href}`;
37
50
  }
38
51
 
39
- const nextEntry = nextPage ? findPage(nextPage) : null;
40
- const lastEntry = lastPage ? findPage(lastPage) : null;
52
+ let prevEntry = prevPath ? findPage(prevPath) : null;
53
+ let prevHref: string | null = prevPath ? makeHref(prevPath) : null;
41
54
 
42
- const nextTitle: string | null = nextEntry ? await remark.parseInline(nextEntry.data.title) as string : null;
43
- const lastTitle: string | null = lastEntry ? await remark.parseInline(lastEntry.data.title) as string : null;
55
+ let nextEntry = nextPath ? findPage(nextPath) : null;
56
+ let nextHref: string | null = nextPath ? makeHref(nextPath) : null;
44
57
 
45
- const nextHref = nextPage ? makeHref(nextPage) : null;
46
- const lastHref = lastPage ? makeHref(lastPage) : null;
58
+ // fall back to sibling order when prev/next not specified via prop or frontmatter
59
+ if (!prevEntry || !nextEntry) {
60
+ const siblings = (pages as any[])
61
+ .filter(p => {
62
+ if (p.data.excludeFromNav || p.data.route === false) return false;
63
+ const cid = stripExt(p.id);
64
+ const pFolder = cid.includes('/') ? cid.replace(/\/[^/]+$/, '') : '';
65
+ const lastSeg = cid.split('/').pop()!;
66
+ return pFolder === currentFolder && lastSeg !== 'index';
67
+ })
68
+ .sort((a, b) => (a.data.order ?? 1000) - (b.data.order ?? 1000) || a.data.title.localeCompare(b.data.title));
69
+
70
+ const idx = siblings.findIndex(p => stripExt(p.id) === pathname);
71
+ if (idx > 0 && !prevEntry) {
72
+ prevEntry = siblings[idx - 1];
73
+ prevHref = `/${collection}/${stripExt(prevEntry.id).replace(/\/index$/, '')}`;
74
+ }
75
+ if (idx >= 0 && idx < siblings.length - 1 && !nextEntry) {
76
+ nextEntry = siblings[idx + 1];
77
+ nextHref = `/${collection}/${stripExt(nextEntry.id).replace(/\/index$/, '')}`;
78
+ }
79
+ }
80
+
81
+ const prevTitle: string | null = prevEntry ? await remark.parseInline(prevEntry.data.title) as string : null;
82
+ const nextTitle: string | null = nextEntry ? await remark.parseInline(nextEntry.data.title) as string : null;
47
83
  ---
48
84
 
49
85
  <nav class="not-prose mt-10 pt-6 border-t border-slate-200 dark:border-slate-700" aria-label="Page navigation">
50
86
  <div class="grid grid-cols-2 gap-4">
51
- {lastEntry ? (
52
- <a href={lastHref}
53
- class="group flex flex-col gap-1 rounded-lg border border-slate-200 dark:border-slate-700 p-4 no-underline hover:border-indigo-500 dark:hover:border-indigo-400 transition-colors">
54
- <span class="flex items-center gap-1 text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">
55
- <span aria-hidden="true">&larr;</span> Previous
56
- </span>
57
- <span class="text-sm font-medium text-slate-800 dark:text-slate-200 group-hover:text-indigo-600 dark:group-hover:text-indigo-400"
58
- set:html={lastTitle} />
87
+ {prevEntry ? (
88
+ <a href={prevHref}
89
+ 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">
90
+ <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">&larr;</span>
91
+ <div class="min-w-0">
92
+ <div class="text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">Previous</div>
93
+ <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"
94
+ set:html={prevTitle} />
95
+ </div>
59
96
  </a>
60
97
  ) : <div />}
61
98
  {nextEntry ? (
62
99
  <a href={nextHref}
63
- class="group flex flex-col gap-1 rounded-lg border border-slate-200 dark:border-slate-700 p-4 text-right no-underline hover:border-indigo-500 dark:hover:border-indigo-400 transition-colors">
64
- <span class="flex items-center justify-end gap-1 text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">
65
- Next <span aria-hidden="true">&rarr;</span>
66
- </span>
67
- <span class="text-sm font-medium text-slate-800 dark:text-slate-200 group-hover:text-indigo-600 dark:group-hover:text-indigo-400"
68
- set:html={nextTitle} />
100
+ 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">
101
+ <div class="min-w-0">
102
+ <div class="text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">Next</div>
103
+ <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"
104
+ set:html={nextTitle} />
105
+ </div>
106
+ <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">&rarr;</span>
69
107
  </a>
70
108
  ) : <div />}
71
109
  </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 (e.g. `'->'`) |
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
- <!-- children of an explicit folder -->
50
- <ChildCards folder="get-started/quickstarts/web" />
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` = grandchildren grouped under section headers |
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, resolving page titles automatically from the collection.
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 set `lastPage` / `nextPage` in front matter and let the layout render it automatically.
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
- | `currentId` | `string` | required | The current page's `entry.id` (e.g. `get-started/start-here/step-1.mdx`) |
90
- | `nextPage` | `string` | | Relative href to the next page (e.g. `step-2`) or absolute (`/docs/...`) |
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 |
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "astro-better-cards",
3
- "version": "0.2.6",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./Card.astro": "./Card.astro",
7
+ "./CardGrid.astro": "./CardGrid.astro",
7
8
  "./ChildCards.astro": "./ChildCards.astro",
8
9
  "./PageNav.astro": "./PageNav.astro"
9
10
  },