Skip to content

Commit 88ceced

Browse files
committed
Redesign the homepage and related pages with a quieter editorial layout.
1 parent 3ae3413 commit 88ceced

58 files changed

Lines changed: 3034 additions & 2142 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 52 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
"validate": "html-validate 'dist/**/*.html'"
1010
},
1111
"dependencies": {
12+
"@fontsource-variable/source-sans-3": "^5.3.0",
13+
"@fontsource-variable/source-serif-4": "^5.3.0",
14+
"@fontsource/ibm-plex-sans-arabic": "^5.3.0",
15+
"@fontsource/ibm-plex-sans-hebrew": "^5.3.0",
16+
"@fontsource/noto-naskh-arabic": "^5.3.0",
1217
"astro": "^6.3.3",
1318
"marked": "^17.0.2",
1419
"rehype-autolink-headings": "^7.1.0",

src/components/BackArrow.astro

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
display: inline-flex;
1717
align-items: center;
1818
justify-content: center;
19-
width: 2rem;
20-
height: 2rem;
21-
padding: 0.3rem;
19+
width: 2.75rem;
20+
height: 2.75rem;
21+
padding: 0;
2222
border-radius: 0.5rem;
23+
border: 1px solid var(--pico-muted-border-color);
2324
color: var(--pico-muted-color, #888);
24-
opacity: 0.6;
25+
opacity: 0.8;
2526
text-decoration: none;
2627
transition: opacity 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
2728
}
@@ -31,6 +32,14 @@
3132
background-color: rgba(128, 128, 128, 0.08);
3233
color: var(--pico-color, inherit);
3334
}
35+
36+
:global([dir='rtl']) .back-arrow svg {
37+
transform: scaleX(-1);
38+
}
39+
40+
:global([dir='rtl']) .back-arrow:hover {
41+
transform: translateX(2px);
42+
}
3443
.back-arrow:focus-visible {
3544
outline: 2px solid currentColor;
3645
outline-offset: 2px;

src/components/LanguagePicker.astro

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* (e.g. "cta/" for CTA page so entries link to /{locale}/cta/).
1111
* For English ("en"), the root is "/" so "/cta/" is the result.
1212
*/
13-
import { languages } from '../i18n/config';
13+
import { languages, isRtl } from '../i18n/config';
1414
1515
interface Props {
1616
locale: string;
@@ -20,6 +20,7 @@ interface Props {
2020
const { locale, suffix = '' } = Astro.props;
2121
const currentLabel = languages[locale as keyof typeof languages]?.label ?? 'English';
2222
const entries = Object.entries(languages) as [string, { label: string; path: string }][];
23+
const menuId = `language-picker-${locale}-${suffix || 'home'}`.replace(/[^a-zA-Z0-9_-]/g, '-');
2324
2425
// Build the URL for a given locale, honoring the suffix
2526
function localeHref(code: string, basePath: string): string {
@@ -30,19 +31,31 @@ function localeHref(code: string, basePath: string): string {
3031
---
3132

3233
<div class="lang-picker" data-current={locale}>
33-
<button class="lang-picker-btn" type="button" aria-expanded="false">
34-
<span class="lang-picker-globe">&#127760;</span>
34+
<button class="lang-picker-btn" type="button" aria-expanded="false" aria-controls={menuId} aria-haspopup="true">
35+
<svg class="lang-picker-globe" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
36+
<circle cx="12" cy="12" r="9"></circle>
37+
<path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"></path>
38+
</svg>
3539
<span class="lang-picker-label">{currentLabel}</span>
36-
<span class="lang-picker-caret">&#9662;</span>
40+
<span class="lang-picker-caret" aria-hidden="true">&#9662;</span>
3741
</button>
38-
<div class="lang-picker-menu">
42+
<ul id={menuId} class="lang-picker-menu" hidden>
3943
{entries.map(([code, { label, path }]) => (
40-
<a href={localeHref(code, path)} data-lang={code} class:list={['lang-picker-item', code === locale && 'lang-current']}>
41-
<span class="lang-picker-item-label">{label}</span>
42-
{code === locale && <span class="lang-picker-check">&#10003;</span>}
43-
</a>
44+
<li>
45+
<a
46+
href={localeHref(code, path)}
47+
data-lang={code}
48+
lang={code}
49+
dir={isRtl(code) ? 'rtl' : 'ltr'}
50+
aria-current={code === locale ? 'page' : undefined}
51+
class:list={['lang-picker-item', code === locale && 'lang-current']}
52+
>
53+
<span class="lang-picker-item-label">{label}</span>
54+
{code === locale && <span class="lang-picker-check" aria-hidden="true">&#10003;</span>}
55+
</a>
56+
</li>
4457
))}
45-
</div>
58+
</ul>
4659
</div>
4760

4861
<script is:inline>
@@ -54,31 +67,79 @@ function localeHref(code: string, basePath: string): string {
5467
*/
5568
(function() {
5669
document.querySelectorAll('.lang-picker-btn').forEach(function(btn) {
70+
var menu = document.getElementById(btn.getAttribute('aria-controls'));
71+
if (!menu) return;
72+
73+
function closeMenu(returnFocus) {
74+
menu.hidden = true;
75+
btn.setAttribute('aria-expanded', 'false');
76+
if (returnFocus) btn.focus();
77+
}
78+
79+
function openMenu(focusFirst) {
80+
document.querySelectorAll('.lang-picker-menu:not([hidden])').forEach(function(otherMenu) {
81+
otherMenu.hidden = true;
82+
});
83+
document.querySelectorAll('.lang-picker-btn').forEach(function(otherButton) {
84+
otherButton.setAttribute('aria-expanded', 'false');
85+
});
86+
menu.hidden = false;
87+
btn.setAttribute('aria-expanded', 'true');
88+
if (focusFirst) {
89+
var current = menu.querySelector('[aria-current="page"]');
90+
var first = menu.querySelector('.lang-picker-item');
91+
(current || first)?.focus();
92+
}
93+
}
94+
5795
btn.addEventListener('click', function(e) {
5896
e.stopPropagation();
59-
var menu = btn.parentElement.querySelector('.lang-picker-menu');
60-
var isOpen = menu.classList.contains('is-open');
61-
// Close any other open pickers first
62-
document.querySelectorAll('.lang-picker-menu.is-open').forEach(function(m) { m.classList.remove('is-open'); });
63-
document.querySelectorAll('.lang-picker-btn').forEach(function(b) { b.setAttribute('aria-expanded', 'false'); });
64-
if (!isOpen) {
65-
menu.classList.add('is-open');
66-
btn.setAttribute('aria-expanded', 'true');
97+
if (menu.hidden) openMenu(false);
98+
else closeMenu(false);
99+
});
100+
101+
btn.addEventListener('keydown', function(e) {
102+
if (e.key === 'ArrowDown') {
103+
e.preventDefault();
104+
openMenu(true);
105+
}
106+
});
107+
108+
menu.addEventListener('keydown', function(e) {
109+
var items = Array.from(menu.querySelectorAll('.lang-picker-item'));
110+
var index = items.indexOf(document.activeElement);
111+
112+
if (e.key === 'Escape') {
113+
e.preventDefault();
114+
closeMenu(true);
115+
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
116+
e.preventDefault();
117+
var direction = e.key === 'ArrowDown' ? 1 : -1;
118+
items[(index + direction + items.length) % items.length]?.focus();
119+
} else if (e.key === 'Home' || e.key === 'End') {
120+
e.preventDefault();
121+
items[e.key === 'Home' ? 0 : items.length - 1]?.focus();
67122
}
68123
});
69124
});
70125

71126
document.addEventListener('click', function(e) {
72127
if (!e.target.closest('.lang-picker')) {
73-
document.querySelectorAll('.lang-picker-menu.is-open').forEach(function(m) { m.classList.remove('is-open'); });
128+
document.querySelectorAll('.lang-picker-menu:not([hidden])').forEach(function(m) { m.hidden = true; });
74129
document.querySelectorAll('.lang-picker-btn').forEach(function(b) { b.setAttribute('aria-expanded', 'false'); });
75130
}
76131
});
77132

78133
document.addEventListener('keydown', function(e) {
79134
if (e.key === 'Escape') {
80-
document.querySelectorAll('.lang-picker-menu.is-open').forEach(function(m) { m.classList.remove('is-open'); });
81-
document.querySelectorAll('.lang-picker-btn').forEach(function(b) { b.setAttribute('aria-expanded', 'false'); });
135+
var openMenu = document.querySelector('.lang-picker-menu:not([hidden])');
136+
if (!openMenu) return;
137+
openMenu.hidden = true;
138+
var button = document.querySelector('[aria-controls="' + openMenu.id + '"]');
139+
if (button) {
140+
button.setAttribute('aria-expanded', 'false');
141+
button.focus();
142+
}
82143
}
83144
});
84145

@@ -106,7 +167,8 @@ function localeHref(code: string, basePath: string): string {
106167

107168
if (match && match.getAttribute('data-lang') !== current) {
108169
match.classList.add('lang-detected');
109-
menu.insertBefore(match, menu.firstChild);
170+
match.setAttribute('title', 'Suggested from browser language');
171+
menu.insertBefore(match.parentElement, menu.firstElementChild);
110172
}
111173
});
112174
})();

src/components/QuoteTicker.astro

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ const actualDuration = Math.round(jitter);
3636
* SPDX-License-Identifier: Apache-2.0
3737
*/
3838
(function() {
39-
if (window.__tickerV6) return;
40-
window.__tickerV6 = true;
39+
if (window.__tickerV7) return;
40+
window.__tickerV7 = true;
4141

4242
var TOUCH_PAUSE_MS = 10000;
4343
var RAMP_MS = 600;
@@ -60,7 +60,6 @@ const actualDuration = Math.round(jitter);
6060
}
6161
}
6262

63-
// Smoothly ramp playbackRate from current to target over RAMP_MS
6463
function rampRate(anim, target, ms) {
6564
if (!anim) return;
6665
var start = anim.playbackRate;
@@ -69,7 +68,6 @@ const actualDuration = Math.round(jitter);
6968
if (!startTime) startTime = ts;
7069
var elapsed = ts - startTime;
7170
var progress = Math.min(elapsed / ms, 1);
72-
// Ease-in-out cubic
7371
var ease = progress < 0.5
7472
? 4 * progress * progress * progress
7573
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
@@ -80,7 +78,6 @@ const actualDuration = Math.round(jitter);
8078
}
8179

8280
ready(function() {
83-
// Assign per-card colors
8481
document.querySelectorAll('.ticker-card').forEach(function(card) {
8582
if (card.getAttribute('data-color')) {
8683
card.style.setProperty('--tc-accent', card.getAttribute('data-color'));
@@ -100,23 +97,34 @@ const actualDuration = Math.round(jitter);
10097
var halves = track.querySelectorAll('.ticker-half');
10198
var touchTimer = null;
10299

103-
// Get the CSS animation object from the track element
104100
function getAnim() {
105101
var anims = track.getAnimations();
106102
return anims.length > 0 ? anims[0] : null;
107103
}
108104

109-
// Mouse hover: gently ramp to 0 on enter, back to 1 on leave
105+
function pause() {
106+
rampRate(getAnim(), 0, RAMP_MS);
107+
}
108+
109+
function resume() {
110+
rampRate(getAnim(), 1, RAMP_MS);
111+
}
112+
110113
el.addEventListener('pointerenter', function(e) {
111114
if (e.pointerType !== 'mouse') return;
112-
rampRate(getAnim(), 0, RAMP_MS);
115+
pause();
113116
});
114117
el.addEventListener('pointerleave', function(e) {
115118
if (e.pointerType !== 'mouse') return;
116-
rampRate(getAnim(), 1, RAMP_MS);
119+
resume();
120+
});
121+
122+
el.addEventListener('focusin', pause);
123+
el.addEventListener('focusout', function(e) {
124+
if (el.contains(e.relatedTarget)) return;
125+
resume();
117126
});
118127

119-
// Touch: instant pause, resume after timeout
120128
el.addEventListener('touchstart', function() {
121129
var anim = getAnim();
122130
if (anim) anim.playbackRate = 0;
@@ -126,7 +134,6 @@ const actualDuration = Math.round(jitter);
126134
}, TOUCH_PAUSE_MS);
127135
}, {passive: true});
128136

129-
// Reshuffle on each animation cycle
130137
track.addEventListener('animationiteration', function() {
131138
for (var i = 0; i < halves.length; i++) {
132139
shuffleChildren(halves[i]);

0 commit comments

Comments
 (0)