astro-better-release-notes 0.2.1 → 0.2.2
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 +40 -3
- package/ReleaseNotes.astro +14 -9
- package/ReleaseNotesSelector.astro +14 -8
- package/package.json +1 -1
- package/style.css +82 -14
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`
|
|
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`
|
|
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 */
|
|
@@ -124,11 +124,30 @@ The github issue links and category badge hover colors are configurable:
|
|
|
124
124
|
| `--release-notes-link-color-dark` | `#818cf8` (indigo-400) | Dark mode link color |
|
|
125
125
|
| `--release-notes-link-hover-color-dark` | `#fed7aa` (orange-200) | Dark mode hover color |
|
|
126
126
|
|
|
127
|
+
### Theme variables
|
|
128
|
+
|
|
129
|
+
Set these on `:root` (and `.dark` for dark mode) to match your site. Each falls back to the default shown.
|
|
130
|
+
|
|
131
|
+
| Variable | Light default | Dark default | Used for |
|
|
132
|
+
|----------|---------------|--------------|----------|
|
|
133
|
+
| `--rn-bg` | `white` | `#0f172a` | Sticky header background |
|
|
134
|
+
| `--rn-border` | `#d1d5db` | `#4b5563` | Selector and filter button borders |
|
|
135
|
+
| `--rn-input-bg` | `#f3f4f6` | `#1e293b` | Selector and filter button background |
|
|
136
|
+
| `--rn-text` | `#111827` | `#cbd5e1` | Selector text |
|
|
137
|
+
| `--rn-text-muted` | `#374151` | `#cbd5e1` | Filter button text |
|
|
138
|
+
| `--rn-focus` | `#6366f1` | `#818cf8` | Focus rings and the active-filter indicator |
|
|
139
|
+
| `--rn-chip-bg` | `#f1f5f9` | `#1e293b` | Unselected chip background |
|
|
140
|
+
| `--rn-chip-border` | `#cbd5e1` | `#475569` | Unselected chip border |
|
|
141
|
+
| `--rn-chip-color` | `#334155` | `#cbd5e1` | Unselected chip text |
|
|
142
|
+
| `--rn-color-{category}` | see `DEFAULT_CATEGORIES` | same | Category color for chips and badges |
|
|
143
|
+
|
|
144
|
+
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`.
|
|
145
|
+
|
|
127
146
|
### Mobile filter behavior
|
|
128
147
|
|
|
129
148
|
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
149
|
|
|
131
|
-
On screens
|
|
150
|
+
On screens 1024px and wider, the chips are always visible.
|
|
132
151
|
|
|
133
152
|
## Custom categories
|
|
134
153
|
|
|
@@ -211,6 +230,24 @@ import { ReleaseNotesSelector } from 'astro-better-release-notes';
|
|
|
211
230
|
</div>
|
|
212
231
|
```
|
|
213
232
|
|
|
233
|
+
## Accessibility
|
|
234
|
+
|
|
235
|
+
- 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.
|
|
236
|
+
- 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.
|
|
237
|
+
- 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.
|
|
238
|
+
- The version selector has a label and announces each jump through a polite live region. Focus stays on the selector, because Chrome and Edge on Windows fire `change` on every arrow key press.
|
|
239
|
+
- Transitions and the badge hover scale are disabled under `prefers-reduced-motion: reduce`.
|
|
240
|
+
- The package ships its own `.rn-sr-only` class, so it doesn't depend on a host `sr-only` utility.
|
|
241
|
+
|
|
242
|
+
## Upgrading to 0.2.2
|
|
243
|
+
|
|
244
|
+
Anchor URLs are unchanged, but the markup moved:
|
|
245
|
+
|
|
246
|
+
- The version id moved from `<article class="version">` to its `<h2>`. Update any CSS or scripts that target `article.version[id]` or `.version:target`.
|
|
247
|
+
- `--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)).
|
|
248
|
+
- The permalink is now `.version-title .anchor-link`, outside the `<h2>`. Selectors using `.release-note .anchor-link` need updating.
|
|
249
|
+
- The filter container is a `<fieldset>` instead of a `<nav>`, and keeps the `release-notes-filters` id and `filter-controls` class.
|
|
250
|
+
|
|
214
251
|
## Dark mode
|
|
215
252
|
|
|
216
253
|
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:
|
package/ReleaseNotes.astro
CHANGED
|
@@ -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
|
-
<
|
|
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"
|
|
88
|
-
|
|
89
|
+
<input type="checkbox" id={`toggle-${cat.id}`} class="filter-input" />
|
|
90
|
+
{cat.label}
|
|
89
91
|
</label>
|
|
90
92
|
))}
|
|
91
|
-
</
|
|
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
|
|
103
|
+
<article class="version" aria-labelledby={versionId}>
|
|
102
104
|
<header class="version-header">
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
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"
|
|
@@ -27,15 +27,19 @@ const { updates } = Astro.props as Props;
|
|
|
27
27
|
return <option value={versionId}>{label}</option>;
|
|
28
28
|
})}
|
|
29
29
|
</select>
|
|
30
|
+
<p id="release-notes-version-status" class="rn-sr-only" aria-live="polite"></p>
|
|
30
31
|
</div>
|
|
31
32
|
|
|
32
33
|
<script is:inline>
|
|
33
34
|
const initVersionSpy = () => {
|
|
35
|
+
// each article's anchor id lives on its h2
|
|
36
|
+
const headingId = (article) => article.querySelector('h2[id]')?.id;
|
|
34
37
|
const selectEl = document.getElementById('release-notes-version');
|
|
35
38
|
if (!selectEl) return;
|
|
36
39
|
|
|
37
40
|
const placeholderOption = selectEl.querySelector('option[value=""]');
|
|
38
|
-
const
|
|
41
|
+
const statusEl = document.getElementById('release-notes-version-status');
|
|
42
|
+
const versions = Array.from(document.querySelectorAll('article.version')).filter(a => headingId(a));
|
|
39
43
|
if (versions.length === 0) return;
|
|
40
44
|
|
|
41
45
|
const HEADER_OFFSET = 210;
|
|
@@ -61,9 +65,9 @@ const { updates } = Astro.props as Props;
|
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
if (activeArticle && activeArticle
|
|
65
|
-
currentActiveId = activeArticle
|
|
66
|
-
selectEl.value =
|
|
68
|
+
if (activeArticle && headingId(activeArticle) !== currentActiveId) {
|
|
69
|
+
currentActiveId = headingId(activeArticle);
|
|
70
|
+
selectEl.value = currentActiveId;
|
|
67
71
|
disablePlaceholder();
|
|
68
72
|
}
|
|
69
73
|
};
|
|
@@ -79,17 +83,19 @@ const { updates } = Astro.props as Props;
|
|
|
79
83
|
}
|
|
80
84
|
};
|
|
81
85
|
|
|
82
|
-
//
|
|
86
|
+
// focus stays on the select (arrow keys fire change on windows), so announce the jump instead
|
|
83
87
|
selectEl.addEventListener('change', () => {
|
|
84
88
|
hasUserInteracted = true;
|
|
85
89
|
disablePlaceholder();
|
|
86
|
-
if (selectEl.value)
|
|
90
|
+
if (!selectEl.value) return;
|
|
91
|
+
window.location.hash = selectEl.value;
|
|
92
|
+
if (statusEl) statusEl.textContent = `Jumped to ${selectEl.selectedOptions[0]?.textContent ?? selectEl.value}`;
|
|
87
93
|
});
|
|
88
94
|
|
|
89
95
|
const filterControls = document.querySelector('.filter-controls');
|
|
90
96
|
if (filterControls) {
|
|
91
97
|
filterControls.addEventListener('change', () => {
|
|
92
|
-
const targetVersion = versions.find(a => a
|
|
98
|
+
const targetVersion = versions.find(a => headingId(a) === currentActiveId) || versions[0];
|
|
93
99
|
window.requestAnimationFrame(() => {
|
|
94
100
|
if (targetVersion && hasUserInteracted) {
|
|
95
101
|
const adjustment = targetVersion.getBoundingClientRect().top - HEADER_OFFSET;
|
package/package.json
CHANGED
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
|
-
|
|
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:
|
|
209
|
+
filter: brightness(0.95);
|
|
193
210
|
}
|
|
194
211
|
|
|
195
212
|
.dark .filter-chip:hover {
|
|
196
|
-
filter:
|
|
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
|
-
|
|
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:
|
|
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,11 +381,13 @@
|
|
|
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
|
-
.
|
|
390
|
+
.version-title .anchor-link {
|
|
353
391
|
opacity: 0;
|
|
354
392
|
text-decoration: none;
|
|
355
393
|
border: none;
|
|
@@ -358,7 +396,37 @@
|
|
|
358
396
|
transition: opacity 0.15s ease;
|
|
359
397
|
}
|
|
360
398
|
|
|
361
|
-
.
|
|
362
|
-
.
|
|
399
|
+
.version-title:hover .anchor-link,
|
|
400
|
+
.version-title .anchor-link:focus-visible {
|
|
363
401
|
opacity: 1;
|
|
364
402
|
}
|
|
403
|
+
|
|
404
|
+
/* ---------------------------------------------------------------
|
|
405
|
+
Utilities and motion
|
|
406
|
+
--------------------------------------------------------------- */
|
|
407
|
+
|
|
408
|
+
.rn-sr-only {
|
|
409
|
+
position: absolute;
|
|
410
|
+
width: 1px;
|
|
411
|
+
height: 1px;
|
|
412
|
+
margin: -1px;
|
|
413
|
+
padding: 0;
|
|
414
|
+
overflow: hidden;
|
|
415
|
+
clip-path: inset(50%);
|
|
416
|
+
white-space: nowrap;
|
|
417
|
+
border: 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@media (prefers-reduced-motion: reduce) {
|
|
421
|
+
.filter-chip,
|
|
422
|
+
.issue-badge,
|
|
423
|
+
.github-link,
|
|
424
|
+
.version-title .anchor-link {
|
|
425
|
+
transition: none;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.issue-badge:hover,
|
|
429
|
+
.issue-badge:active {
|
|
430
|
+
transform: none;
|
|
431
|
+
}
|
|
432
|
+
}
|