Skip to content

Commit 05cb8b4

Browse files
author
DavidQ
committed
feat(theme): add local Font Awesome and align ToolboxAid-style return-to-top control
- Vendored Font Awesome 4.7.0 into `src/engine/theme/fontawesome/` (css + webfonts). - Imported local FA stylesheet in `src/engine/theme/main.css` for theme-wide icon availability. - Updated shared Return-to-Top control to use ToolboxAid-style `#ball-container` markup and behavior: - wave rings + bounce animation - fade in/out on scroll threshold - smooth scroll to top on click - keyboard accessible activation - Switched arrow back to true FA icon rendering (`fa fa-arrow-up`) and removed fallback font overrides. - Kept implementation centralized in shared theme files so it applies consistently across hubs/tools.
1 parent acd053d commit 05cb8b4

7 files changed

Lines changed: 217 additions & 0 deletions

File tree

src/engine/theme/fontawesome/css/font-awesome.min.css

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
162 KB
Binary file not shown.
95.7 KB
Binary file not shown.
75.4 KB
Binary file not shown.

src/engine/theme/main.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@import url('./tokens.css');
2+
@import url('./fontawesome/css/font-awesome.min.css');
23
@import url('./layout.css');
34
@import url('./header.css');
45
@import url('./nav.css');

src/engine/theme/mount-shared-header.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,109 @@ async function mountSampleDetailEnhancementIfNeeded() {
2121
}
2222
}
2323

24+
function mountReturnToTopControl() {
25+
const existing = document.getElementById('ball-container');
26+
if (existing instanceof HTMLElement) {
27+
return;
28+
}
29+
30+
const container = document.createElement('div');
31+
container.id = 'ball-container';
32+
container.className = 'ball-loc bounce';
33+
container.style.display = 'none';
34+
container.setAttribute('role', 'button');
35+
container.setAttribute('tabindex', '0');
36+
container.setAttribute('aria-label', 'Return to Top');
37+
container.innerHTML = `
38+
<div class="ball wave1"></div>
39+
<div class="ball wave2"></div>
40+
<div class="ball wave3"></div>
41+
<div class="ball static-ball"><i class="fa fa-arrow-up" aria-hidden="true"></i></div>
42+
`;
43+
44+
let fadeRafId = 0;
45+
let fadeStart = 0;
46+
let fadeFrom = 0;
47+
let fadeTo = 0;
48+
const fadeDurationMs = 1500;
49+
50+
const applyVisibility = (value) => {
51+
container.style.opacity = String(value);
52+
if (value <= 0.001) {
53+
container.style.display = 'none';
54+
container.style.pointerEvents = 'none';
55+
} else {
56+
container.style.display = 'block';
57+
container.style.pointerEvents = 'auto';
58+
}
59+
};
60+
61+
const getOpacity = () => {
62+
const raw = Number.parseFloat(container.style.opacity || '0');
63+
return Number.isFinite(raw) ? raw : 0;
64+
};
65+
66+
const startFade = (targetOpacity) => {
67+
if (fadeRafId) {
68+
window.cancelAnimationFrame(fadeRafId);
69+
fadeRafId = 0;
70+
}
71+
fadeFrom = getOpacity();
72+
fadeTo = targetOpacity;
73+
if (Math.abs(fadeTo - fadeFrom) < 0.01) {
74+
applyVisibility(fadeTo);
75+
return;
76+
}
77+
if (fadeTo > 0) {
78+
container.style.display = 'block';
79+
container.style.pointerEvents = 'auto';
80+
}
81+
fadeStart = performance.now();
82+
const step = (now) => {
83+
const elapsed = now - fadeStart;
84+
const progress = Math.min(1, elapsed / fadeDurationMs);
85+
const nextOpacity = fadeFrom + ((fadeTo - fadeFrom) * progress);
86+
applyVisibility(nextOpacity);
87+
if (progress < 1) {
88+
fadeRafId = window.requestAnimationFrame(step);
89+
} else {
90+
fadeRafId = 0;
91+
}
92+
};
93+
fadeRafId = window.requestAnimationFrame(step);
94+
};
95+
96+
const updateVisibility = () => {
97+
const scrollTop = Math.max(
98+
window.scrollY || 0,
99+
document.documentElement?.scrollTop || 0,
100+
document.body?.scrollTop || 0
101+
);
102+
if (scrollTop > 250) {
103+
startFade(1);
104+
} else {
105+
startFade(0);
106+
}
107+
};
108+
109+
const moveToTop = () => {
110+
window.scrollTo({ top: 0, behavior: 'smooth' });
111+
};
112+
113+
container.addEventListener('click', moveToTop);
114+
container.addEventListener('keydown', (event) => {
115+
if (event.key === 'Enter' || event.key === ' ') {
116+
event.preventDefault();
117+
moveToTop();
118+
}
119+
});
120+
121+
window.addEventListener('scroll', updateVisibility, { passive: true });
122+
window.addEventListener('resize', updateVisibility, { passive: true });
123+
updateVisibility();
124+
document.body.appendChild(container);
125+
}
126+
24127
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
25128
const headerIntroAccordion = document.querySelector('.is-collapsible');
26129
const headerIntroSummary = headerIntroAccordion?.querySelector('.is-collapsible__summary');
@@ -55,5 +158,6 @@ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
55158
});
56159
}
57160

161+
mountReturnToTopControl();
58162
void mountSampleDetailEnhancementIfNeeded();
59163
}

src/engine/theme/pages.css

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,111 @@ body.hub-page-samples .card-link p {
118118
padding: 18px;
119119
}
120120
}
121+
122+
i.fa.fa-arrow-up {
123+
position: absolute;
124+
top: 50%;
125+
left: 50%;
126+
transform: translate(-50%, -50%);
127+
font-size: 23px;
128+
font-weight: 900;
129+
font-style: normal;
130+
color: #eeeeee;
131+
}
132+
133+
.ball-loc {
134+
position: fixed;
135+
left: 92vw;
136+
top: 86vh;
137+
z-index: 1000;
138+
}
139+
140+
.ball {
141+
width: 52px;
142+
height: 52px;
143+
border-radius: 50%;
144+
position: absolute;
145+
transform: translate(-50%, -50%);
146+
z-index: 1;
147+
}
148+
149+
.static-ball {
150+
opacity: 1;
151+
background-color: #ed9700;
152+
}
153+
154+
.wave1,
155+
.wave2,
156+
.wave3 {
157+
animation: wave-animation 3s infinite;
158+
background-color: #ed9700;
159+
}
160+
161+
.wave1 {
162+
animation-delay: 0s;
163+
}
164+
165+
.wave2 {
166+
animation-delay: 0.3s;
167+
}
168+
169+
.wave3 {
170+
animation-delay: 0.6s;
171+
}
172+
173+
.bounce:hover {
174+
animation-name: bounce;
175+
animation-timing-function: ease;
176+
animation-duration: 0.75s;
177+
animation-iteration-count: 3;
178+
cursor: pointer;
179+
}
180+
181+
@keyframes wave-animation {
182+
0% {
183+
opacity: 0.75;
184+
width: 0;
185+
height: 0;
186+
}
187+
100% {
188+
opacity: 0;
189+
width: 167px;
190+
height: 167px;
191+
}
192+
}
193+
194+
@keyframes bounce {
195+
0% {
196+
transform: translate(1px, 1px) rotate(0deg);
197+
}
198+
10% {
199+
transform: translate(-1px, -2px) rotate(-1deg);
200+
}
201+
20% {
202+
transform: translate(-3px, 0) rotate(1deg);
203+
}
204+
30% {
205+
transform: translate(3px, 2px) rotate(0deg);
206+
}
207+
40% {
208+
transform: translate(1px, -1px) rotate(1deg);
209+
}
210+
50% {
211+
transform: translate(-1px, 2px) rotate(-1deg);
212+
}
213+
60% {
214+
transform: translate(-3px, 1px) rotate(0deg);
215+
}
216+
70% {
217+
transform: translate(3px, 1px) rotate(-1deg);
218+
}
219+
80% {
220+
transform: translate(-1px, -1px) rotate(1deg);
221+
}
222+
90% {
223+
transform: translate(1px, 2px) rotate(0deg);
224+
}
225+
100% {
226+
transform: translate(1px, -2px) rotate(-1deg);
227+
}
228+
}

0 commit comments

Comments
 (0)