astro-better-release-notes 0.1.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/README.md +158 -0
- package/ReleaseNotes.astro +98 -0
- package/ReleaseNotesItem.astro +117 -0
- package/ReleaseNotesSelector.astro +109 -0
- package/index.js +3 -0
- package/package.json +20 -0
- package/style.css +234 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# astro-better-release-notes
|
|
2
|
+
|
|
3
|
+
Configurable release notes components for Astro with category chip filtering, sticky version navigation, and deep-linkable release note items.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install astro-better-release-notes
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Import the stylesheet once in your root layout:
|
|
12
|
+
|
|
13
|
+
```astro
|
|
14
|
+
---
|
|
15
|
+
import 'astro-better-release-notes/style.css';
|
|
16
|
+
---
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Components
|
|
20
|
+
|
|
21
|
+
- `ReleaseNotes` - renders the filter chips and the full version list
|
|
22
|
+
- `ReleaseNotesItem` - a single categorized release note row (used inside MDX content)
|
|
23
|
+
- `ReleaseNotesSelector` - a sticky version jump dropdown
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Content collection setup
|
|
28
|
+
|
|
29
|
+
Your content collection entries must have at minimum:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// src/content.config.ts
|
|
33
|
+
const releases = defineCollection({
|
|
34
|
+
schema: z.object({
|
|
35
|
+
version: z.string(),
|
|
36
|
+
date: z.coerce.date(),
|
|
37
|
+
name: z.string().optional(),
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Each `.mdx` file in the collection uses `ReleaseNotesItem`:
|
|
43
|
+
|
|
44
|
+
```mdx
|
|
45
|
+
---
|
|
46
|
+
version: "2.1.0"
|
|
47
|
+
date: 2024-05-01
|
|
48
|
+
---
|
|
49
|
+
import { ReleaseNotesItem } from 'astro-better-release-notes';
|
|
50
|
+
|
|
51
|
+
<ReleaseNotesItem category="new-feature">Added dark mode.</ReleaseNotesItem>
|
|
52
|
+
<ReleaseNotesItem category="fix" issue="42" issueBaseUrl="https://github.com/org/repo/issues/">Fixed login redirect loop.</ReleaseNotesItem>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Page component
|
|
56
|
+
|
|
57
|
+
```astro
|
|
58
|
+
---
|
|
59
|
+
import { getCollection, render } from 'astro:content';
|
|
60
|
+
import {
|
|
61
|
+
ReleaseNotes,
|
|
62
|
+
ReleaseNotesSelector,
|
|
63
|
+
DEFAULT_CATEGORIES,
|
|
64
|
+
} from 'astro-better-release-notes';
|
|
65
|
+
|
|
66
|
+
const allUpdates = await getCollection('releases');
|
|
67
|
+
const updates = allUpdates
|
|
68
|
+
.sort((a, b) => {
|
|
69
|
+
const d = b.data.date.valueOf() - a.data.date.valueOf();
|
|
70
|
+
return d !== 0 ? d : b.data.version.localeCompare(a.data.version, undefined, { numeric: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const rendered = await Promise.all(
|
|
74
|
+
updates.map(async item => {
|
|
75
|
+
const { Content } = await render(item);
|
|
76
|
+
return { data: item.data, Content };
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
<ReleaseNotesSelector updates={rendered} />
|
|
82
|
+
<ReleaseNotes updates={rendered} categories={DEFAULT_CATEGORIES} />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Custom categories
|
|
86
|
+
|
|
87
|
+
Pass a `categories` array to `ReleaseNotes` to define your own set:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
const MY_CATEGORIES = [
|
|
91
|
+
{ id: 'new-feature', label: 'New Feature', color: '#16a34a' },
|
|
92
|
+
{ id: 'fix', label: 'Fix', color: '#d97706' },
|
|
93
|
+
{ id: 'security', label: 'Security', color: '#566f98' },
|
|
94
|
+
];
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Category IDs must match the `category` prop values used in your MDX files.
|
|
98
|
+
|
|
99
|
+
## Custom ReleaseNotesItem
|
|
100
|
+
|
|
101
|
+
To inject project-specific behavior (e.g. a fixed issue base URL), pass a wrapper component via the `components` prop:
|
|
102
|
+
|
|
103
|
+
```astro
|
|
104
|
+
---
|
|
105
|
+
// src/components/MyReleaseNotesItem.astro
|
|
106
|
+
import { ReleaseNotesItem } from 'astro-better-release-notes';
|
|
107
|
+
const { issue, bonusIssue, ...rest } = Astro.props;
|
|
108
|
+
const base = 'https://github.com/org/repo/issues/';
|
|
109
|
+
---
|
|
110
|
+
<ReleaseNotesItem
|
|
111
|
+
{...rest}
|
|
112
|
+
issue={issue}
|
|
113
|
+
bonusIssue={bonusIssue}
|
|
114
|
+
issueBaseUrl={base}
|
|
115
|
+
categoryColor={rest.categoryColor}
|
|
116
|
+
categoryLabel={rest.categoryLabel}
|
|
117
|
+
><slot /></ReleaseNotesItem>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```astro
|
|
121
|
+
<ReleaseNotes
|
|
122
|
+
updates={rendered}
|
|
123
|
+
categories={MY_CATEGORIES}
|
|
124
|
+
components={{ ReleaseNotesItem: MyReleaseNotesItem }}
|
|
125
|
+
/>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Note: for the `components` injection to work with categories, you also need to pass `categoryColor` and `categoryLabel` in your wrapper. Use the exported helpers:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { getCategoryColor, getCategoryLabel, DEFAULT_CATEGORIES } from 'astro-better-release-notes';
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Or provide these values in your own wrapper by computing them from your category list.
|
|
135
|
+
|
|
136
|
+
## ReleaseNotesItem props
|
|
137
|
+
|
|
138
|
+
| Prop | Type | Description |
|
|
139
|
+
|------|------|-------------|
|
|
140
|
+
| `category` | `string` | Category ID; must match a configured category |
|
|
141
|
+
| `issue` | `string` | Issue number shown as a link at row end |
|
|
142
|
+
| `bonusIssue` | `string` | Secondary issue number |
|
|
143
|
+
| `thanks` | `string` | GitHub username to credit |
|
|
144
|
+
| `bonusThanks` | `string` | Secondary GitHub username |
|
|
145
|
+
| `since` | `string` | Version this was introduced |
|
|
146
|
+
| `resolvedIn` | `string` | Version a known issue was resolved in |
|
|
147
|
+
| `viaIssue` | `string` | Issue that resolved a known issue |
|
|
148
|
+
| `issueBaseUrl` | `string` | Base URL for issue links (no default) |
|
|
149
|
+
| `categoryColor` | `string` | CSS color for the badge (default `#71717a`) |
|
|
150
|
+
| `categoryLabel` | `string` | Human label for the badge (default: generated from id) |
|
|
151
|
+
|
|
152
|
+
## Injecting category color and label automatically
|
|
153
|
+
|
|
154
|
+
`ReleaseNotes.astro` does not automatically inject `categoryColor`/`categoryLabel` into the MDX-rendered items -- those props come from the MDX markup. To automate this, create a wrapper that looks up the category in your list and injects the values, then pass it via `components`.
|
|
155
|
+
|
|
156
|
+
## Dark mode
|
|
157
|
+
|
|
158
|
+
The stylesheet supports both `@media (prefers-color-scheme: dark)` and a `.dark` ancestor class.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
import ReleaseNotesItem from './ReleaseNotesItem.astro';
|
|
3
|
+
|
|
4
|
+
export interface Category {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
color: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_CATEGORIES: Category[] = [
|
|
11
|
+
{ id: 'new-feature', label: 'New Feature', color: '#16a34a' },
|
|
12
|
+
{ id: 'enhancement', label: 'Enhancement', color: '#2563eb' },
|
|
13
|
+
{ id: 'fix', label: 'Fix', color: '#d97706' },
|
|
14
|
+
{ id: 'security', label: 'Security', color: '#566f98' },
|
|
15
|
+
{ id: 'deprecated', label: 'Deprecated', color: '#ffcc48' },
|
|
16
|
+
{ id: 'breaking-change', label: 'Breaking Change', color: '#aaaa33' },
|
|
17
|
+
{ id: 'known-issue', label: 'Known Issue', color: '#bb4448' },
|
|
18
|
+
{ id: 'internal', label: 'Internal', color: '#71717a' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const getCategoryColor = (id: string, categories: Category[]) =>
|
|
22
|
+
categories.find(c => c.id === id)?.color ?? '#71717a';
|
|
23
|
+
|
|
24
|
+
export const getCategoryLabel = (id: string, categories: Category[]) => {
|
|
25
|
+
const cat = categories.find(c => c.id === id);
|
|
26
|
+
if (cat) return cat.label;
|
|
27
|
+
return id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface ReleaseEntry {
|
|
31
|
+
data: {
|
|
32
|
+
version: string;
|
|
33
|
+
date: Date;
|
|
34
|
+
name?: string;
|
|
35
|
+
};
|
|
36
|
+
Content: any;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Props {
|
|
40
|
+
updates: ReleaseEntry[];
|
|
41
|
+
categories?: Category[];
|
|
42
|
+
// pass { ReleaseNotesItem: YourWrapper } to inject a custom item component
|
|
43
|
+
components?: Record<string, any>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const {
|
|
47
|
+
updates,
|
|
48
|
+
categories = DEFAULT_CATEGORIES,
|
|
49
|
+
components: extraComponents = {},
|
|
50
|
+
} = Astro.props as Props;
|
|
51
|
+
|
|
52
|
+
const mergedComponents = { ReleaseNotesItem, ...extraComponents };
|
|
53
|
+
|
|
54
|
+
// generate filter CSS from the configured categories
|
|
55
|
+
const filterCSS = [
|
|
56
|
+
'.release-container:has(.filter-input:checked) .issue-row { display: none !important; }',
|
|
57
|
+
...categories.map(cat =>
|
|
58
|
+
`.release-container:has(#toggle-${cat.id}:checked) [data-category="${cat.id}"] { display: flex !important; }`
|
|
59
|
+
),
|
|
60
|
+
].join('\n');
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
<Fragment set:html={`<style>${filterCSS}</style>`} />
|
|
64
|
+
|
|
65
|
+
<div class="release-container">
|
|
66
|
+
<nav class="filter-controls" aria-label="Filter release notes by category">
|
|
67
|
+
{categories.map(cat => (
|
|
68
|
+
<label class="filter-chip" style={`--cat-color: ${cat.color}`}>
|
|
69
|
+
<input type="checkbox" id={`toggle-${cat.id}`} class="filter-input" aria-label={`Show ${cat.label}`} />
|
|
70
|
+
<span aria-hidden="true">{cat.label}</span>
|
|
71
|
+
</label>
|
|
72
|
+
))}
|
|
73
|
+
</nav>
|
|
74
|
+
|
|
75
|
+
<div class="version-list">
|
|
76
|
+
{updates.map((item) => {
|
|
77
|
+
const { Content } = item;
|
|
78
|
+
const versionId = `version-${item.data.version.replaceAll('.', '-')}`;
|
|
79
|
+
return (
|
|
80
|
+
<article id={versionId} class="version">
|
|
81
|
+
<header class="version-header">
|
|
82
|
+
<h2 class="release-note group">
|
|
83
|
+
Version {item.data.version} {item.data.name && `(${item.data.name})`}
|
|
84
|
+
<a class="anchor-link" title="copy header link" href={`#${versionId}`} aria-label={`Permalink to version ${item.data.version}`}>#</a>
|
|
85
|
+
</h2>
|
|
86
|
+
<time class="version-date" datetime={item.data.date.toISOString()}>
|
|
87
|
+
{item.data.date.toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' })}
|
|
88
|
+
</time>
|
|
89
|
+
</header>
|
|
90
|
+
|
|
91
|
+
<div class="version-content">
|
|
92
|
+
<Content components={mergedComponents} />
|
|
93
|
+
</div>
|
|
94
|
+
</article>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
export interface Props {
|
|
3
|
+
category: string;
|
|
4
|
+
issue?: string;
|
|
5
|
+
bonusIssue?: string;
|
|
6
|
+
thanks?: string;
|
|
7
|
+
bonusThanks?: string;
|
|
8
|
+
since?: string;
|
|
9
|
+
resolvedIn?: string;
|
|
10
|
+
viaIssue?: string;
|
|
11
|
+
// base URL for issue links, e.g. "https://github.com/org/repo/issues/"
|
|
12
|
+
// if omitted, issue numbers are shown as plain text
|
|
13
|
+
issueBaseUrl?: string;
|
|
14
|
+
// color for this item's badge; if omitted, uses a neutral gray
|
|
15
|
+
categoryColor?: string;
|
|
16
|
+
// human-readable category label; if omitted, generated from id
|
|
17
|
+
categoryLabel?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
category = 'fix',
|
|
22
|
+
issue,
|
|
23
|
+
bonusIssue,
|
|
24
|
+
thanks,
|
|
25
|
+
bonusThanks,
|
|
26
|
+
since,
|
|
27
|
+
resolvedIn,
|
|
28
|
+
viaIssue,
|
|
29
|
+
issueBaseUrl = '',
|
|
30
|
+
categoryColor = '#71717a',
|
|
31
|
+
categoryLabel,
|
|
32
|
+
} = Astro.props as Props;
|
|
33
|
+
|
|
34
|
+
const label = categoryLabel
|
|
35
|
+
?? category.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
36
|
+
|
|
37
|
+
const versionPart = resolvedIn
|
|
38
|
+
? `v-${resolvedIn.replaceAll('.', '-')}`
|
|
39
|
+
: (since ? `since-${since.replaceAll('.', '-')}` : '');
|
|
40
|
+
|
|
41
|
+
const issuePart = issue
|
|
42
|
+
? `issue-${issue}`
|
|
43
|
+
: (viaIssue ? `via-${viaIssue}` : '');
|
|
44
|
+
|
|
45
|
+
const thanksPart = thanks
|
|
46
|
+
? `thanks-${thanks.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
|
|
47
|
+
: '';
|
|
48
|
+
|
|
49
|
+
const baseId = [versionPart, category, issuePart, thanksPart].filter(Boolean).join('--');
|
|
50
|
+
|
|
51
|
+
// deterministic content hash for unique deep-link IDs across builds
|
|
52
|
+
const slotContent = await Astro.slots.render('default');
|
|
53
|
+
let hash = 0;
|
|
54
|
+
for (let i = 0; i < slotContent.length; i++) {
|
|
55
|
+
hash = (hash << 5) - hash + slotContent.charCodeAt(i);
|
|
56
|
+
hash |= 0;
|
|
57
|
+
}
|
|
58
|
+
const contentSlug = Math.abs(hash).toString(36).substring(0, 4);
|
|
59
|
+
const rowId = baseId ? `${baseId}--${contentSlug}` : contentSlug;
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
<div
|
|
63
|
+
id={rowId}
|
|
64
|
+
class="issue-row"
|
|
65
|
+
data-category={category}
|
|
66
|
+
style={`--cat-color: ${categoryColor}`}
|
|
67
|
+
>
|
|
68
|
+
<a
|
|
69
|
+
href={`#${rowId}`}
|
|
70
|
+
class="issue-badge"
|
|
71
|
+
aria-label={`${label} - permalink to this release note`}
|
|
72
|
+
>
|
|
73
|
+
{label}
|
|
74
|
+
</a>
|
|
75
|
+
|
|
76
|
+
<div class="issue-content">
|
|
77
|
+
<div class="rni-body">
|
|
78
|
+
<slot />
|
|
79
|
+
{resolvedIn && viaIssue && (
|
|
80
|
+
<p class="rni-meta">
|
|
81
|
+
Resolved in version{' '}
|
|
82
|
+
<a href={`#version-${resolvedIn.replaceAll('.', '-')}`}><code>{resolvedIn}</code></a>
|
|
83
|
+
{' '}via{' '}
|
|
84
|
+
{issueBaseUrl
|
|
85
|
+
? <a target="_blank" rel="noopener noreferrer" href={`${issueBaseUrl}${viaIssue}`}>#{viaIssue}</a>
|
|
86
|
+
: <span>#{viaIssue}</span>
|
|
87
|
+
}.
|
|
88
|
+
</p>
|
|
89
|
+
)}
|
|
90
|
+
{since && (
|
|
91
|
+
<p class="rni-meta">
|
|
92
|
+
Since: <a href={`#version-${since.replaceAll('.', '-')}`}><code>{since}</code></a>
|
|
93
|
+
</p>
|
|
94
|
+
)}
|
|
95
|
+
{thanks && (
|
|
96
|
+
<p class="rni-meta rni-thanks">
|
|
97
|
+
Thanks to{' '}
|
|
98
|
+
<a href={`https://github.com/${thanks}`}>@{thanks}</a>
|
|
99
|
+
{bonusThanks && (
|
|
100
|
+
<span> and <a href={`https://github.com/${bonusThanks}`}>@{bonusThanks}</a></span>
|
|
101
|
+
)}!
|
|
102
|
+
</p>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{issue && (
|
|
108
|
+
issueBaseUrl
|
|
109
|
+
? <a class="github-link" target="_blank" rel="noopener noreferrer" href={`${issueBaseUrl}${issue}`}>#{issue}</a>
|
|
110
|
+
: <span class="github-link">#{issue}</span>
|
|
111
|
+
)}
|
|
112
|
+
{bonusIssue && (
|
|
113
|
+
issueBaseUrl
|
|
114
|
+
? <a class="github-link" target="_blank" rel="noopener noreferrer" href={`${issueBaseUrl}${bonusIssue}`}>#{bonusIssue}</a>
|
|
115
|
+
: <span class="github-link">#{bonusIssue}</span>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
export interface SelectorEntry {
|
|
3
|
+
data: {
|
|
4
|
+
version: string;
|
|
5
|
+
date: Date;
|
|
6
|
+
name?: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Props {
|
|
11
|
+
updates: SelectorEntry[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { updates } = Astro.props as Props;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<div class="release-selector">
|
|
18
|
+
<label for="release-notes-version" class="sr-only">Jump to a release version</label>
|
|
19
|
+
<select
|
|
20
|
+
id="release-notes-version"
|
|
21
|
+
class="release-selector-select"
|
|
22
|
+
>
|
|
23
|
+
<option value="">Jump to a version...</option>
|
|
24
|
+
{updates.map((item) => {
|
|
25
|
+
const versionId = `version-${item.data.version.replaceAll('.', '-')}`;
|
|
26
|
+
const label = `Version ${item.data.version}${item.data.name ? ` (${item.data.name})` : ''}`;
|
|
27
|
+
return <option value={versionId}>{label}</option>;
|
|
28
|
+
})}
|
|
29
|
+
</select>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<script is:inline>
|
|
33
|
+
const initVersionSpy = () => {
|
|
34
|
+
const selectEl = document.getElementById('release-notes-version');
|
|
35
|
+
if (!selectEl) return;
|
|
36
|
+
|
|
37
|
+
const placeholderOption = selectEl.querySelector('option[value=""]');
|
|
38
|
+
const versions = Array.from(document.querySelectorAll('article.version[id]'));
|
|
39
|
+
if (versions.length === 0) return;
|
|
40
|
+
|
|
41
|
+
const HEADER_OFFSET = 210;
|
|
42
|
+
let currentActiveId = null;
|
|
43
|
+
let isTicking = false;
|
|
44
|
+
let hasUserInteracted = false;
|
|
45
|
+
|
|
46
|
+
const disablePlaceholder = () => {
|
|
47
|
+
if (placeholderOption && !placeholderOption.disabled) {
|
|
48
|
+
placeholderOption.disabled = true;
|
|
49
|
+
placeholderOption.hidden = true;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const updateScrollSpy = () => {
|
|
54
|
+
if (window.scrollY === 0 && !hasUserInteracted) return;
|
|
55
|
+
|
|
56
|
+
let activeArticle = versions[0];
|
|
57
|
+
for (let i = versions.length - 1; i >= 0; i--) {
|
|
58
|
+
if (versions[i].getBoundingClientRect().top < HEADER_OFFSET) {
|
|
59
|
+
activeArticle = versions[i];
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (activeArticle && activeArticle.id !== currentActiveId) {
|
|
65
|
+
currentActiveId = activeArticle.id;
|
|
66
|
+
selectEl.value = activeArticle.id;
|
|
67
|
+
disablePlaceholder();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const onScroll = () => {
|
|
72
|
+
if (window.scrollY > 0) hasUserInteracted = true;
|
|
73
|
+
if (!isTicking) {
|
|
74
|
+
window.requestAnimationFrame(() => {
|
|
75
|
+
updateScrollSpy();
|
|
76
|
+
isTicking = false;
|
|
77
|
+
});
|
|
78
|
+
isTicking = true;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// navigate on change; aria-live on the region announces the jump for screen readers
|
|
83
|
+
selectEl.addEventListener('change', () => {
|
|
84
|
+
hasUserInteracted = true;
|
|
85
|
+
disablePlaceholder();
|
|
86
|
+
if (selectEl.value) window.location.hash = selectEl.value;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const filterControls = document.querySelector('.filter-controls');
|
|
90
|
+
if (filterControls) {
|
|
91
|
+
filterControls.addEventListener('change', () => {
|
|
92
|
+
const targetVersion = versions.find(a => a.id === currentActiveId) || versions[0];
|
|
93
|
+
window.requestAnimationFrame(() => {
|
|
94
|
+
if (targetVersion && hasUserInteracted) {
|
|
95
|
+
const adjustment = targetVersion.getBoundingClientRect().top - HEADER_OFFSET;
|
|
96
|
+
window.scrollBy({ top: adjustment, behavior: 'instant' });
|
|
97
|
+
}
|
|
98
|
+
updateScrollSpy();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
104
|
+
updateScrollSpy();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
document.addEventListener('DOMContentLoaded', initVersionSpy);
|
|
108
|
+
document.addEventListener('astro:page-load', initVersionSpy);
|
|
109
|
+
</script>
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "astro-better-release-notes",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Configurable release notes components with category filtering and version navigation for Astro",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js",
|
|
8
|
+
"./style.css": "./style.css",
|
|
9
|
+
"./ReleaseNotes.astro": "./ReleaseNotes.astro",
|
|
10
|
+
"./ReleaseNotesItem.astro": "./ReleaseNotesItem.astro",
|
|
11
|
+
"./ReleaseNotesSelector.astro": "./ReleaseNotesSelector.astro"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"*.js",
|
|
15
|
+
"*.astro",
|
|
16
|
+
"*.css"
|
|
17
|
+
],
|
|
18
|
+
"keywords": ["astro", "release-notes", "changelog", "filter", "categories"],
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|
package/style.css
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/*
|
|
2
|
+
astro-better-release-notes/style.css
|
|
3
|
+
|
|
4
|
+
Import this in your project root layout to enable release notes styles.
|
|
5
|
+
No external dependencies -- no Tailwind, no framework.
|
|
6
|
+
|
|
7
|
+
Dark mode: supports both @media (prefers-color-scheme: dark) and a .dark
|
|
8
|
+
ancestor class (e.g. Tailwind's darkMode: 'class').
|
|
9
|
+
|
|
10
|
+
Category colors are driven by --cat-color, set inline by ReleaseNotesItem.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* ---------------------------------------------------------------
|
|
14
|
+
Filter controls (category chips)
|
|
15
|
+
--------------------------------------------------------------- */
|
|
16
|
+
|
|
17
|
+
.filter-controls {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
gap: 0.75rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.filter-input {
|
|
24
|
+
display: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.filter-chip {
|
|
28
|
+
padding: 0.5rem 1rem;
|
|
29
|
+
font-size: 0.875rem;
|
|
30
|
+
font-weight: 500;
|
|
31
|
+
border-radius: 9999px;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
transition: filter 0.15s ease-in-out;
|
|
34
|
+
opacity: 0.7;
|
|
35
|
+
filter: grayscale(100%);
|
|
36
|
+
color: #334155;
|
|
37
|
+
background-color: #f1f5f9;
|
|
38
|
+
border: 1px solid #cbd5e1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (prefers-color-scheme: dark) {
|
|
42
|
+
.filter-chip {
|
|
43
|
+
color: #cbd5e1;
|
|
44
|
+
background-color: #1e293b;
|
|
45
|
+
border-color: #475569;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.dark .filter-chip {
|
|
50
|
+
color: #cbd5e1;
|
|
51
|
+
background-color: #1e293b;
|
|
52
|
+
border-color: #475569;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.filter-chip:hover {
|
|
56
|
+
filter: grayscale(100%) brightness(0.95);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (prefers-color-scheme: dark) {
|
|
60
|
+
.filter-chip:hover {
|
|
61
|
+
filter: grayscale(100%) brightness(1.1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.dark .filter-chip:hover {
|
|
66
|
+
filter: grayscale(100%) brightness(1.1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.filter-chip:has(input:checked) {
|
|
70
|
+
filter: grayscale(0);
|
|
71
|
+
opacity: 1;
|
|
72
|
+
background-color: var(--cat-color) !important;
|
|
73
|
+
border-color: var(--cat-color) !important;
|
|
74
|
+
color: #fff !important;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ---------------------------------------------------------------
|
|
78
|
+
Release note rows
|
|
79
|
+
Note: per-category show/hide rules are generated dynamically
|
|
80
|
+
by ReleaseNotes.astro based on the configured categories.
|
|
81
|
+
--------------------------------------------------------------- */
|
|
82
|
+
|
|
83
|
+
.issue-row {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: baseline;
|
|
86
|
+
gap: 1rem;
|
|
87
|
+
margin-bottom: 2.5rem;
|
|
88
|
+
border-radius: 6px;
|
|
89
|
+
transition: border-color 0.2s;
|
|
90
|
+
padding: 6px 8px;
|
|
91
|
+
border: 2px solid transparent;
|
|
92
|
+
scroll-margin-top: 5rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@media (min-width: 768px) {
|
|
96
|
+
.issue-row {
|
|
97
|
+
margin-bottom: 1rem;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.issue-row:target {
|
|
102
|
+
border-color: rgba(148, 163, 184, 0.5);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.issue-badge {
|
|
106
|
+
width: 7rem;
|
|
107
|
+
padding: 4px 8px;
|
|
108
|
+
flex-shrink: 0;
|
|
109
|
+
text-align: center;
|
|
110
|
+
font-size: 0.75rem;
|
|
111
|
+
font-weight: 700;
|
|
112
|
+
border-radius: 2px;
|
|
113
|
+
text-transform: uppercase;
|
|
114
|
+
text-decoration: none;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
user-select: none;
|
|
117
|
+
transition: transform 0.15s ease-out;
|
|
118
|
+
transform-origin: left center;
|
|
119
|
+
background-color: color-mix(in srgb, var(--cat-color) 13%, transparent);
|
|
120
|
+
color: var(--cat-color);
|
|
121
|
+
border: 1px solid color-mix(in srgb, var(--cat-color) 27%, transparent);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.issue-badge:hover { transform: scale(1.05); }
|
|
125
|
+
.issue-badge:active { transform: scale(0.95); }
|
|
126
|
+
|
|
127
|
+
.issue-content {
|
|
128
|
+
flex: 1;
|
|
129
|
+
min-width: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.rni-body {
|
|
133
|
+
max-width: 100%;
|
|
134
|
+
overflow-x: auto;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.rni-meta {
|
|
138
|
+
font-size: 0.875rem;
|
|
139
|
+
margin-top: 0.25rem;
|
|
140
|
+
margin-bottom: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.rni-thanks {
|
|
144
|
+
font-style: italic;
|
|
145
|
+
color: #475569;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@media (prefers-color-scheme: dark) {
|
|
149
|
+
.rni-thanks { color: #94a3b8; }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.dark .rni-thanks { color: #94a3b8; }
|
|
153
|
+
|
|
154
|
+
.github-link {
|
|
155
|
+
flex-shrink: 0;
|
|
156
|
+
font-size: 0.875rem;
|
|
157
|
+
text-decoration: none;
|
|
158
|
+
opacity: 0.7;
|
|
159
|
+
color: inherit;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.github-link:hover { opacity: 1; text-decoration: underline; }
|
|
163
|
+
|
|
164
|
+
/* ---------------------------------------------------------------
|
|
165
|
+
Version articles
|
|
166
|
+
--------------------------------------------------------------- */
|
|
167
|
+
|
|
168
|
+
.version {
|
|
169
|
+
content-visibility: auto;
|
|
170
|
+
contain-intrinsic-size: 0 800px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.version-header {
|
|
174
|
+
display: flex;
|
|
175
|
+
flex-wrap: wrap;
|
|
176
|
+
align-items: baseline;
|
|
177
|
+
justify-content: space-between;
|
|
178
|
+
padding-top: 0.75rem;
|
|
179
|
+
padding-bottom: 0.75rem;
|
|
180
|
+
margin-bottom: 0.5rem;
|
|
181
|
+
border-bottom: 1px solid #e2e8f0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@media (prefers-color-scheme: dark) {
|
|
185
|
+
.version-header { border-bottom-color: #1e293b; }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.dark .version-header { border-bottom-color: #1e293b; }
|
|
189
|
+
|
|
190
|
+
.version-date {
|
|
191
|
+
font-size: 0.875rem;
|
|
192
|
+
color: #64748b;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* ---------------------------------------------------------------
|
|
196
|
+
Version selector (jump dropdown)
|
|
197
|
+
--------------------------------------------------------------- */
|
|
198
|
+
|
|
199
|
+
.release-selector {
|
|
200
|
+
position: sticky;
|
|
201
|
+
top: 2.75rem;
|
|
202
|
+
z-index: 10;
|
|
203
|
+
padding: 0.75rem 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.release-selector-select {
|
|
207
|
+
width: 100%;
|
|
208
|
+
height: 2.5rem;
|
|
209
|
+
padding: 0 0.5rem;
|
|
210
|
+
border-radius: 6px;
|
|
211
|
+
border: 2px solid #d1d5db;
|
|
212
|
+
background-color: #f3f4f6;
|
|
213
|
+
color: #111827;
|
|
214
|
+
cursor: pointer;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.release-selector-select:focus {
|
|
218
|
+
outline: 2px solid #6366f1;
|
|
219
|
+
outline-offset: 1px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@media (prefers-color-scheme: dark) {
|
|
223
|
+
.release-selector-select {
|
|
224
|
+
border-color: #4b5563;
|
|
225
|
+
background-color: #1e293b;
|
|
226
|
+
color: #cbd5e1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.dark .release-selector-select {
|
|
231
|
+
border-color: #4b5563;
|
|
232
|
+
background-color: #1e293b;
|
|
233
|
+
color: #cbd5e1;
|
|
234
|
+
}
|