Skip to content

Commit 7f3f7da

Browse files
committed
[2.0.2] feat: Add BibTeX syntax highlighting and polish mobile UX
Before: BibTeX citations were monochrome text blobs in both the article cite panel and the footer. Switching formats hard-swapped with no transition. The nav search button did nothing on mobile. The floating filter FAB covered the footer citation block on short scrolls. After: BibTeX entries get colored syntax — types, keys, delimiters each get their own class — shared through a single formatBibtexHtml function so article and footer rendering never drift. Format switching crossfades. Copy tooltip confirms which format was grabbed ("BibTeX copied" / "APA copied"). On mobile, the nav search button closes the hamburger menu (if open) and opens the filter bottom sheet with the search input focused. Keyboard shortcut / does the same via open-mobile-search custom event. The filter FAB fades out when the footer enters the viewport via IntersectionObserver. Engagement bar groups Share + Cite in a centered row instead of stacking everything vertically with full- width separators. Share dropdown centers itself within viewport bounds instead of overflowing off-screen. Also: - Fix article content overflow on narrow viewports (min-width: 0) - CatalogHero trust badge: text-wrap balance, span wrapper to prevent icon reflowing into wrapped text - BrowseNav wraps on small screens, dots hidden - Footer copyright name links to author profile - Feedback follow-up animates with opacity instead of display toggle - Extract shared E2E helpers to tests/e2e/helpers.ts - Fix webkit test failures in 404 suggestion link navigation - Fix analytics-consent race condition: await astro:page-load - Mark axe-core a11y tests as test.slow() to stop CI flakes - Add CHANGELOG entry for 2.0.2
1 parent 60e6ced commit 7f3f7da

23 files changed

Lines changed: 420 additions & 130 deletions

docs/CHANGELOG.md

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,69 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
---
88

9-
## [`2.0.1`] - 2026-03-29
9+
## [`2.0.2`] - 2026-03-30
10+
11+
**Citations look like they belong in a paper now, and mobile stopped pretending it's desktop.** BibTeX gets syntax highlighting everywhere it appears, the search button actually works on phones, and a handful of layout rough edges got sanded down.
12+
13+
### Added
14+
15+
- **BibTeX syntax highlighting** - entry types, field names, and delimiters get distinct colors in both the article cite panel and footer citation block. Shared via `formatBibtexHtml` in `src/lib/citation.ts` so the two never drift.
16+
- **Citation format crossfade** - switching between BibTeX / APA / Markdown fades the content out and back in instead of hard-swapping. Copy button tooltip confirms which format was copied.
17+
- **Mobile search integration** - the nav search button now opens the filter bottom sheet with the search input focused (dispatches `open-mobile-search` event). Keyboard shortcut `/` does the same. Closes the mobile nav first if it's open.
18+
- **Mobile FAB auto-hide** - the floating "Filters" button fades out when the footer scrolls into view, so it stops covering the citation block on short pages
19+
- **E2E test helpers module** - `isMobileProject`, `waitForNextAstroPageLoad`, `setTheme`, `trackConsoleErrors` extracted to `tests/e2e/helpers.ts` and shared across specs
20+
21+
### Changed
22+
23+
- Engagement bar stacks cleanly on mobile: Share + Cite stay grouped in a row, feedback sits above, separators hidden
24+
- Share dropdown centers itself on mobile viewports instead of overflowing off-screen
25+
- Feedback follow-up text animates in with opacity transition instead of a `display: none` toggle
26+
- CatalogHero trust badge uses `text-wrap: balance` and wraps the text in a `<span>` so the book icon doesn't reflow into the second line
27+
- BrowseNav wraps gracefully on small screens (dots hidden)
28+
- Footer copyright name now links to author profile
29+
- Accessibility axe-core tests marked `test.slow()` to stop flaking under load
30+
31+
### Fixed
32+
33+
- Article content overflowing its container on narrow viewports (`min-width: 0` on the content column)
34+
- 404 suggestion link navigation failing in webkit - uses `force: true` click with parallel `waitForURL`
35+
- Analytics consent test race condition on Astro navigation - now awaits `astro:page-load` event before asserting page view count
36+
- Console error filtering in 404 tests - expected 404 resource errors no longer pollute the error list
37+
38+
---
39+
40+
## [`2.0.1`] - 2026-03-30
1041

1142
**Users can now quiet animations without touching their OS settings.** A sparkles toggle in the nav gives per-site control, persisted across sessions.
1243

1344
### Added
1445

15-
- **Animation toggle** sparkles button in the nav lets users reduce motion on this site alone. Bounces when sparkles come back to life; goes instantly quiet when they don't.
16-
- **Tooltip system** for nav icon buttons CSS-only `data-tooltip` with arrow, hover delay, and keyboard focus support, replacing native `title` attributes
17-
- **Motion contract** (`motion-contract.ts`) shared constants and types mirroring the theme system architecture
18-
- **Motion preference blocking script** resolves `html[data-motion]` before first paint (localStorage first, OS `prefers-reduced-motion` as fallback), stamps new documents on view transitions
46+
- **Animation toggle** - sparkles button in the nav lets users reduce motion on this site alone. Bounces when sparkles come back to life; goes instantly quiet when they don't.
47+
- **Tooltip system** for nav icon buttons - CSS-only `data-tooltip` with arrow, hover delay, and keyboard focus support, replacing native `title` attributes
48+
- **Motion contract** (`motion-contract.ts`) - shared constants and types mirroring the theme system architecture
49+
- **Motion preference blocking script** - resolves `html[data-motion]` before first paint (localStorage first, OS `prefers-reduced-motion` as fallback), stamps new documents on view transitions
1950

2051
### Changed
2152

22-
- Motion system rewired from fifteen independent `@media (prefers-reduced-motion)` blocks to one `html[data-motion]` attribute driving a global CSS kill switch near-zero durations on all transitions, animations, and view transitions when reduced
53+
- Motion system rewired from fifteen independent `@media (prefers-reduced-motion)` blocks to one `html[data-motion]` attribute driving a global CSS kill switch - near-zero durations on all transitions, animations, and view transitions when reduced
2354
- Mobile nav hardened against view-transition DOM replacement: lazy element lookups, re-bound listeners on `astro:page-load`, duplicate-event guards via data attributes
2455
- E2E test helpers extracted for consent flow, catalog filter scoping, and stable click-with-scroll patterns; clipboard tests skip non-Chromium; force-click fallbacks for mobile viewports
2556

2657
### Fixed
2758

28-
- Horizontal overflow from `left: -100vw; right: -100vw` on full-bleed decorative backgrounds in CatalogHero and ArticleLayout replaced with `translateX(-50%)` centering
29-
- ThemeToggle spin animation clipping tooltip pseudo-elements now targets inner icons instead of the button, `overflow: hidden` dropped
59+
- Horizontal overflow from `left: -100vw; right: -100vw` on full-bleed decorative backgrounds in CatalogHero and ArticleLayout - replaced with `translateX(-50%)` centering
60+
- ThemeToggle spin animation clipping tooltip pseudo-elements - now targets inner icons instead of the button, `overflow: hidden` dropped
3061

3162
---
3263

3364
## [`2.0.0`] - 2026-03-23
3465

35-
**Complete rewrite Gatsby → Astro 5.** New architecture, new design, same 56 smells.
66+
**Complete rewrite - Gatsby → Astro 5.** New architecture, new design, same 56 smells.
3667

3768
### Architecture
3869

3970
- Migrated from Gatsby (React, Material UI) to **Astro 5** with static output
40-
- Adopted **Preact islands** for interactive components (FilterSidebar, CodeExample) most pages ship zero framework JS
71+
- Adopted **Preact islands** for interactive components (FilterSidebar, CodeExample) - most pages ship zero framework JS
4172
- Replaced client-side state with **Nano Stores** (shared between islands and vanilla scripts)
4273
- Switched styling from Material UI to **Tailwind CSS v4** (via `@tailwindcss/vite`)
4374
- Self-hosted fonts via **Fontsource** (Fraunces, Plus Jakarta Sans, JetBrains Mono)
@@ -351,6 +382,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
351382
[`1.0.20`]: https://github.com/Luzkan/smells/releases/tag/1.0.20
352383
[`1.0.21`]: https://github.com/Luzkan/smells/releases/tag/1.0.21
353384
[`1.0.22`]: https://github.com/Luzkan/smells/releases/tag/1.0.22
385+
[`2.0.2`]: https://github.com/Luzkan/smells/releases/tag/2.0.2
354386
[`2.0.1`]: https://github.com/Luzkan/smells/releases/tag/2.0.1
355387
[`2.0.0`]: https://github.com/Luzkan/smells/releases/tag/2.0.0
356388
[`1.0.23-alpha.1`]: https://github.com/Luzkan/smells/releases/tag/1.0.23-alpha.1

src/components/article/EngagementBar.astro

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ const { citation } = Astro.props;
1515
<div class="engagement-bar">
1616
<FeedbackButton slug={citation.slug} />
1717
<span class="engage-sep"></span>
18-
<ShareButton slug={citation.slug} />
19-
<span class="engage-sep"></span>
20-
<CiteToggle />
18+
<div class="engage-actions">
19+
<ShareButton slug={citation.slug} />
20+
<span class="engage-sep"></span>
21+
<CiteToggle />
22+
</div>
2123
</div>
2224
<CitePanel citation={citation} />
2325

@@ -34,6 +36,10 @@ const { citation } = Astro.props;
3436
margin-bottom: 0;
3537
}
3638

39+
.engage-actions {
40+
display: contents;
41+
}
42+
3743
.engage-sep {
3844
width: 1px;
3945
height: 24px;
@@ -71,13 +77,21 @@ const { citation } = Astro.props;
7177
@media (max-width: 640px) {
7278
.engagement-bar {
7379
flex-direction: column;
80+
align-items: center;
7481
padding: 16px 20px;
7582
gap: 12px;
83+
margin-bottom: 16px;
84+
}
85+
86+
.engage-actions {
87+
display: flex;
88+
align-items: center;
89+
justify-content: center;
90+
gap: 12px;
7691
}
7792

7893
.engage-sep {
79-
width: 100%;
80-
height: 1px;
94+
display: none;
8195
}
8296
}
8397
</style>

src/components/catalog/BrowseNav.astro

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,5 +156,19 @@ const { totalSmells, allSlugs, currentSlug } = Astro.props;
156156
}
157157
}
158158

159+
@media (max-width: 640px) {
160+
.browse-nav {
161+
flex-wrap: wrap;
162+
}
163+
164+
.browse-nav__all {
165+
padding: 8px 18px;
166+
}
167+
168+
.browse-nav__dot {
169+
display: none;
170+
}
171+
}
172+
159173
/* diceShake keyframes defined globally in global.css */
160174
</style>

src/components/catalog/CatalogHero.astro

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ const { totalCount, categoryCount, hierarchyCount } = Astro.props;
4545
{/* HERO-05: Springer Nature trust badge */}
4646
<a class="catalog-hero__trust" href={SPRINGER_PAPER_URL} target="_blank" rel="noopener">
4747
<BookIcon class="catalog-hero__trust-icon" size={14} strokeWidth={2} />
48-
Companion to peer-reviewed research published by <strong>Springer Nature</strong>
48+
{/* Wrapper <span> prevents the icon from reflowing into wrapped text on small screens */}
49+
<span>Companion to peer-reviewed research published by <strong>Springer Nature</strong></span>
4950
</a>
5051
</section>
5152

@@ -190,6 +191,7 @@ const { totalCount, categoryCount, hierarchyCount } = Astro.props;
190191
background: var(--surface);
191192
transition: all 0.25s var(--ease-smooth);
192193
text-decoration: none;
194+
text-wrap: balance;
193195
}
194196

195197
.catalog-hero__trust:hover {

src/components/engagement/CitePanel.astro

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ interface Props {
77
import Icon from '../icons/Icon.astro';
88
import { ICON_COPY } from '../../lib/icon-paths';
99
import { SITE_URL } from '../../lib/constants';
10+
import { buildArticleBibtex, formatBibtexHtml } from '../../lib/citation';
1011
1112
const { citation } = Astro.props;
1213
const { slug, title, author, year } = citation;
1314
const url = `${SITE_URL}/smells/${slug}`;
15+
const bibtexRaw = buildArticleBibtex(slug, title, author, year, url);
16+
const bibtexHtml = formatBibtexHtml(bibtexRaw);
1417
---
1518

1619
<div class="cite-panel" id="cite-panel" data-slug={slug} aria-hidden="true" inert>
@@ -25,11 +28,11 @@ const url = `${SITE_URL}/smells/${slug}`;
2528
</div>
2629
<div class="cite-block">
2730
<pre
28-
class="cite-block__code"
29-
data-bibtex={`@misc{jerzyk${year}${slug.replace(/-/g, '')},\n title = {${title} — Code Smells Catalog},\n author = {${author}},\n year = {${year}},\n url = {${url}}\n}`}
31+
class="cite-bib-block"
32+
data-bibtex={bibtexRaw}
33+
data-bibtex-html={bibtexHtml}
3034
data-apa={`${author} (${year}). ${title} — Code Smells Catalog. ${url}`}
31-
data-markdown={`[${title}](${url}) — *Code Smells Catalog* by ${author} (${year})`}>
32-
</pre>
35+
data-markdown={`[${title}](${url}) — *Code Smells Catalog* by ${author} (${year})`}><code class="cite-block__code" /></pre>
3336
<button class="cite-copy-btn" title="Copy citation" aria-label="Copy citation">
3437
<Icon icon={ICON_COPY} size={12} />
3538
</button>
@@ -39,7 +42,7 @@ const url = `${SITE_URL}/smells/${slug}`;
3942

4043
<script>
4144
import { trackEvent } from '../../lib/analytics/tracker';
42-
import { copyToClipboardWithFeedback } from '../../lib/copy-code';
45+
import { copyToClipboardWithFeedback, COPY_FEEDBACK_MS } from '../../lib/copy-code';
4346
import { initOnce } from '../../lib/lifecycle';
4447

4548
function toCiteFormat(value: string | null): 'bibtex' | 'apa' | 'markdown' {
@@ -49,10 +52,24 @@ const url = `${SITE_URL}/smells/${slug}`;
4952

5053
initOnce('.cite-panel', (citePanel) => {
5154
const slug = citePanel.getAttribute('data-slug') || '';
52-
const citeBlock = citePanel.querySelector('.cite-block__code');
53-
if (!(citeBlock instanceof HTMLElement)) return;
55+
const citePre = citePanel.querySelector('.cite-bib-block');
56+
const citeCodeEl = citePanel.querySelector('.cite-block__code');
57+
if (!(citePre instanceof HTMLElement) || !(citeCodeEl instanceof HTMLElement)) return;
58+
59+
const pre = citePre;
60+
const code = citeCodeEl;
61+
let fadeRaf: number | null = null;
62+
function crossfade(updateFn: () => void): void {
63+
if (fadeRaf !== null) cancelAnimationFrame(fadeRaf);
64+
code.classList.add('cite-block__code--fading');
65+
fadeRaf = requestAnimationFrame(() => {
66+
updateFn();
67+
code.classList.remove('cite-block__code--fading');
68+
fadeRaf = null;
69+
});
70+
}
5471

55-
citeBlock.textContent = citeBlock.getAttribute('data-bibtex') || '';
72+
code.innerHTML = pre.getAttribute('data-bibtex-html') || '';
5673

5774
const tabs = citePanel.querySelectorAll('.cite-tab');
5875
tabs.forEach((tab) => {
@@ -61,15 +78,21 @@ const url = `${SITE_URL}/smells/${slug}`;
6178
tabs.forEach((candidate) => candidate.classList.remove('cite-tab--active'));
6279
tab.classList.add('cite-tab--active');
6380
const format = toCiteFormat(tab.getAttribute('data-format'));
64-
citeBlock.textContent = citeBlock.getAttribute(`data-${format}`) || '';
81+
crossfade(() => {
82+
if (format === 'bibtex') {
83+
code.innerHTML = pre.getAttribute('data-bibtex-html') || '';
84+
} else {
85+
code.textContent = pre.getAttribute(`data-${format}`) || '';
86+
}
87+
});
6588
});
6689
});
6790

6891
const copyButton = citePanel.querySelector('.cite-copy-btn');
6992
if (!(copyButton instanceof HTMLButtonElement)) return;
7093

7194
copyButton.addEventListener('click', async () => {
72-
const copied = await copyToClipboardWithFeedback(citeBlock.textContent || '', copyButton, {
95+
const copied = await copyToClipboardWithFeedback(code.textContent || '', copyButton, {
7396
feedbackClass: 'cite-copy-btn--copied',
7497
});
7598
if (!copied) return;
@@ -78,6 +101,12 @@ const url = `${SITE_URL}/smells/${slug}`;
78101
const format = toCiteFormat(
79102
activeTab instanceof HTMLElement ? activeTab.getAttribute('data-format') : null,
80103
);
104+
const formatLabel = format === 'bibtex' ? 'BibTeX' : format === 'apa' ? 'APA' : 'Markdown';
105+
copyButton.title = `${formatLabel} copied`;
106+
setTimeout(() => {
107+
copyButton.title = 'Copy citation';
108+
}, COPY_FEEDBACK_MS);
109+
81110
trackEvent({ name: 'cite_copy', params: { format, smell: slug } });
82111
});
83112
});
@@ -156,19 +185,25 @@ const url = `${SITE_URL}/smells/${slug}`;
156185
padding: 14px 44px 14px 14px;
157186
}
158187

159-
.cite-block__code {
160-
font-family: var(--font-mono);
161-
font-size: 11px;
162-
line-height: 1.55;
163-
color: var(--text-secondary);
164-
white-space: pre-wrap;
165-
word-break: break-word;
166-
margin: 0;
167-
/* Override global <pre> styles: keep citation text on the white cite-block surface. */
188+
.cite-bib-block {
168189
background: transparent;
169190
padding: 0;
170191
border-radius: 0;
171192
overflow: visible;
193+
margin: 0;
194+
}
195+
196+
.cite-block__code {
197+
font-family: var(--font-mono);
198+
font-size: 11px;
199+
line-height: 1.7;
200+
color: var(--text-secondary);
201+
transition: opacity 0.15s ease;
202+
}
203+
204+
.cite-block__code--fading {
205+
opacity: 0;
206+
transition: none;
172207
}
173208

174209
.cite-copy-btn {

src/components/engagement/FeedbackButton.astro

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ const { slug } = Astro.props;
138138
gap: 10px;
139139
flex: 1;
140140
min-width: 0;
141+
position: relative;
141142
}
142143

143144
.engage-feedback__label {
@@ -184,10 +185,14 @@ const { slug } = Astro.props;
184185
}
185186

186187
.engage-feedback__followup {
187-
display: none;
188+
display: inline-flex;
189+
visibility: hidden;
190+
opacity: 0;
191+
position: absolute;
188192
font-size: var(--text-sm);
189193
font-weight: 500;
190194
color: var(--text-secondary);
195+
transition: opacity 0.2s ease;
191196
}
192197

193198
.engage-feedback__followup a {
@@ -210,7 +215,9 @@ const { slug } = Astro.props;
210215
}
211216

212217
.engage-feedback.submitted .engage-feedback__followup.active {
213-
display: inline-flex;
218+
visibility: visible;
219+
opacity: 1;
220+
position: static;
214221
}
215222

216223
@media (max-width: 640px) {

src/components/engagement/ShareButton.astro

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,12 @@ const shareDropdownId = `share-dropdown-${slug}`;
240240

241241
@media (max-width: 640px) {
242242
.share-dropdown {
243-
bottom: auto;
244-
top: calc(100% + 8px);
243+
left: 50%;
244+
transform: translateX(-50%) translateY(4px) scale(0.96);
245+
}
246+
247+
.share-dropdown--open {
248+
transform: translateX(-50%) translateY(0) scale(1);
245249
}
246250
}
247251
</style>

src/components/islands/FilterSidebar.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,12 @@
562562
transition: all 0.25s var(--ease-spring);
563563
}
564564

565+
.filter-sidebar__mobile-fab--hidden {
566+
opacity: 0;
567+
pointer-events: none;
568+
transform: translateX(-50%) translateY(12px);
569+
}
570+
565571
.filter-sidebar__mobile-fab:hover {
566572
transform: translateX(-50%) scale(1.04);
567573
box-shadow: var(--shadow-lg);

0 commit comments

Comments
 (0)