astro-better-release-notes 0.1.0 → 0.1.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/README.md CHANGED
@@ -18,9 +18,9 @@ import 'astro-better-release-notes/style.css';
18
18
 
19
19
  ## Components
20
20
 
21
- - `ReleaseNotes` - renders the filter chips and the full version list
21
+ - `ReleaseNotes` - renders a unified sticky header (version selector + category filter chips) and the full version list
22
22
  - `ReleaseNotesItem` - a single categorized release note row (used inside MDX content)
23
- - `ReleaseNotesSelector` - a sticky version jump dropdown
23
+ - `ReleaseNotesSelector` - a standalone version jump dropdown (for custom layouts that need it separately)
24
24
 
25
25
  ## Usage
26
26
 
@@ -54,14 +54,12 @@ import { ReleaseNotesItem } from 'astro-better-release-notes';
54
54
 
55
55
  ### Page component
56
56
 
57
+ `ReleaseNotes` renders everything in one call: a sticky header containing the version selector and filter chips, then the full version list below.
58
+
57
59
  ```astro
58
60
  ---
59
61
  import { getCollection, render } from 'astro:content';
60
- import {
61
- ReleaseNotes,
62
- ReleaseNotesSelector,
63
- DEFAULT_CATEGORIES,
64
- } from 'astro-better-release-notes';
62
+ import { ReleaseNotes, DEFAULT_CATEGORIES } from 'astro-better-release-notes';
65
63
 
66
64
  const allUpdates = await getCollection('releases');
67
65
  const updates = allUpdates
@@ -78,10 +76,60 @@ const rendered = await Promise.all(
78
76
  );
79
77
  ---
80
78
 
81
- <ReleaseNotesSelector updates={rendered} />
82
79
  <ReleaseNotes updates={rendered} categories={DEFAULT_CATEGORIES} />
83
80
  ```
84
81
 
82
+ ### ReleaseNotes props
83
+
84
+ | Prop | Type | Default | Description |
85
+ |------|------|---------|-------------|
86
+ | `updates` | `ReleaseEntry[]` | required | Pre-rendered version entries |
87
+ | `categories` | `Category[]` | `DEFAULT_CATEGORIES` | Category definitions |
88
+ | `components` | `Record<string, any>` | `{}` | Component overrides (e.g. custom `ReleaseNotesItem`) |
89
+ | `showSelector` | `boolean` | `true` | Set `false` to hide the version jump dropdown |
90
+
91
+ ### Sticky offset and scroll margins
92
+
93
+ Two CSS custom properties control the positioning:
94
+
95
+ `--release-notes-top` — how far the sticky header sits from the viewport top (should match your site's fixed nav height). Default: `3.5rem`.
96
+
97
+ `--release-notes-scroll-margin` — the `scroll-margin-top` applied to both version articles and individual release note rows when navigating via anchor links. Should be at least as large as your total sticky height (nav + release-notes sticky header). Default: `5rem`.
98
+
99
+ ```css
100
+ /* in your root layout or global CSS */
101
+ html {
102
+ --release-notes-top: 56px;
103
+ --release-notes-scroll-margin: 6rem; /* nav + sticky header height */
104
+ }
105
+
106
+ @media (min-width: 1024px) {
107
+ html {
108
+ --release-notes-top: 80px;
109
+ --release-notes-scroll-margin: 11rem;
110
+ }
111
+ }
112
+ ```
113
+
114
+ Set both variables on `html` or any ancestor of `.release-container`.
115
+
116
+ ### Link colors
117
+
118
+ The github issue links and category badge hover colors are configurable:
119
+
120
+ | Variable | Default | Description |
121
+ |----------|---------|-------------|
122
+ | `--release-notes-link-color` | `#4338ca` (indigo-700) | Issue link color |
123
+ | `--release-notes-link-hover-color` | `#9a3412` (orange-800) | Hover color for links and badge |
124
+ | `--release-notes-link-color-dark` | `#818cf8` (indigo-400) | Dark mode link color |
125
+ | `--release-notes-link-hover-color-dark` | `#fed7aa` (orange-200) | Dark mode hover color |
126
+
127
+ ### Mobile filter behavior
128
+
129
+ On narrow screens the category filter chips are hidden behind a "Filter" toggle button that sits next to the version selector. Tapping it expands the chips below the selector row. When any filter is active, the button changes color as a visual indicator.
130
+
131
+ On screens 768px and wider, the chips are always visible.
132
+
85
133
  ## Custom categories
86
134
 
87
135
  Pass a `categories` array to `ReleaseNotes` to define your own set:
@@ -131,8 +179,6 @@ Note: for the `components` injection to work with categories, you also need to p
131
179
  import { getCategoryColor, getCategoryLabel, DEFAULT_CATEGORIES } from 'astro-better-release-notes';
132
180
  ```
133
181
 
134
- Or provide these values in your own wrapper by computing them from your category list.
135
-
136
182
  ## ReleaseNotesItem props
137
183
 
138
184
  | Prop | Type | Description |
@@ -153,6 +199,28 @@ Or provide these values in your own wrapper by computing them from your category
153
199
 
154
200
  `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
201
 
202
+ ## Standalone ReleaseNotesSelector
203
+
204
+ `ReleaseNotesSelector` is available as a standalone component for custom layouts that need the version dropdown without the rest of the notes. It is not sticky by default; wrap it in your own sticky container if needed.
205
+
206
+ ```astro
207
+ import { ReleaseNotesSelector } from 'astro-better-release-notes';
208
+ ---
209
+ <div style="position: sticky; top: 56px;">
210
+ <ReleaseNotesSelector {updates} />
211
+ </div>
212
+ ```
213
+
156
214
  ## Dark mode
157
215
 
158
- The stylesheet supports both `@media (prefers-color-scheme: dark)` and a `.dark` ancestor class.
216
+ The stylesheet uses a `.dark` ancestor class (compatible with Tailwind's `darkMode: 'class'`). To also enable automatic OS-based dark mode, add to your project CSS:
217
+
218
+ ```css
219
+ @media (prefers-color-scheme: dark) {
220
+ html:not([data-theme="light"]) {
221
+ /* copy the .dark rules you need, or add the .dark class via JS */
222
+ }
223
+ }
224
+ ```
225
+
226
+ Most Tailwind setups initialize the `.dark` class from OS preference on first load, so no extra CSS is needed.
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import ReleaseNotesItem from './ReleaseNotesItem.astro';
3
+ import ReleaseNotesSelector from './ReleaseNotesSelector.astro';
3
4
 
4
5
  export interface Category {
5
6
  id: string;
@@ -41,12 +42,15 @@ export interface Props {
41
42
  categories?: Category[];
42
43
  // pass { ReleaseNotesItem: YourWrapper } to inject a custom item component
43
44
  components?: Record<string, any>;
45
+ // set false to hide the version jump dropdown
46
+ showSelector?: boolean;
44
47
  }
45
48
 
46
49
  const {
47
50
  updates,
48
51
  categories = DEFAULT_CATEGORIES,
49
52
  components: extraComponents = {},
53
+ showSelector = true,
50
54
  } = Astro.props as Props;
51
55
 
52
56
  const mergedComponents = { ReleaseNotesItem, ...extraComponents };
@@ -63,14 +67,31 @@ const filterCSS = [
63
67
  <Fragment set:html={`<style>${filterCSS}</style>`} />
64
68
 
65
69
  <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>
70
+ <div class="release-sticky-header">
71
+ {showSelector && <ReleaseNotesSelector {updates} />}
72
+
73
+ <button
74
+ class="filter-toggle-btn"
75
+ aria-expanded="false"
76
+ aria-controls="release-notes-filters"
77
+ >
78
+ <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor">
79
+ <path d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/>
80
+ </svg>
81
+ Filter
82
+ </button>
83
+
84
+ <nav id="release-notes-filters" class="filter-controls" aria-label="Filter release notes by category">
85
+ {categories.map(cat => (
86
+ <label class="filter-chip" style={`--cat-color: ${cat.color}`}>
87
+ <input type="checkbox" id={`toggle-${cat.id}`} class="filter-input" aria-label={`Show ${cat.label}`} />
88
+ <span aria-hidden="true">{cat.label}</span>
89
+ </label>
90
+ ))}
91
+ </nav>
92
+
93
+ <noscript><style>@media (max-width: 767px) { #release-notes-filters { display: flex !important; flex-wrap: wrap; } .filter-toggle-btn { display: none !important; } }</style></noscript>
94
+ </div>
74
95
 
75
96
  <div class="version-list">
76
97
  {updates.map((item) => {
@@ -96,3 +117,18 @@ const filterCSS = [
96
117
  })}
97
118
  </div>
98
119
  </div>
120
+
121
+ <script is:inline>
122
+ const initFilterToggle = () => {
123
+ const btn = document.querySelector('.filter-toggle-btn');
124
+ const nav = document.getElementById('release-notes-filters');
125
+ if (!btn || !nav) return;
126
+ btn.addEventListener('click', () => {
127
+ const expanded = btn.getAttribute('aria-expanded') === 'true';
128
+ btn.setAttribute('aria-expanded', String(!expanded));
129
+ nav.classList.toggle('is-open', !expanded);
130
+ });
131
+ };
132
+ document.addEventListener('DOMContentLoaded', initFilterToggle);
133
+ document.addEventListener('astro:page-load', initFilterToggle);
134
+ </script>
@@ -67,7 +67,7 @@ const rowId = baseId ? `${baseId}--${contentSlug}` : contentSlug;
67
67
  >
68
68
  <a
69
69
  href={`#${rowId}`}
70
- class="issue-badge"
70
+ class="issue-badge not-prose"
71
71
  aria-label={`${label} - permalink to this release note`}
72
72
  >
73
73
  {label}
@@ -106,12 +106,12 @@ const rowId = baseId ? `${baseId}--${contentSlug}` : contentSlug;
106
106
 
107
107
  {issue && (
108
108
  issueBaseUrl
109
- ? <a class="github-link" target="_blank" rel="noopener noreferrer" href={`${issueBaseUrl}${issue}`}>#{issue}</a>
109
+ ? <a class="github-link not-prose" target="_blank" rel="noopener noreferrer" href={`${issueBaseUrl}${issue}`}>#{issue}</a>
110
110
  : <span class="github-link">#{issue}</span>
111
111
  )}
112
112
  {bonusIssue && (
113
113
  issueBaseUrl
114
- ? <a class="github-link" target="_blank" rel="noopener noreferrer" href={`${issueBaseUrl}${bonusIssue}`}>#{bonusIssue}</a>
114
+ ? <a class="github-link not-prose" target="_blank" rel="noopener noreferrer" href={`${issueBaseUrl}${bonusIssue}`}>#{bonusIssue}</a>
115
115
  : <span class="github-link">#{bonusIssue}</span>
116
116
  )}
117
117
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-better-release-notes",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Configurable release notes components with category filtering and version navigation for Astro",
5
5
  "type": "module",
6
6
  "exports": {
package/style.css CHANGED
@@ -4,20 +4,151 @@
4
4
  Import this in your project root layout to enable release notes styles.
5
5
  No external dependencies -- no Tailwind, no framework.
6
6
 
7
- Dark mode: supports both @media (prefers-color-scheme: dark) and a .dark
8
- ancestor class (e.g. Tailwind's darkMode: 'class').
7
+ Dark mode: uses a .dark ancestor class (e.g. Tailwind's darkMode: 'class').
8
+ See README for enabling OS-based dark mode without a class toggle.
9
9
 
10
10
  Category colors are driven by --cat-color, set inline by ReleaseNotesItem.
11
+
12
+ Sticky offset configuration:
13
+ Set --release-notes-top on :root (or any ancestor of .release-sticky-header)
14
+ to match your site's fixed navigation height. Defaults to 3.5rem (56px).
15
+
16
+ Example:
17
+ :root { --release-notes-top: 57px; }
18
+ @media (min-width: 1024px) { :root { --release-notes-top: 80px; } }
11
19
  */
12
20
 
21
+ /* ---------------------------------------------------------------
22
+ Sticky header (selector + filter toggle + chips)
23
+ --------------------------------------------------------------- */
24
+
25
+ .release-sticky-header {
26
+ position: sticky;
27
+ top: var(--release-notes-top, 3.5rem);
28
+ z-index: 11;
29
+ display: flex;
30
+ flex-wrap: wrap;
31
+ align-items: center;
32
+ gap: 0.5rem;
33
+ padding: 0.5rem 0;
34
+ background-color: white;
35
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
36
+ margin-bottom: 0.5rem;
37
+ }
38
+
39
+ .dark .release-sticky-header {
40
+ background-color: #0f172a;
41
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06);
42
+ }
43
+
44
+ /* ---------------------------------------------------------------
45
+ Version selector (jump dropdown)
46
+ --------------------------------------------------------------- */
47
+
48
+ .release-selector {
49
+ flex: 1;
50
+ min-width: 200px;
51
+ }
52
+
53
+ .release-selector-select {
54
+ width: 100%;
55
+ height: 2.5rem;
56
+ padding: 0 0.5rem;
57
+ border-radius: 6px;
58
+ border: 2px solid #d1d5db;
59
+ background-color: #f3f4f6;
60
+ color: #111827;
61
+ cursor: pointer;
62
+ }
63
+
64
+ .release-selector-select:focus {
65
+ outline: 2px solid #6366f1;
66
+ outline-offset: 1px;
67
+ }
68
+
69
+ .dark .release-selector-select {
70
+ border-color: #4b5563;
71
+ background-color: #1e293b;
72
+ color: #cbd5e1;
73
+ }
74
+
75
+ /* ---------------------------------------------------------------
76
+ Filter toggle button (mobile only)
77
+ --------------------------------------------------------------- */
78
+
79
+ .filter-toggle-btn {
80
+ flex: 0 0 auto;
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 0.375rem;
84
+ padding: 0.4375rem 0.75rem;
85
+ height: 2.5rem;
86
+ font-size: 0.875rem;
87
+ font-weight: 500;
88
+ border-radius: 6px;
89
+ border: 2px solid #d1d5db;
90
+ background-color: #f3f4f6;
91
+ color: #374151;
92
+ cursor: pointer;
93
+ white-space: nowrap;
94
+ }
95
+
96
+ .filter-toggle-btn:focus {
97
+ outline: 2px solid #6366f1;
98
+ outline-offset: 1px;
99
+ }
100
+
101
+ .dark .filter-toggle-btn {
102
+ border-color: #4b5563;
103
+ background-color: #1e293b;
104
+ color: #cbd5e1;
105
+ }
106
+
107
+ /* indicate active filters */
108
+ .release-container:has(.filter-input:checked) .filter-toggle-btn {
109
+ border-color: #6366f1;
110
+ background-color: color-mix(in srgb, #6366f1 12%, transparent);
111
+ color: #6366f1;
112
+ }
113
+
114
+ .dark .release-container:has(.filter-input:checked) .filter-toggle-btn {
115
+ border-color: #818cf8;
116
+ color: #818cf8;
117
+ background-color: color-mix(in srgb, #818cf8 15%, transparent);
118
+ }
119
+
120
+ @media (min-width: 768px) {
121
+ .filter-toggle-btn {
122
+ display: none;
123
+ }
124
+ }
125
+
13
126
  /* ---------------------------------------------------------------
14
127
  Filter controls (category chips)
15
128
  --------------------------------------------------------------- */
16
129
 
130
+ /* hidden on mobile by default; shown via .is-open (JS) or desktop breakpoint */
131
+ @media (max-width: 767px) {
132
+ .filter-controls {
133
+ display: none;
134
+ }
135
+
136
+ .filter-controls.is-open {
137
+ display: flex;
138
+ }
139
+ }
140
+
141
+ @media (min-width: 768px) {
142
+ .filter-controls {
143
+ display: flex;
144
+ }
145
+ }
146
+
17
147
  .filter-controls {
18
- display: flex;
148
+ flex: 0 0 100%;
19
149
  flex-wrap: wrap;
20
150
  gap: 0.75rem;
151
+ padding: 0.25rem 0;
21
152
  }
22
153
 
23
154
  .filter-input {
@@ -38,14 +169,6 @@
38
169
  border: 1px solid #cbd5e1;
39
170
  }
40
171
 
41
- @media (prefers-color-scheme: dark) {
42
- .filter-chip {
43
- color: #cbd5e1;
44
- background-color: #1e293b;
45
- border-color: #475569;
46
- }
47
- }
48
-
49
172
  .dark .filter-chip {
50
173
  color: #cbd5e1;
51
174
  background-color: #1e293b;
@@ -56,12 +179,6 @@
56
179
  filter: grayscale(100%) brightness(0.95);
57
180
  }
58
181
 
59
- @media (prefers-color-scheme: dark) {
60
- .filter-chip:hover {
61
- filter: grayscale(100%) brightness(1.1);
62
- }
63
- }
64
-
65
182
  .dark .filter-chip:hover {
66
183
  filter: grayscale(100%) brightness(1.1);
67
184
  }
@@ -89,7 +206,7 @@
89
206
  transition: border-color 0.2s;
90
207
  padding: 6px 8px;
91
208
  border: 2px solid transparent;
92
- scroll-margin-top: 5rem;
209
+ scroll-margin-top: var(--release-notes-scroll-margin, 5rem);
93
210
  }
94
211
 
95
212
  @media (min-width: 768px) {
@@ -114,15 +231,22 @@
114
231
  text-decoration: none;
115
232
  cursor: pointer;
116
233
  user-select: none;
117
- transition: transform 0.15s ease-out;
234
+ transition: transform 0.15s ease-out, color 0.15s ease-out;
118
235
  transform-origin: left center;
119
236
  background-color: color-mix(in srgb, var(--cat-color) 13%, transparent);
120
237
  color: var(--cat-color);
121
238
  border: 1px solid color-mix(in srgb, var(--cat-color) 27%, transparent);
122
239
  }
123
240
 
124
- .issue-badge:hover { transform: scale(1.05); }
241
+ .issue-badge:hover {
242
+ transform: scale(1.05);
243
+ color: var(--release-notes-link-hover-color, #9a3412);
244
+ text-decoration: none;
245
+ }
125
246
  .issue-badge:active { transform: scale(0.95); }
247
+ .dark .issue-badge:hover {
248
+ color: var(--release-notes-link-hover-color-dark, #fed7aa);
249
+ }
126
250
 
127
251
  .issue-content {
128
252
  flex: 1;
@@ -145,29 +269,35 @@
145
269
  color: #475569;
146
270
  }
147
271
 
148
- @media (prefers-color-scheme: dark) {
149
- .rni-thanks { color: #94a3b8; }
150
- }
151
-
152
272
  .dark .rni-thanks { color: #94a3b8; }
153
273
 
154
274
  .github-link {
155
275
  flex-shrink: 0;
156
276
  font-size: 0.875rem;
157
277
  text-decoration: none;
158
- opacity: 0.7;
159
- color: inherit;
278
+ color: var(--release-notes-link-color, #4338ca);
279
+ transition: color 0.15s;
280
+ }
281
+
282
+ .github-link:hover {
283
+ color: var(--release-notes-link-hover-color, #9a3412);
284
+ text-decoration: none;
285
+ }
286
+
287
+ .dark .github-link {
288
+ color: var(--release-notes-link-color-dark, #818cf8);
160
289
  }
161
290
 
162
- .github-link:hover { opacity: 1; text-decoration: underline; }
291
+ .dark .github-link:hover {
292
+ color: var(--release-notes-link-hover-color-dark, #fed7aa);
293
+ }
163
294
 
164
295
  /* ---------------------------------------------------------------
165
296
  Version articles
166
297
  --------------------------------------------------------------- */
167
298
 
168
299
  .version {
169
- content-visibility: auto;
170
- contain-intrinsic-size: 0 800px;
300
+ scroll-margin-top: var(--release-notes-scroll-margin, 5rem);
171
301
  }
172
302
 
173
303
  .version-header {
@@ -181,10 +311,6 @@
181
311
  border-bottom: 1px solid #e2e8f0;
182
312
  }
183
313
 
184
- @media (prefers-color-scheme: dark) {
185
- .version-header { border-bottom-color: #1e293b; }
186
- }
187
-
188
314
  .dark .version-header { border-bottom-color: #1e293b; }
189
315
 
190
316
  .version-date {
@@ -193,42 +319,19 @@
193
319
  }
194
320
 
195
321
  /* ---------------------------------------------------------------
196
- Version selector (jump dropdown)
322
+ Version anchor links (# permalink)
197
323
  --------------------------------------------------------------- */
198
324
 
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
- }
325
+ .release-note .anchor-link {
326
+ opacity: 0;
327
+ text-decoration: none;
328
+ border: none;
329
+ margin-left: 0.5rem;
330
+ color: inherit;
331
+ transition: opacity 0.15s ease;
228
332
  }
229
333
 
230
- .dark .release-selector-select {
231
- border-color: #4b5563;
232
- background-color: #1e293b;
233
- color: #cbd5e1;
334
+ .release-note:hover .anchor-link,
335
+ .release-note .anchor-link:focus-visible {
336
+ opacity: 1;
234
337
  }