astro-better-cards 0.1.0 → 0.2.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 +148 -0
- package/PageNav.astro +72 -0
- package/README.md +66 -47
- package/package.json +7 -2
package/ChildCards.astro
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import { Marked } from 'marked';
|
|
4
|
+
import Card from './Card.astro';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
collection?: string;
|
|
8
|
+
folder?: string;
|
|
9
|
+
depth?: 1 | 2;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { collection = 'docs', folder, depth = 1 } = Astro.props;
|
|
13
|
+
|
|
14
|
+
const pages = await getCollection(collection as any);
|
|
15
|
+
|
|
16
|
+
// Compute collection-relative prefix (no leading/trailing slashes, no extension)
|
|
17
|
+
let prefix: string;
|
|
18
|
+
if (folder !== undefined) {
|
|
19
|
+
if (folder.startsWith('/')) {
|
|
20
|
+
const noSlash = folder.replace(/^\/+/, '');
|
|
21
|
+
prefix = noSlash.startsWith(`${collection}/`) ? noSlash.slice(collection.length + 1) : noSlash;
|
|
22
|
+
} else if (folder.startsWith('./') || folder.startsWith('../')) {
|
|
23
|
+
const pathname = Astro.url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.html$/, '');
|
|
24
|
+
const pagePrefix = pathname.startsWith(`${collection}/`) ? pathname.slice(collection.length + 1) : pathname;
|
|
25
|
+
const pageFolder = pagePrefix.includes('/') ? pagePrefix.replace(/\/[^/]+$/, '') : '';
|
|
26
|
+
const parts = pageFolder ? pageFolder.split('/') : [];
|
|
27
|
+
for (const seg of folder.split('/')) {
|
|
28
|
+
if (seg === '..') parts.pop();
|
|
29
|
+
else if (seg !== '.' && seg !== '') parts.push(seg);
|
|
30
|
+
}
|
|
31
|
+
prefix = parts.join('/');
|
|
32
|
+
} else {
|
|
33
|
+
prefix = folder;
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
const pathname = Astro.url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.html$/, '');
|
|
37
|
+
prefix = pathname.startsWith(`${collection}/`) ? pathname.slice(collection.length + 1) : pathname;
|
|
38
|
+
}
|
|
39
|
+
prefix = prefix.replace(/^\/+|\/+$/g, '');
|
|
40
|
+
|
|
41
|
+
function stripExt(id: string): string {
|
|
42
|
+
return id.replace(/\.(md|mdx)$/i, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isDirectChild(id: string): boolean {
|
|
46
|
+
const cid = stripExt(id);
|
|
47
|
+
if (!prefix) return !cid.includes('/') && cid !== 'index';
|
|
48
|
+
if (!cid.startsWith(`${prefix}/`)) return false;
|
|
49
|
+
const rem = cid.slice(prefix.length + 1);
|
|
50
|
+
return rem.length > 0 && !rem.includes('/') && rem !== 'index';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isGrandchildPage(id: string): boolean {
|
|
54
|
+
const cid = stripExt(id);
|
|
55
|
+
if (!cid.startsWith(`${prefix}/`)) return false;
|
|
56
|
+
const rem = cid.slice(prefix.length + 1);
|
|
57
|
+
const parts = rem.split('/');
|
|
58
|
+
return parts.length === 2 && parts[1] !== 'index';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getSubfolder(id: string): string {
|
|
62
|
+
return stripExt(id).slice(prefix.length + 1).split('/')[0];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const remark = new Marked();
|
|
66
|
+
const md = (s: string) => remark.parseInline(s) as Promise<string>;
|
|
67
|
+
|
|
68
|
+
const valid = (pages as any[]).filter(p => !p.data.excludeFromNav && p.data.route !== false);
|
|
69
|
+
|
|
70
|
+
type NavItem = {
|
|
71
|
+
href: string;
|
|
72
|
+
title: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
icon?: string | null;
|
|
75
|
+
darkIcon?: string;
|
|
76
|
+
cardImage?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type NavGroup = { title: string; order: number; items: NavItem[] };
|
|
80
|
+
|
|
81
|
+
let items: NavItem[] = [];
|
|
82
|
+
let groups: NavGroup[] = [];
|
|
83
|
+
|
|
84
|
+
if (depth === 1) {
|
|
85
|
+
const children = valid.filter(p => isDirectChild(p.id));
|
|
86
|
+
children.sort((a, b) => (a.data.order ?? 1000) - (b.data.order ?? 1000) || a.data.title.localeCompare(b.data.title));
|
|
87
|
+
items = await Promise.all(children.map(async p => ({
|
|
88
|
+
href: `/${collection}/${stripExt(p.id).replace(/\/index$/, '')}`,
|
|
89
|
+
title: await md(p.data.title),
|
|
90
|
+
description: p.data.description,
|
|
91
|
+
icon: p.data.icon ?? null,
|
|
92
|
+
darkIcon: p.data.darkIcon,
|
|
93
|
+
cardImage: p.data.cardImage,
|
|
94
|
+
})));
|
|
95
|
+
} else {
|
|
96
|
+
const grandchildren = valid.filter(p => isGrandchildPage(p.id));
|
|
97
|
+
const subfolderNames = [...new Set(grandchildren.map(p => getSubfolder(p.id)))];
|
|
98
|
+
|
|
99
|
+
groups = await Promise.all(subfolderNames.map(async sf => {
|
|
100
|
+
const indexEntry = (pages as any[]).find(p => stripExt(p.id) === `${prefix}/${sf}/index`);
|
|
101
|
+
const sectionTitle: string = indexEntry
|
|
102
|
+
? await md(indexEntry.data.title)
|
|
103
|
+
: sf.replace(/-/g, ' ').replace(/\b\w/g, (c: string) => c.toUpperCase());
|
|
104
|
+
const sectionOrder: number = indexEntry?.data.order ?? 1000;
|
|
105
|
+
|
|
106
|
+
const sfItems = grandchildren.filter(p => getSubfolder(p.id) === sf);
|
|
107
|
+
sfItems.sort((a, b) => (a.data.order ?? 1000) - (b.data.order ?? 1000) || a.data.title.localeCompare(b.data.title));
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
title: sectionTitle,
|
|
111
|
+
order: sectionOrder,
|
|
112
|
+
items: await Promise.all(sfItems.map(async p => ({
|
|
113
|
+
href: `/${collection}/${stripExt(p.id)}`,
|
|
114
|
+
title: await md(p.data.title),
|
|
115
|
+
description: p.data.description,
|
|
116
|
+
icon: p.data.icon ?? null,
|
|
117
|
+
darkIcon: p.data.darkIcon,
|
|
118
|
+
cardImage: p.data.cardImage,
|
|
119
|
+
}))),
|
|
120
|
+
};
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
groups.sort((a, b) => a.order - b.order);
|
|
124
|
+
}
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
{depth === 1 ? (
|
|
128
|
+
<div class="gap-6 mt-6 grid grid-cols-1 not-prose lg:grid-cols-2" xmlns="http://www.w3.org/1999/xhtml">
|
|
129
|
+
{items.map(item => (
|
|
130
|
+
<Card href={item.href} title={item.title} description={item.description}
|
|
131
|
+
icon={item.icon} darkIcon={item.darkIcon} cardImage={item.cardImage} variant="full" />
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
) : (
|
|
135
|
+
<Fragment>
|
|
136
|
+
{groups.map(group => (
|
|
137
|
+
<Fragment>
|
|
138
|
+
<h2 class="mt-10 mb-2 text-xl font-semibold not-prose" set:html={group.title} />
|
|
139
|
+
<div class="gap-6 grid grid-cols-1 not-prose lg:grid-cols-2" xmlns="http://www.w3.org/1999/xhtml">
|
|
140
|
+
{group.items.map(item => (
|
|
141
|
+
<Card href={item.href} title={item.title} description={item.description}
|
|
142
|
+
icon={item.icon} darkIcon={item.darkIcon} cardImage={item.cardImage} variant="full" />
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
</Fragment>
|
|
146
|
+
))}
|
|
147
|
+
</Fragment>
|
|
148
|
+
)}
|
package/PageNav.astro
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import { Marked } from 'marked';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
collection?: string;
|
|
7
|
+
currentId: string;
|
|
8
|
+
nextPage?: string;
|
|
9
|
+
lastPage?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { collection = 'docs', currentId, nextPage, lastPage } = Astro.props;
|
|
13
|
+
|
|
14
|
+
const pages = await getCollection(collection as any);
|
|
15
|
+
const remark = new Marked();
|
|
16
|
+
|
|
17
|
+
const currentClean = currentId.replace(/\.(md|mdx)$/i, '');
|
|
18
|
+
const currentFolder = currentClean.includes('/') ? currentClean.replace(/\/[^/]+$/, '') : '';
|
|
19
|
+
|
|
20
|
+
function stripExt(id: string): string {
|
|
21
|
+
return id.replace(/\.(md|mdx)$/i, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findPage(href: string) {
|
|
25
|
+
const target = href.startsWith('/')
|
|
26
|
+
? href.replace(new RegExp(`^\\/${collection}\\/`), '')
|
|
27
|
+
: currentFolder ? `${currentFolder}/${href}` : href;
|
|
28
|
+
return (pages as any[]).find(p => {
|
|
29
|
+
const cid = stripExt(p.id);
|
|
30
|
+
return cid === target || cid === `${target}/index`;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeHref(href: string): string {
|
|
35
|
+
if (href.startsWith('/')) return href;
|
|
36
|
+
return `/${collection}/${currentFolder ? `${currentFolder}/` : ''}${href}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const nextEntry = nextPage ? findPage(nextPage) : null;
|
|
40
|
+
const lastEntry = lastPage ? findPage(lastPage) : null;
|
|
41
|
+
|
|
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;
|
|
44
|
+
|
|
45
|
+
const nextHref = nextPage ? makeHref(nextPage) : null;
|
|
46
|
+
const lastHref = lastPage ? makeHref(lastPage) : null;
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
<nav class="not-prose mt-10 pt-6 border-t border-slate-200 dark:border-slate-700" aria-label="Page navigation">
|
|
50
|
+
<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">←</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} />
|
|
59
|
+
</a>
|
|
60
|
+
) : <div />}
|
|
61
|
+
{nextEntry ? (
|
|
62
|
+
<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">→</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} />
|
|
69
|
+
</a>
|
|
70
|
+
) : <div />}
|
|
71
|
+
</div>
|
|
72
|
+
</nav>
|
package/README.md
CHANGED
|
@@ -1,72 +1,91 @@
|
|
|
1
1
|
# astro-better-cards
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Components for cards and navigation in Astro documentation sites.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Card
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
npm install astro-better-cards
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
7
|
+
A single card with three display variants.
|
|
12
8
|
|
|
13
|
-
```
|
|
9
|
+
```astro
|
|
14
10
|
import Card from 'astro-better-cards/Card.astro';
|
|
11
|
+
|
|
12
|
+
<Card
|
|
13
|
+
href="/docs/section/page"
|
|
14
|
+
title="Page Title"
|
|
15
|
+
description="Optional description text."
|
|
16
|
+
icon="/img/icons/example.svg"
|
|
17
|
+
variant="full"
|
|
18
|
+
/>
|
|
15
19
|
```
|
|
16
20
|
|
|
17
|
-
###
|
|
21
|
+
### Props
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
| Prop | Type | Default | Description |
|
|
24
|
+
|------|------|---------|-------------|
|
|
25
|
+
| `href` | `string` | required | Link destination |
|
|
26
|
+
| `title` | `string` | required | Card title (HTML allowed) |
|
|
27
|
+
| `description` | `string` | — | Subtitle text |
|
|
28
|
+
| `icon` | `string` | — | Icon image URL (light mode) |
|
|
29
|
+
| `darkIcon` | `string` | — | Icon image URL (dark mode) |
|
|
30
|
+
| `cardImage` | `string` | — | Hero image URL |
|
|
31
|
+
| `variant` | `'full' \| 'compact' \| 'quickstart'` | `'full'` | Display style |
|
|
32
|
+
| `comingSoon` | `boolean` | `false` | Greys out the card |
|
|
33
|
+
| `label` | `string` | — | Small label text (e.g. `'->'`) |
|
|
34
|
+
| `labelFirst` | `boolean` | `false` | Render label before title |
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
<Card href="/docs/section" title="Section Title" description="A short description." />
|
|
23
|
-
```
|
|
36
|
+
## ChildCards
|
|
24
37
|
|
|
25
|
-
|
|
38
|
+
Automatically renders cards for child pages of the current section, pulled from an Astro content collection.
|
|
26
39
|
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
```
|
|
40
|
+
```astro
|
|
41
|
+
import ChildCards from 'astro-better-cards/ChildCards.astro';
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
<!-- direct children of the current page's folder -->
|
|
44
|
+
<ChildCards />
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
<
|
|
46
|
+
<!-- grandchildren grouped by subfolder with section headers -->
|
|
47
|
+
<ChildCards depth={2} />
|
|
48
|
+
|
|
49
|
+
<!-- children of an explicit folder -->
|
|
50
|
+
<ChildCards folder="get-started/quickstarts/web" />
|
|
51
|
+
|
|
52
|
+
<!-- children from a different collection -->
|
|
53
|
+
<ChildCards collection="articles" />
|
|
35
54
|
```
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
Typically used via the `sectionIndex: true` front matter field (rendered automatically by the layout), or imported directly in MDX for more control.
|
|
38
57
|
|
|
39
|
-
|
|
58
|
+
### Props
|
|
40
59
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
| Prop | Type | Default | Description |
|
|
61
|
+
|------|------|---------|-------------|
|
|
62
|
+
| `collection` | `string` | `'docs'` | Astro content collection name |
|
|
63
|
+
| `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 |
|
|
45
65
|
|
|
46
|
-
|
|
66
|
+
Pages with `excludeFromNav: true` or `route: false` are excluded.
|
|
47
67
|
|
|
48
|
-
|
|
68
|
+
## PageNav
|
|
49
69
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
Renders previous/next navigation links at the bottom of a page, resolving page titles automatically from the collection.
|
|
71
|
+
|
|
72
|
+
```astro
|
|
73
|
+
import PageNav from 'astro-better-cards/PageNav.astro';
|
|
54
74
|
|
|
55
|
-
|
|
75
|
+
<PageNav
|
|
76
|
+
currentId={entry.id}
|
|
77
|
+
lastPage="step-1"
|
|
78
|
+
nextPage="step-3"
|
|
79
|
+
/>
|
|
80
|
+
```
|
|
56
81
|
|
|
57
|
-
|
|
58
|
-
|------|----------|------|---------|-------------|
|
|
59
|
-
| `href` | all | `string` | — | Link destination. |
|
|
60
|
-
| `title` | all | `string` | — | Card title. Rendered with `set:html` so HTML entities and inline markup work. |
|
|
61
|
-
| `variant` | all | `'full' \| 'compact' \| 'quickstart'` | `'full'` | Which card style to render. |
|
|
62
|
-
| `description` | `full` | `string` | — | Optional subtitle below the title. |
|
|
63
|
-
| `icon` | `full`, `quickstart` | `string` | — | Icon image URL. Hidden in dark mode when `darkIcon` is also set. |
|
|
64
|
-
| `darkIcon` | `full` | `string` | — | Dark-mode icon image URL. |
|
|
65
|
-
| `cardImage` | `full` | `string` | — | Banner image displayed above the card content. |
|
|
66
|
-
| `label` | `compact` | `string` | — | Short badge text (e.g. `"->"`, `"<-"`, a step number). |
|
|
67
|
-
| `labelFirst` | `compact` | `boolean` | `false` | When `true`, the label appears after the title (right-aligned). |
|
|
68
|
-
| `comingSoon` | `quickstart` | `boolean` | `false` | Greys out the card and appends `* Coming Soon` to the title. |
|
|
82
|
+
Or set `lastPage` / `nextPage` in front matter and let the layout render it automatically.
|
|
69
83
|
|
|
70
|
-
|
|
84
|
+
### Props
|
|
71
85
|
|
|
72
|
-
|
|
86
|
+
| Prop | Type | Default | Description |
|
|
87
|
+
|------|------|---------|-------------|
|
|
88
|
+
| `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 |
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-better-cards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
"./Card.astro": "./Card.astro"
|
|
6
|
+
"./Card.astro": "./Card.astro",
|
|
7
|
+
"./ChildCards.astro": "./ChildCards.astro",
|
|
8
|
+
"./PageNav.astro": "./PageNav.astro"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"marked": "^12.0.0"
|
|
7
12
|
},
|
|
8
13
|
"peerDependencies": {
|
|
9
14
|
"astro": ">=4.0.0"
|