@sarimarcus/content-sites-ui 0.29.3 → 0.29.4
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/CHANGELOG.md +6 -0
- package/package.json +2 -2
- package/src/composed/CardCarousel.astro +7 -4
- package/src/composed/Pagination.astro +3 -3
- package/src/composed/TableOfContents.astro +1 -0
- package/src/composed/carousel.ts +19 -7
- package/src/composed/toc.ts +1 -0
- package/src/navigation/ArticlesNavMenu.astro +3 -5
- package/src/navigation/NavDropdown.astro +6 -7
- package/src/navigation/SiteFooter.astro +2 -1
- package/src/navigation/navigation.ts +13 -4
- package/src/seo/ArticleMetadata.astro +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All packages in the platform release in lockstep; entries are per release version.
|
|
4
4
|
|
|
5
|
+
## 0.29.4 (2026-10-07)
|
|
6
|
+
|
|
7
|
+
- `./composed`: `Pagination`'s current page uses `text-primary-foreground` (was `text-white`, 1.8:1 on a light primary) and Previous/Next are named by their visible text; `TableOfContents`' hidden mini bar is `inert`; `CardCarousel`'s arrows use `aria-disabled`, so the focused arrow keeps focus at an edge, and arrows and dots have 44px targets (SLA-2984).
|
|
8
|
+
- `./seo`: `ArticleMetadata`'s byline is a labelled `group`, no longer a second `contentinfo` landmark inside `<main>` (SLA-2984).
|
|
9
|
+
- `./navigation`: `NavDropdown` opens only through `aria-expanded` (no `:focus-within`), so Escape and click-to-close stay closed while focus is inside, and drops `aria-haspopup`; the mobile menu closes and releases the body scroll lock when the viewport crosses `lg`; `SiteFooter`'s legal links are 44px tall (SLA-2984).
|
|
10
|
+
|
|
5
11
|
## 0.29.3 (2026-10-07)
|
|
6
12
|
|
|
7
13
|
- `./layout`: `ConsentAnalytics` builds the absolute `gtm.js` URL at render time and passes it to the inline script, which no longer holds a relative `'/gtm.js?id='` literal that Googlebot crawled as a 404 on each site (SLA-3001).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarimarcus/content-sites-ui",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.4",
|
|
4
4
|
"description": "Universal Astro UI components, layout shell and design tokens shared by the content sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
"@lucide/astro": "^1.23.0",
|
|
50
50
|
"astro": "^7.0.6",
|
|
51
51
|
"tailwindcss": "^4.1.18",
|
|
52
|
-
"@sarimarcus/content-sites-core": "0.29.
|
|
52
|
+
"@sarimarcus/content-sites-core": "0.29.4"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -51,6 +51,9 @@ const HeadingTag = headingTag;
|
|
|
51
51
|
// Ids must be unique per page AND stable across builds.
|
|
52
52
|
const containerId = deterministicId('cc', Astro.url.pathname, id, ariaLabel ?? heading);
|
|
53
53
|
const trackId = `${containerId}-t`;
|
|
54
|
+
// The ::before grows the 36px circle into a 44px touch target.
|
|
55
|
+
const arrowClass =
|
|
56
|
+
'relative flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border border-border bg-card transition-colors before:absolute before:-inset-1 hover:border-primary/40 hover:bg-primary/10 aria-disabled:pointer-events-none aria-disabled:opacity-25';
|
|
54
57
|
---
|
|
55
58
|
|
|
56
59
|
<div id={id} data-carousel={containerId} class:list={['scroll-mt-28', className]}>
|
|
@@ -80,8 +83,8 @@ const trackId = `${containerId}-t`;
|
|
|
80
83
|
data-carousel-prev
|
|
81
84
|
type="button"
|
|
82
85
|
aria-label={`Previous, ${ariaLabel ?? heading ?? 'carousel'}`}
|
|
83
|
-
disabled
|
|
84
|
-
class=
|
|
86
|
+
aria-disabled="true"
|
|
87
|
+
class={arrowClass}
|
|
85
88
|
>
|
|
86
89
|
<Icon name="chevron-left" class="h-4 w-4" aria-hidden="true" />
|
|
87
90
|
</button>
|
|
@@ -89,7 +92,7 @@ const trackId = `${containerId}-t`;
|
|
|
89
92
|
data-carousel-next
|
|
90
93
|
type="button"
|
|
91
94
|
aria-label={`Next, ${ariaLabel ?? heading ?? 'carousel'}`}
|
|
92
|
-
class=
|
|
95
|
+
class={arrowClass}
|
|
93
96
|
>
|
|
94
97
|
<Icon name="chevron-right" class="h-4 w-4" aria-hidden="true" />
|
|
95
98
|
</button>
|
|
@@ -106,7 +109,7 @@ const trackId = `${containerId}-t`;
|
|
|
106
109
|
>
|
|
107
110
|
<slot />
|
|
108
111
|
</div>
|
|
109
|
-
<div data-carousel-dots class="mt-4 hidden justify-center
|
|
112
|
+
<div data-carousel-dots class="mt-4 hidden justify-center"></div>
|
|
110
113
|
</div>
|
|
111
114
|
|
|
112
115
|
<style is:global>
|
|
@@ -44,7 +44,7 @@ const edgeBase = `${linkBase} px-3`;
|
|
|
44
44
|
{totalPages > 1 && (
|
|
45
45
|
<nav aria-label="Pagination" class="mt-14 flex items-center justify-center gap-2 flex-wrap">
|
|
46
46
|
{prevPage ? (
|
|
47
|
-
<a href={href(prevPage)} rel="prev"
|
|
47
|
+
<a href={href(prevPage)} rel="prev" class={edgeBase}>
|
|
48
48
|
<Icon name="arrow-left" class="w-3.5 h-3.5" />
|
|
49
49
|
<span>Previous</span>
|
|
50
50
|
</a>
|
|
@@ -65,7 +65,7 @@ const edgeBase = `${linkBase} px-3`;
|
|
|
65
65
|
<span
|
|
66
66
|
aria-current="page"
|
|
67
67
|
aria-label={`Page ${item}, current page`}
|
|
68
|
-
class={`${numberBase} border-primary bg-primary text-
|
|
68
|
+
class={`${numberBase} border-primary bg-primary text-primary-foreground hover:text-primary-foreground`}
|
|
69
69
|
>
|
|
70
70
|
{item}
|
|
71
71
|
</span>
|
|
@@ -82,7 +82,7 @@ const edgeBase = `${linkBase} px-3`;
|
|
|
82
82
|
</div>
|
|
83
83
|
|
|
84
84
|
{nextPage ? (
|
|
85
|
-
<a href={href(nextPage)} rel="next"
|
|
85
|
+
<a href={href(nextPage)} rel="next" class={edgeBase}>
|
|
86
86
|
<span>Next</span>
|
|
87
87
|
<Icon name="arrow-right" class="w-3.5 h-3.5" />
|
|
88
88
|
</a>
|
|
@@ -71,6 +71,7 @@ const showMini = variant === 'card';
|
|
|
71
71
|
data-toc-section-selector={sectionSelector}
|
|
72
72
|
class="fixed top-16 left-0 right-0 z-40 translate-y-[-100%] opacity-0 pointer-events-none transition-[transform,opacity] duration-300 ease-out bg-background/95 backdrop-blur-sm border-b border-border shadow-sm"
|
|
73
73
|
aria-hidden="true"
|
|
74
|
+
inert
|
|
74
75
|
>
|
|
75
76
|
<div class="mx-auto max-w-shell px-4 sm:px-6 lg:px-8 py-2.5">
|
|
76
77
|
<nav class="flex gap-2 overflow-x-auto scrollbar-hide" aria-label={`${ariaLabel} (sticky)`}>
|
package/src/composed/carousel.ts
CHANGED
|
@@ -17,18 +17,26 @@ function initCarousel(section: HTMLElement): void {
|
|
|
17
17
|
const dotsWrap = section.querySelector<HTMLElement>('[data-carousel-dots]');
|
|
18
18
|
let currentIndex = 0;
|
|
19
19
|
|
|
20
|
+
// Each dot is a 44px hit area around its 6px pill; the negative margin keeps the row 6px tall.
|
|
20
21
|
const dots: HTMLButtonElement[] = [];
|
|
22
|
+
const pills: HTMLElement[] = [];
|
|
21
23
|
if (dotsWrap) {
|
|
22
24
|
cards.forEach((_, i) => {
|
|
23
25
|
const dot = document.createElement('button');
|
|
24
26
|
dot.type = 'button';
|
|
25
27
|
dot.setAttribute('aria-label', `Go to item ${i + 1}`);
|
|
26
28
|
dot.style.cssText =
|
|
27
|
-
'
|
|
29
|
+
'display:inline-flex;align-items:center;justify-content:center;width:2.75rem;height:2.75rem;' +
|
|
30
|
+
'margin:-1.1875rem 0;border:0;padding:0;background:transparent;cursor:pointer;';
|
|
31
|
+
const pill = document.createElement('span');
|
|
32
|
+
pill.style.cssText =
|
|
33
|
+
'display:block;height:0.375rem;border-radius:9999px;transition:all 0.2s;' +
|
|
28
34
|
(i === 0 ? `width:1rem;background:${ACTIVE};` : `width:0.375rem;background:${INACTIVE};`);
|
|
35
|
+
dot.appendChild(pill);
|
|
29
36
|
dot.addEventListener('click', () => scrollToIndex(i));
|
|
30
37
|
dotsWrap.appendChild(dot);
|
|
31
38
|
dots.push(dot);
|
|
39
|
+
pills.push(pill);
|
|
32
40
|
});
|
|
33
41
|
}
|
|
34
42
|
|
|
@@ -43,14 +51,17 @@ function initCarousel(section: HTMLElement): void {
|
|
|
43
51
|
currentIndex = Math.max(0, Math.min(idx, cards.length - 1));
|
|
44
52
|
// Disabled state comes from the scroll position, not the card index: with several cards per view the track hits
|
|
45
53
|
// max scroll while currentIndex is still mid-list.
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
// aria-disabled, not disabled: disabling the focused arrow would drop focus to body.
|
|
55
|
+
prevBtn?.setAttribute('aria-disabled', String(startEdge));
|
|
56
|
+
nextBtn?.setAttribute('aria-disabled', String(endEdge));
|
|
48
57
|
// At the extremes the dot shows the first or last card, otherwise the trailing dots could never light up.
|
|
49
58
|
const dotIndex = endEdge ? cards.length - 1 : startEdge ? 0 : currentIndex;
|
|
50
59
|
dots.forEach((dot, i) => {
|
|
51
60
|
const active = i === dotIndex;
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
pills[i].style.width = active ? '1rem' : '0.375rem';
|
|
62
|
+
pills[i].style.background = active ? ACTIVE : INACTIVE;
|
|
63
|
+
if (active) dot.setAttribute('aria-current', 'true');
|
|
64
|
+
else dot.removeAttribute('aria-current');
|
|
54
65
|
});
|
|
55
66
|
}
|
|
56
67
|
|
|
@@ -81,8 +92,9 @@ function initCarousel(section: HTMLElement): void {
|
|
|
81
92
|
});
|
|
82
93
|
}, { passive: true });
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
const enabled = (btn: HTMLButtonElement) => btn.getAttribute('aria-disabled') !== 'true';
|
|
96
|
+
prevBtn?.addEventListener('click', () => enabled(prevBtn) && scrollToIndex(currentIndex - 1));
|
|
97
|
+
nextBtn?.addEventListener('click', () => enabled(nextBtn) && scrollToIndex(currentIndex + 1));
|
|
86
98
|
|
|
87
99
|
// Controls show only when the track overflows; re-checked whenever the track resizes.
|
|
88
100
|
function syncControls(): void {
|
package/src/composed/toc.ts
CHANGED
|
@@ -12,6 +12,7 @@ function setMiniVisible(mini: Element, visible: boolean): void {
|
|
|
12
12
|
mini.classList.toggle('opacity-100', visible);
|
|
13
13
|
mini.classList.toggle('pointer-events-auto', visible);
|
|
14
14
|
mini.setAttribute('aria-hidden', visible ? 'false' : 'true');
|
|
15
|
+
mini.toggleAttribute('inert', !visible);
|
|
15
16
|
document.dispatchEvent(new CustomEvent('toc:mini-visibility-change', { detail: { visible } }));
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -125,16 +125,14 @@ const featureStyle = featureBg
|
|
|
125
125
|
)}
|
|
126
126
|
|
|
127
127
|
<style>
|
|
128
|
-
/* data-opened is set on the first open and kept
|
|
129
|
-
:global([data-nav-dropdown][data-opened]) .articles-nav-feature
|
|
130
|
-
:global(.nav-dropdown:focus-within) .articles-nav-feature {
|
|
128
|
+
/* data-opened is set on the first open and kept, so the image never unpaints mid-fade. */
|
|
129
|
+
:global([data-nav-dropdown][data-opened]) .articles-nav-feature {
|
|
131
130
|
background-image: var(--articles-nav-feature, none);
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
/* A var() holding an unsupported image-set() computes to none, so older engines get the 1x file instead. */
|
|
135
134
|
@supports not (background-image: image-set(url('x.webp') 1x)) {
|
|
136
|
-
:global([data-nav-dropdown][data-opened]) .articles-nav-feature
|
|
137
|
-
:global(.nav-dropdown:focus-within) .articles-nav-feature {
|
|
135
|
+
:global([data-nav-dropdown][data-opened]) .articles-nav-feature {
|
|
138
136
|
background-image: var(--articles-nav-feature-1x, none);
|
|
139
137
|
}
|
|
140
138
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* A desktop dropdown: the trigger button (aria-expanded/
|
|
4
|
-
* default slot. The panel opens on click, hover (closing after a short delay)
|
|
3
|
+
* A desktop dropdown: the trigger button (aria-expanded/controls) and the panel the site fills through the
|
|
4
|
+
* default slot. The panel opens on click, hover (closing after a short delay) and ArrowDown, never on focus alone; the
|
|
5
5
|
* SiteHeader controller drives it. `buttonClass` replaces the default trigger styling, which `tone` tints.
|
|
6
6
|
*/
|
|
7
7
|
import { Icon } from '../primitives/index.ts';
|
|
@@ -37,7 +37,6 @@ const triggerClass = buttonClass ?? [
|
|
|
37
37
|
class:list={['nav-dropdown-btn', triggerClass]}
|
|
38
38
|
aria-current={current ? 'page' : undefined}
|
|
39
39
|
aria-expanded="false"
|
|
40
|
-
aria-haspopup="true"
|
|
41
40
|
aria-controls={id}
|
|
42
41
|
data-nav-dropdown-button
|
|
43
42
|
>
|
|
@@ -61,16 +60,16 @@ const triggerClass = buttonClass ?? [
|
|
|
61
60
|
pointer-events: none;
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
.nav-dropdown
|
|
63
|
+
/* visibility flips at once on open, so ArrowDown can focus a link in the same tick; closing still fades. */
|
|
64
|
+
.nav-dropdown-btn[aria-expanded='true'] + .dropdown-menu {
|
|
65
|
+
transition: opacity 200ms, visibility 0s, transform 200ms;
|
|
66
66
|
opacity: 1;
|
|
67
67
|
visibility: visible;
|
|
68
68
|
transform: translateY(0);
|
|
69
69
|
pointer-events: auto;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
.nav-dropdown-btn[aria-expanded='true'] :global(.dropdown-chevron)
|
|
73
|
-
.nav-dropdown:focus-within :global(.dropdown-chevron) {
|
|
72
|
+
.nav-dropdown-btn[aria-expanded='true'] :global(.dropdown-chevron) {
|
|
74
73
|
transform: rotate(180deg);
|
|
75
74
|
}
|
|
76
75
|
|
|
@@ -49,7 +49,8 @@ const {
|
|
|
49
49
|
ringClass = 'rounded focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-inverse',
|
|
50
50
|
} = Astro.props;
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// A real 44px box, not an overflowing hit area: wrapped rows would overlap and steal each other's taps.
|
|
53
|
+
const legalItemClass = `${smallClass} inline-flex min-h-11 items-center hover:text-inverse-foreground transition-colors ${ringClass}`;
|
|
53
54
|
---
|
|
54
55
|
|
|
55
56
|
<footer class={className}>
|
|
@@ -113,6 +113,13 @@ function searchPathOf(root: HTMLElement): string | null {
|
|
|
113
113
|
return (window.location.pathname.indexOf(compiled) === 0 ? compiled : '') + path;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function closeOpenMobileMenus(refocus: boolean): void {
|
|
117
|
+
roots().forEach((root) => {
|
|
118
|
+
const toggle = root.querySelector<HTMLButtonElement>('[data-mobile-menu-toggle]');
|
|
119
|
+
if (toggle?.getAttribute('aria-expanded') === 'true') setMobileMenu(root, toggle, false, refocus);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
function bindDocument(): void {
|
|
117
124
|
if (documentBound) return;
|
|
118
125
|
documentBound = true;
|
|
@@ -125,10 +132,7 @@ function bindDocument(): void {
|
|
|
125
132
|
|
|
126
133
|
document.addEventListener('keydown', (event) => {
|
|
127
134
|
if (event.key === 'Escape') {
|
|
128
|
-
|
|
129
|
-
const toggle = root.querySelector<HTMLButtonElement>('[data-mobile-menu-toggle]');
|
|
130
|
-
if (toggle?.getAttribute('aria-expanded') === 'true') setMobileMenu(root, toggle, false, true);
|
|
131
|
-
});
|
|
135
|
+
closeOpenMobileMenus(true);
|
|
132
136
|
return;
|
|
133
137
|
}
|
|
134
138
|
if (!(event.metaKey || event.ctrlKey) || event.key !== 'k') return;
|
|
@@ -140,6 +144,11 @@ function bindDocument(): void {
|
|
|
140
144
|
event.preventDefault();
|
|
141
145
|
window.location.assign(`${searchPath}?focus=1`);
|
|
142
146
|
});
|
|
147
|
+
|
|
148
|
+
// Tailwind's lg: the panel hides itself past it, so the body scroll lock must go with it.
|
|
149
|
+
window.matchMedia('(min-width: 64rem)').addEventListener('change', (event) => {
|
|
150
|
+
if (event.matches) closeOpenMobileMenus(false);
|
|
151
|
+
});
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
export function initNavigation(): void {
|
|
@@ -84,7 +84,7 @@ const grouped = bylineWhitespace === 'grouped';
|
|
|
84
84
|
const bylineLead = author ? (grouped ? ' ' : ' ') : (grouped ? '' : ' ');
|
|
85
85
|
---
|
|
86
86
|
|
|
87
|
-
<address class="not-italic mb-5" role="
|
|
87
|
+
<address class="not-italic mb-5" role="group" aria-label={ariaLabel}>
|
|
88
88
|
<div class="flex items-start gap-3">
|
|
89
89
|
{author && (author.image ? (
|
|
90
90
|
<Image
|