astro-better-release-notes 0.2.1 → 0.2.3

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
@@ -92,9 +92,9 @@ const rendered = await Promise.all(
92
92
 
93
93
  Two CSS custom properties control the positioning:
94
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`.
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
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`.
97
+ `--release-notes-scroll-margin` -- the `scroll-margin-top` applied to version headings and individual release note rows, which are the targets of anchor links and the version selector. Set it to at least the distance from the viewport top to the bottom edge of the release notes sticky header (your nav plus the selector and chip rows), or anchored headings land underneath the header. The chip rows wrap with viewport width, so measure at your breakpoints. Default: `5rem`.
98
98
 
99
99
  ```css
100
100
  /* in your root layout or global CSS */
@@ -115,20 +115,42 @@ Set both variables on `html` or any ancestor of `.release-container`.
115
115
 
116
116
  ### Link colors
117
117
 
118
- The github issue links and category badge hover colors are configurable:
118
+ The issue links, version permalinks (`#`), and category badge hover colors are configurable:
119
119
 
120
120
  | Variable | Default | Description |
121
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 |
122
+ | `--release-notes-link-color` | `#4338ca` (indigo-700) | Issue link and permalink color |
123
+ | `--release-notes-link-hover-color` | `#9a3412` (orange-800) | Hover color for links, permalinks, and badges |
124
+ | `--release-notes-link-color-dark` | `#818cf8` (indigo-400) | Dark mode link and permalink color |
125
125
  | `--release-notes-link-hover-color-dark` | `#fed7aa` (orange-200) | Dark mode hover color |
126
+ | `--release-notes-anchor-size` | `1.5em` | Font size of the version permalink; match it to your `h2` size |
127
+
128
+ The permalink sits beside the version heading rather than inside it, so it doesn't inherit the heading's size. Set `--release-notes-anchor-size` if your `h2` isn't `1.5em`.
129
+
130
+ ### Theme variables
131
+
132
+ Set these on `:root` (and `.dark` for dark mode) to match your site. Each falls back to the default shown.
133
+
134
+ | Variable | Light default | Dark default | Used for |
135
+ |----------|---------------|--------------|----------|
136
+ | `--rn-bg` | `white` | `#0f172a` | Sticky header background |
137
+ | `--rn-border` | `#d1d5db` | `#4b5563` | Selector and filter button borders |
138
+ | `--rn-input-bg` | `#f3f4f6` | `#1e293b` | Selector and filter button background |
139
+ | `--rn-text` | `#111827` | `#cbd5e1` | Selector text |
140
+ | `--rn-text-muted` | `#374151` | `#cbd5e1` | Filter button text |
141
+ | `--rn-focus` | `#6366f1` | `#818cf8` | Focus rings and the active-filter indicator |
142
+ | `--rn-chip-bg` | `#f1f5f9` | `#1e293b` | Unselected chip background |
143
+ | `--rn-chip-border` | `#cbd5e1` | `#475569` | Unselected chip border |
144
+ | `--rn-chip-color` | `#334155` | `#cbd5e1` | Unselected chip text |
145
+ | `--rn-color-{category}` | see `DEFAULT_CATEGORIES` | same | Category color for chips and badges |
146
+
147
+ If you map these to your own palette, check that `--rn-chip-color` and `--rn-text-muted` keep a 4.5:1 contrast ratio against `--rn-chip-bg` and `--rn-input-bg`.
126
148
 
127
149
  ### Mobile filter behavior
128
150
 
129
151
  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
152
 
131
- On screens 768px and wider, the chips are always visible.
153
+ On screens 1024px and wider, the chips are always visible.
132
154
 
133
155
  ## Custom categories
134
156
 
@@ -211,6 +233,30 @@ import { ReleaseNotesSelector } from 'astro-better-release-notes';
211
233
  </div>
212
234
  ```
213
235
 
236
+ ## Accessibility
237
+
238
+ - Each version is an `<article>` labelled by its `<h2>`. The `<h2>` carries the anchor id (`version-1-2-3`), so fragment links, search crawlers, and TOC tools all resolve to the heading. The `#` permalink sits next to the heading rather than inside it, so the heading's accessible name is only the version.
239
+ - Category filters are native checkboxes inside a `<fieldset>` with a visually hidden `<legend>`. They work with Tab and Space, show a focus ring on the chip, and announce their checked state. Selected chips show a check mark as well as color.
240
+ - Badge and selected-chip text colors are mixed from the category color (darker in light mode, lighter in dark mode), so they stay at or above 4.5:1 for the default categories and for most custom mid-tone colors.
241
+ - The version selector has a label, and its options are bare version numbers, so typing `1.56` jumps to 1.56.0. Each jump is announced through a polite live region. Focus stays on the selector so type-ahead and arrow keys keep working: the selector scrolls with `scrollIntoView` and updates the URL with `history.replaceState`, because assigning `location.hash` would blur it. Jumps don't add browser history entries.
242
+ - Transitions and the badge hover scale are disabled under `prefers-reduced-motion: reduce`.
243
+ - The package ships its own `.rn-sr-only` class, so it doesn't depend on a host `sr-only` utility.
244
+
245
+ ## Upgrading to 0.2.3
246
+
247
+ - Version selector options no longer start with "Version" (`1.56.0 (SSO Snake)` instead of `Version 1.56.0 (SSO Snake)`).
248
+ - Choosing a version updates the URL with `history.replaceState` instead of a new history entry, so the Back button leaves the page rather than stepping back through jumps.
249
+ - The version permalink now uses the link color variables and `--release-notes-anchor-size` instead of inheriting the surrounding text color and size.
250
+
251
+ ## Upgrading to 0.2.2
252
+
253
+ Anchor URLs are unchanged, but the markup moved:
254
+
255
+ - The version id moved from `<article class="version">` to its `<h2>`. Update any CSS or scripts that target `article.version[id]` or `.version:target`.
256
+ - `--release-notes-scroll-margin` now applies to the heading instead of the article, which starts slightly higher. If anchored headings land under your sticky header, increase the value (see [Sticky offset and scroll margins](#sticky-offset-and-scroll-margins)).
257
+ - The permalink is now `.version-title .anchor-link`, outside the `<h2>`. Selectors using `.release-note .anchor-link` need updating.
258
+ - The filter container is a `<fieldset>` instead of a `<nav>`, and keeps the `release-notes-filters` id and `filter-controls` class.
259
+
214
260
  ## Dark mode
215
261
 
216
262
  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:
@@ -71,6 +71,7 @@ const filterCSS = [
71
71
  {showSelector && <ReleaseNotesSelector {updates} />}
72
72
 
73
73
  <button
74
+ type="button"
74
75
  class="filter-toggle-btn"
75
76
  aria-expanded="false"
76
77
  aria-controls="release-notes-filters"
@@ -81,14 +82,15 @@ const filterCSS = [
81
82
  Filter
82
83
  </button>
83
84
 
84
- <nav id="release-notes-filters" class="filter-controls" aria-label="Filter release notes by category">
85
+ <fieldset id="release-notes-filters" class="filter-controls">
86
+ <legend class="rn-sr-only">Show only these categories</legend>
85
87
  {categories.map(cat => (
86
88
  <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
+ <input type="checkbox" id={`toggle-${cat.id}`} class="filter-input" />
90
+ {cat.label}
89
91
  </label>
90
92
  ))}
91
- </nav>
93
+ </fieldset>
92
94
 
93
95
  <noscript><style>@media (max-width: 1023px) { #release-notes-filters { display: flex !important; flex-wrap: wrap; } .filter-toggle-btn { display: none !important; } }</style></noscript>
94
96
  </div>
@@ -98,12 +100,15 @@ const filterCSS = [
98
100
  const { Content } = item;
99
101
  const versionId = `version-${item.data.version.replaceAll('.', '-')}`;
100
102
  return (
101
- <article id={versionId} class="version">
103
+ <article class="version" aria-labelledby={versionId}>
102
104
  <header class="version-header">
103
- <h2 class="release-note group">
104
- Version {item.data.version} {item.data.name && `(${item.data.name})`}
105
- <a class="anchor-link" title="copy header link" href={`#${versionId}`} aria-label={`Permalink to version ${item.data.version}`}>#</a>
106
- </h2>
105
+ {/* id on h2 so crawlers resolve anchors; permalink outside h2 keeps the heading name clean */}
106
+ <div class="version-title">
107
+ <h2 id={versionId} class="release-note">
108
+ Version {item.data.version}{item.data.name && ` (${item.data.name})`}
109
+ </h2>
110
+ <a class="anchor-link" href={`#${versionId}`} aria-label={`Permalink to version ${item.data.version}`}>#</a>
111
+ </div>
107
112
  <time class="version-date" datetime={item.data.date.toISOString()}>
108
113
  {item.data.date.toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' })}
109
114
  </time>
@@ -15,7 +15,7 @@ const { updates } = Astro.props as Props;
15
15
  ---
16
16
 
17
17
  <div class="release-selector">
18
- <label for="release-notes-version" class="sr-only">Jump to a release version</label>
18
+ <label for="release-notes-version" class="rn-sr-only">Jump to a release version</label>
19
19
  <select
20
20
  id="release-notes-version"
21
21
  class="release-selector-select"
@@ -23,19 +23,24 @@ const { updates } = Astro.props as Props;
23
23
  <option value="">Jump to a version...</option>
24
24
  {updates.map((item) => {
25
25
  const versionId = `version-${item.data.version.replaceAll('.', '-')}`;
26
- const label = `Version ${item.data.version}${item.data.name ? ` (${item.data.name})` : ''}`;
26
+ // bare version so typing in the select jumps straight to it; the label supplies the context
27
+ const label = `${item.data.version}${item.data.name ? ` (${item.data.name})` : ''}`;
27
28
  return <option value={versionId}>{label}</option>;
28
29
  })}
29
30
  </select>
31
+ <p id="release-notes-version-status" class="rn-sr-only" aria-live="polite"></p>
30
32
  </div>
31
33
 
32
34
  <script is:inline>
33
35
  const initVersionSpy = () => {
36
+ // each article's anchor id lives on its h2
37
+ const headingId = (article) => article.querySelector('h2[id]')?.id;
34
38
  const selectEl = document.getElementById('release-notes-version');
35
39
  if (!selectEl) return;
36
40
 
37
41
  const placeholderOption = selectEl.querySelector('option[value=""]');
38
- const versions = Array.from(document.querySelectorAll('article.version[id]'));
42
+ const statusEl = document.getElementById('release-notes-version-status');
43
+ const versions = Array.from(document.querySelectorAll('article.version')).filter(a => headingId(a));
39
44
  if (versions.length === 0) return;
40
45
 
41
46
  const HEADER_OFFSET = 210;
@@ -61,9 +66,14 @@ const { updates } = Astro.props as Props;
61
66
  }
62
67
  }
63
68
 
64
- if (activeArticle && activeArticle.id !== currentActiveId) {
65
- currentActiveId = activeArticle.id;
66
- selectEl.value = activeArticle.id;
69
+ if (activeArticle) currentActiveId = headingId(activeArticle);
70
+ // writing value mid-interaction resets native type-ahead, so wait for blur
71
+ if (document.activeElement !== selectEl) syncSelect();
72
+ };
73
+
74
+ const syncSelect = () => {
75
+ if (currentActiveId && selectEl.value !== currentActiveId) {
76
+ selectEl.value = currentActiveId;
67
77
  disablePlaceholder();
68
78
  }
69
79
  };
@@ -79,17 +89,25 @@ const { updates } = Astro.props as Props;
79
89
  }
80
90
  };
81
91
 
82
- // navigate on change; aria-live on the region announces the jump for screen readers
92
+ // hide the placeholder before typing starts; changing options mid-type-ahead resets it
93
+ selectEl.addEventListener('focus', disablePlaceholder, { once: true });
94
+ selectEl.addEventListener('blur', syncSelect);
95
+
96
+ // location.hash blurs the select (breaks type-ahead/arrows); replaceState also skips per-key history entries
83
97
  selectEl.addEventListener('change', () => {
84
98
  hasUserInteracted = true;
85
99
  disablePlaceholder();
86
- if (selectEl.value) window.location.hash = selectEl.value;
100
+ const target = selectEl.value && document.getElementById(selectEl.value);
101
+ if (!target) return;
102
+ history.replaceState(history.state, '', `#${selectEl.value}`);
103
+ target.scrollIntoView({ block: 'start' });
104
+ if (statusEl) statusEl.textContent = `Jumped to version ${selectEl.selectedOptions[0]?.textContent ?? selectEl.value}`;
87
105
  });
88
106
 
89
107
  const filterControls = document.querySelector('.filter-controls');
90
108
  if (filterControls) {
91
109
  filterControls.addEventListener('change', () => {
92
- const targetVersion = versions.find(a => a.id === currentActiveId) || versions[0];
110
+ const targetVersion = versions.find(a => headingId(a) === currentActiveId) || versions[0];
93
111
  window.requestAnimationFrame(() => {
94
112
  if (targetVersion && hasUserInteracted) {
95
113
  const adjustment = targetVersion.getBoundingClientRect().top - HEADER_OFFSET;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-better-release-notes",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Configurable release notes components with category filtering and version navigation for Astro",
5
5
  "keywords": [
6
6
  "astro",
package/style.css CHANGED
@@ -162,21 +162,38 @@
162
162
  flex-wrap: wrap;
163
163
  gap: 0.75rem;
164
164
  padding: 0.25rem 0;
165
+ /* fieldset resets */
166
+ border: 0;
167
+ margin: 0;
168
+ min-inline-size: 0;
165
169
  }
166
170
 
171
+ /* visually hidden but still focusable and announced; the chip shows its state */
167
172
  .filter-input {
168
- display: none;
173
+ position: absolute;
174
+ width: 1px;
175
+ height: 1px;
176
+ margin: -1px;
177
+ padding: 0;
178
+ overflow: hidden;
179
+ clip-path: inset(50%);
180
+ white-space: nowrap;
181
+ border: 0;
182
+ }
183
+
184
+ .filter-chip:has(.filter-input:focus-visible) {
185
+ outline: 2px solid var(--rn-focus, #6366f1);
186
+ outline-offset: 2px;
169
187
  }
170
188
 
171
189
  .filter-chip {
190
+ position: relative;
172
191
  padding: 0.5rem 1rem;
173
192
  font-size: 0.875rem;
174
193
  font-weight: 500;
175
194
  border-radius: 9999px;
176
195
  cursor: pointer;
177
196
  transition: filter 0.15s ease-in-out;
178
- opacity: 0.7;
179
- filter: grayscale(100%);
180
197
  color: var(--rn-chip-color, #334155);
181
198
  background-color: var(--rn-chip-bg, #f1f5f9);
182
199
  border: 1px solid var(--rn-chip-border, #cbd5e1);
@@ -189,19 +206,28 @@
189
206
  }
190
207
 
191
208
  .filter-chip:hover {
192
- filter: grayscale(100%) brightness(0.95);
209
+ filter: brightness(0.95);
193
210
  }
194
211
 
195
212
  .dark .filter-chip:hover {
196
- filter: grayscale(100%) brightness(1.1);
213
+ filter: brightness(1.1);
197
214
  }
198
215
 
216
+ /* selected: tint + border + check so state isn't color-only; text mixes keep 4.5:1 */
199
217
  .filter-chip:has(input:checked) {
200
- filter: grayscale(0);
201
- opacity: 1;
202
- background-color: var(--cat-color) !important;
218
+ background-color: color-mix(in srgb, var(--cat-color) 18%, white) !important;
203
219
  border-color: var(--cat-color) !important;
204
- color: #fff !important;
220
+ color: color-mix(in srgb, var(--cat-color) 45%, black) !important;
221
+ }
222
+
223
+ .dark .filter-chip:has(input:checked) {
224
+ background-color: color-mix(in srgb, var(--cat-color) 18%, #1e293b) !important;
225
+ color: color-mix(in srgb, var(--cat-color) 60%, white) !important;
226
+ }
227
+
228
+ .filter-chip:has(input:checked)::before {
229
+ content: "\2713" / "";
230
+ margin-right: 0.375rem;
205
231
  }
206
232
 
207
233
  /* ---------------------------------------------------------------
@@ -261,10 +287,14 @@
261
287
  transition: transform 0.15s ease-out, color 0.15s ease-out;
262
288
  transform-origin: left center;
263
289
  background-color: color-mix(in srgb, var(--cat-color) 13%, transparent);
264
- color: var(--cat-color);
290
+ color: color-mix(in srgb, var(--cat-color) 45%, black);
265
291
  border: 1px solid color-mix(in srgb, var(--cat-color) 27%, transparent);
266
292
  }
267
293
 
294
+ .dark .issue-badge {
295
+ color: color-mix(in srgb, var(--cat-color) 60%, white);
296
+ }
297
+
268
298
  .issue-badge:hover {
269
299
  transform: scale(1.05);
270
300
  color: var(--release-notes-link-hover-color, #9a3412);
@@ -323,7 +353,13 @@
323
353
  Version articles
324
354
  --------------------------------------------------------------- */
325
355
 
326
- .version {
356
+ .version-title {
357
+ display: flex;
358
+ align-items: baseline;
359
+ }
360
+
361
+ /* the heading carries the anchor id */
362
+ .release-note[id] {
327
363
  scroll-margin-top: var(--release-notes-scroll-margin, 5rem);
328
364
  }
329
365
 
@@ -345,20 +381,67 @@
345
381
  color: #64748b;
346
382
  }
347
383
 
384
+ .dark .version-date { color: #94a3b8; }
385
+
348
386
  /* ---------------------------------------------------------------
349
387
  Version anchor links (# permalink)
350
388
  --------------------------------------------------------------- */
351
389
 
352
- .release-note .anchor-link {
390
+ /* sits beside the h2, not inside it, so size it to match the heading */
391
+ .version-title .anchor-link {
353
392
  opacity: 0;
393
+ font-size: var(--release-notes-anchor-size, 1.5em);
394
+ font-weight: 500;
354
395
  text-decoration: none;
355
396
  border: none;
356
397
  margin-left: 0.5rem;
357
- color: inherit;
358
- transition: opacity 0.15s ease;
398
+ color: var(--release-notes-link-color, #4338ca);
399
+ transition: opacity 0.15s ease, color 0.15s ease;
359
400
  }
360
401
 
361
- .release-note:hover .anchor-link,
362
- .release-note .anchor-link:focus-visible {
402
+ .version-title:hover .anchor-link,
403
+ .version-title .anchor-link:focus-visible {
363
404
  opacity: 1;
364
405
  }
406
+
407
+ .version-title .anchor-link:hover {
408
+ color: var(--release-notes-link-hover-color, #9a3412);
409
+ }
410
+
411
+ .dark .version-title .anchor-link {
412
+ color: var(--release-notes-link-color-dark, #818cf8);
413
+ }
414
+
415
+ .dark .version-title .anchor-link:hover {
416
+ color: var(--release-notes-link-hover-color-dark, #fed7aa);
417
+ }
418
+
419
+ /* ---------------------------------------------------------------
420
+ Utilities and motion
421
+ --------------------------------------------------------------- */
422
+
423
+ .rn-sr-only {
424
+ position: absolute;
425
+ width: 1px;
426
+ height: 1px;
427
+ margin: -1px;
428
+ padding: 0;
429
+ overflow: hidden;
430
+ clip-path: inset(50%);
431
+ white-space: nowrap;
432
+ border: 0;
433
+ }
434
+
435
+ @media (prefers-reduced-motion: reduce) {
436
+ .filter-chip,
437
+ .issue-badge,
438
+ .github-link,
439
+ .version-title .anchor-link {
440
+ transition: none;
441
+ }
442
+
443
+ .issue-badge:hover,
444
+ .issue-badge:active {
445
+ transform: none;
446
+ }
447
+ }