Skip to content

Commit ebea48e

Browse files
add rss, and some design improvements
1 parent 16669d7 commit ebea48e

5 files changed

Lines changed: 272 additions & 13 deletions

File tree

public/images/main/ada-editor.jpeg

423 KB
Loading

src/content.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import authorEntries from '../Resources/authors.json' with { type: 'json' }
44
export type ArticleAuthor = {
55
name: string
66
url?: string
7+
avatar?: string
78
}
89

910
export type ArticleFrontmatter = {
@@ -43,6 +44,7 @@ type AuthorEntry = {
4344
username?: string
4445
url?: string
4546
profileUrl?: string
47+
avatar?: string
4648
socials?: AuthorSocial[]
4749
}
4850

@@ -188,6 +190,7 @@ function resolveAuthor(value: unknown, filePath: string): ArticleAuthor {
188190
return {
189191
name: author.name,
190192
url: authorUrlFor(author),
193+
avatar: typeof author.avatar === 'string' ? author.avatar : undefined,
191194
}
192195
}
193196

@@ -325,7 +328,7 @@ function renderArticleMedia(line: string): string | null {
325328
const isVideo = Boolean(videoMatch) || /\.(mp4|webm|ogg|mov)$/i.test(rawSrc)
326329
const media = isVideo
327330
? `<video controls playsinline preload="metadata" src="${escapeHtml(src)}">${escapeHtml(alt)}</video>`
328-
: `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" loading="lazy" />`
331+
: `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" loading="lazy" role="button" tabindex="0" aria-label="Open image fullscreen" data-article-lightbox-image />`
329332

330333
return `
331334
<figure class="article-media ${isVideo ? 'article-media-video' : 'article-media-image'}">

src/main.ts

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,18 +1037,58 @@ function renderArticlePage(slug: string) {
10371037
${renderArticleReaderNavigation(article.toc)}
10381038
</div>
10391039
</main>
1040+
${renderArticleImageLightbox()}
10401041
${renderFooter()}
10411042
`
10421043
}
10431044

1045+
function renderArticleImageLightbox(): string {
1046+
return `
1047+
<div class="article-image-lightbox" role="dialog" aria-modal="true" aria-label="Fullscreen article image" hidden data-article-lightbox>
1048+
<button class="article-image-lightbox-backdrop" type="button" aria-label="Close fullscreen image" data-article-lightbox-close></button>
1049+
<figure class="article-image-lightbox-frame">
1050+
<button class="article-image-lightbox-close demo-player-fullscreen" type="button" aria-label="Close fullscreen image" title="Close" data-article-lightbox-close>
1051+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
1052+
<path d="m6 6 12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round"/>
1053+
</svg>
1054+
</button>
1055+
<img src="" alt="" data-article-lightbox-preview />
1056+
<figcaption data-article-lightbox-caption hidden></figcaption>
1057+
</figure>
1058+
</div>
1059+
`
1060+
}
1061+
10441062
function renderArticleAuthor(author: ArticleAuthor): string {
10451063
const label = `By ${author.name}`
1064+
const avatar = author.avatar
1065+
? `<img class="article-author-avatar" src="${escapeHtml(authorAvatarAssetFor(author.avatar))}" alt="${escapeHtml(`${author.name} avatar`)}" loading="lazy" />`
1066+
: ''
1067+
const content = `
1068+
${avatar}
1069+
<span class="article-author-label">${escapeHtml(label)}</span>
1070+
`
10461071

10471072
if (!author.url || !isSafeAuthorUrl(author.url)) {
1048-
return `<span>${escapeHtml(label)}</span>`
1073+
return `<span class="article-author">${content}</span>`
10491074
}
10501075

1051-
return `<a class="article-author-link" href="${escapeHtml(author.url)}" target="_blank" rel="author noreferrer">${escapeHtml(label)}</a>`
1076+
return `
1077+
<a class="article-author article-author-link" href="${escapeHtml(author.url)}" target="_blank" rel="author noreferrer">
1078+
${content}
1079+
<svg class="article-author-arrow" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
1080+
<path d="M5 3.5h7.5V11M12.25 3.75 4 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
1081+
</svg>
1082+
</a>
1083+
`
1084+
}
1085+
1086+
function authorAvatarAssetFor(path: string): string {
1087+
if (/^(https?:|data:|blob:|\/)/.test(path) || path.startsWith('images/')) {
1088+
return assetFor(path)
1089+
}
1090+
1091+
return assetFor(`images/${path}`)
10521092
}
10531093

10541094
function renderArticleTocLinks(toc: ArticleHeading[], context: 'desktop' | 'mobile'): string {
@@ -1135,6 +1175,7 @@ function setupArticleReadingNavigation() {
11351175
if (!content || !headings.length || !links.length) return
11361176

11371177
let mobileSheetCloseTimer: number | undefined
1178+
let lastActiveId: string | undefined
11381179

11391180
const setMobileSheetOpen = (isOpen: boolean) => {
11401181
if (!mobileToggle || !mobileSheet || !mobileNav) return
@@ -1165,6 +1206,28 @@ function setupArticleReadingNavigation() {
11651206
setMobileSheetOpen(false)
11661207
}
11671208

1209+
const scrollTocLinkIntoView = (link: HTMLElement) => {
1210+
const container = link.closest<HTMLElement>('.article-toc-list, .article-mobile-toc-list')
1211+
if (!container) return
1212+
1213+
const containerRect = container.getBoundingClientRect()
1214+
const linkRect = link.getBoundingClientRect()
1215+
const scrollPadding = 16
1216+
const isAboveView = linkRect.top < containerRect.top + scrollPadding
1217+
const isBelowView = linkRect.bottom > containerRect.bottom - scrollPadding
1218+
1219+
if (!isAboveView && !isBelowView) return
1220+
1221+
const scrollDelta = isAboveView
1222+
? linkRect.top - containerRect.top - scrollPadding
1223+
: linkRect.bottom - containerRect.bottom + scrollPadding
1224+
1225+
container.scrollTo({
1226+
top: container.scrollTop + scrollDelta,
1227+
behavior: 'smooth',
1228+
})
1229+
}
1230+
11681231
links.forEach((link) => {
11691232
link.addEventListener('click', (event) => {
11701233
const headingId = link.dataset.articleTocLink
@@ -1203,6 +1266,9 @@ function setupArticleReadingNavigation() {
12031266
const progress = contentEnd <= contentStart ? 1 : Math.min(1, Math.max(0, (window.scrollY - contentStart) / (contentEnd - contentStart)))
12041267
const progressText = `${Math.round(progress * 100)}%`
12051268
const activeTitle = activeHeading.textContent?.trim() || 'Start'
1269+
const didActiveSectionChange = activeId !== lastActiveId
1270+
1271+
lastActiveId = activeId
12061272

12071273
progressFills.forEach((fill) => {
12081274
fill.style.transform = `scaleX(${progress})`
@@ -1217,6 +1283,10 @@ function setupArticleReadingNavigation() {
12171283
const isActive = link.dataset.articleTocLink === activeId
12181284
link.classList.toggle('is-active', isActive)
12191285
link.setAttribute('aria-current', isActive ? 'true' : 'false')
1286+
1287+
if (isActive && didActiveSectionChange) {
1288+
scrollTocLinkIntoView(link)
1289+
}
12201290
})
12211291
}
12221292

@@ -1341,6 +1411,58 @@ function setupInteractions() {
13411411
setupDemoFullscreen()
13421412
setupDemoAmbientLight()
13431413
setupArticleReadingNavigation()
1414+
setupArticleImageLightbox()
1415+
}
1416+
1417+
function setupArticleImageLightbox() {
1418+
const lightbox = document.querySelector<HTMLElement>('[data-article-lightbox]')
1419+
const preview = lightbox?.querySelector<HTMLImageElement>('[data-article-lightbox-preview]')
1420+
const caption = lightbox?.querySelector<HTMLElement>('[data-article-lightbox-caption]')
1421+
const closeButton = lightbox?.querySelector<HTMLButtonElement>('.article-image-lightbox-close')
1422+
let previouslyFocused: Element | null = null
1423+
1424+
if (!lightbox || !preview || !caption) return
1425+
1426+
const closeLightbox = () => {
1427+
lightbox.hidden = true
1428+
preview.removeAttribute('src')
1429+
document.body.classList.remove('article-lightbox-open')
1430+
1431+
if (previouslyFocused instanceof HTMLElement) {
1432+
previouslyFocused.focus()
1433+
}
1434+
}
1435+
1436+
const openLightbox = (image: HTMLImageElement) => {
1437+
const figure = image.closest('figure')
1438+
const figureCaption = figure?.querySelector('figcaption')?.textContent?.trim() ?? ''
1439+
1440+
previouslyFocused = document.activeElement
1441+
preview.src = image.currentSrc || image.src
1442+
preview.alt = image.alt
1443+
caption.textContent = figureCaption
1444+
caption.hidden = !figureCaption
1445+
lightbox.hidden = false
1446+
document.body.classList.add('article-lightbox-open')
1447+
closeButton?.focus()
1448+
}
1449+
1450+
document.querySelectorAll<HTMLImageElement>('[data-article-lightbox-image]').forEach((image) => {
1451+
image.addEventListener('click', () => openLightbox(image))
1452+
image.addEventListener('keydown', (event) => {
1453+
if (event.key !== 'Enter' && event.key !== ' ') return
1454+
event.preventDefault()
1455+
openLightbox(image)
1456+
})
1457+
})
1458+
1459+
lightbox.querySelectorAll('[data-article-lightbox-close]').forEach((control) => {
1460+
control.addEventListener('click', closeLightbox)
1461+
})
1462+
1463+
document.addEventListener('keydown', (event) => {
1464+
if (event.key === 'Escape' && !lightbox.hidden) closeLightbox()
1465+
})
13441466
}
13451467

13461468
function setupDemoFullscreen() {

src/routing.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,20 @@ assert.match(llms, /https:\/\/github\.com\/AdaEngine\/AdaEngine/)
140140
const releaseArticleSource = readFileSync('src/content/articles/introducing-adaengine-0-1-0.md', 'utf8')
141141
assert.match(releaseArticleSource, /author: "SpectralDragon"/)
142142

143+
const authorsSource = readFileSync('Resources/authors.json', 'utf8')
144+
assert.match(authorsSource, /"avatar": "authors\/spectraldragon\.jpg"/)
145+
146+
const contentSource = readFileSync('src/content.ts', 'utf8')
147+
assert.match(contentSource, /data-article-lightbox-image/)
148+
143149
const mainSource = readFileSync('src/main.ts', 'utf8')
144150
assert.doesNotMatch(mainSource, /AdaEngine Team/)
145-
assert.match(mainSource, /class="article-author-link"/)
151+
assert.match(mainSource, /class="article-author article-author-link"/)
152+
assert.match(mainSource, /class="article-author-avatar"/)
153+
assert.match(mainSource, /class="article-author-arrow"/)
154+
assert.match(mainSource, /assetFor\(`images\/\$\{path\}`\)/)
155+
assert.match(mainSource, /class="article-image-lightbox"/)
156+
assert.match(mainSource, /setupArticleImageLightbox\(\)/)
146157
assert.match(mainSource, /rel="author noreferrer"/)
147158
assert.match(mainSource, /https:\/\/docs\.adaengine\.org\//)
148159
assert.match(mainSource, /https:\/\/docs\.adaengine\.org\/tutorials\/adaengine/)

0 commit comments

Comments
 (0)