Skip to content

Commit 0abbc72

Browse files
committed
Make popups wikipedia style
History was rewritten to sidestep a merge conflict
1 parent 67047b4 commit 0abbc72

File tree

3 files changed

+202
-67
lines changed

3 files changed

+202
-67
lines changed

app/components/Article/Contents.tsx

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,73 @@ const footnoteHTML = (el: HTMLDivElement, e: HTMLAnchorElement): string | null =
2525
return elem.firstElementChild?.innerHTML || null
2626
}
2727

28-
const addPopup = (e: HTMLElement, id: string, contents: string, mobile: boolean): HTMLElement => {
28+
const addPopup = (
29+
e: HTMLElement,
30+
id: string,
31+
contents: string,
32+
mobile: boolean,
33+
layout?: string
34+
): HTMLElement => {
2935
const preexisting = document.getElementById(id)
3036
if (preexisting) return preexisting
3137

3238
const popup = document.createElement('div')
3339
popup.className = 'link-popup bordered small background'
40+
if (layout) {
41+
popup.classList.add(`${layout}-image-layout`)
42+
}
3443
popup.innerHTML = contents
3544
popup.id = id
3645

3746
e.insertAdjacentElement('afterend', popup)
3847

48+
// Timeout management for show/hide delays
49+
let showTimeout: number | null = null
50+
let hideTimeout: number | null = null
51+
52+
const clearTimeouts = () => {
53+
if (showTimeout) {
54+
clearTimeout(showTimeout)
55+
showTimeout = null
56+
}
57+
if (hideTimeout) {
58+
clearTimeout(hideTimeout)
59+
hideTimeout = null
60+
}
61+
}
62+
3963
const toggle = () => popup.classList.toggle('shown')
40-
const show = () => popup.classList.add('shown')
41-
const hide = () => popup.classList.remove('shown')
64+
65+
const show = () => {
66+
clearTimeouts()
67+
showTimeout = setTimeout(() => {
68+
// Position popup above if it would not fit in viewport
69+
const elementRect = e.getBoundingClientRect()
70+
const viewportHeight = window.innerHeight
71+
72+
// Check if popup would fit below the element
73+
const estimatedPopupHeight = 250
74+
const wouldFitBelow = elementRect.bottom + estimatedPopupHeight <= viewportHeight - 50
75+
76+
// If it doesn't fit below, position it above
77+
if (!wouldFitBelow) {
78+
popup.classList.add('position-above')
79+
} else {
80+
popup.classList.remove('position-above')
81+
}
82+
83+
popup.classList.add('shown')
84+
showTimeout = null
85+
}, 500) as unknown as number
86+
}
87+
88+
const hide = () => {
89+
clearTimeouts()
90+
hideTimeout = setTimeout(() => {
91+
popup.classList.remove('shown')
92+
hideTimeout = null
93+
}, 100) as unknown as number
94+
}
4295

4396
if (!mobile) {
4497
e.addEventListener('mouseover', show)
@@ -193,23 +246,45 @@ const insertGlossary = (
193246
linkedQuestionIsViewable &&
194247
`<a href="${questionUrl(entry)}" target="_blank" rel="noopener noreferrer" class="button secondary">View full definition</a>`
195248
const isGoogleDrive = entry.image && entry.image.includes('drive.google.com/file/d/')
196-
const image = entry.image
249+
const imageHtml = entry.image
197250
? isGoogleDrive
198251
? `<iframe src="${entry.image.replace(/\/view$/, '/preview')}" style="width:100%; border:none;" allowFullScreen></iframe>`
199252
: `<img src="${entry.image}"/>`
200253
: ''
254+
255+
// Determine layout based on pre-computed dimensions
256+
let layout: 'right' | 'top' = 'right'
257+
if (entry.image && !isGoogleDrive && entry.imageDimensions) {
258+
const aspectRatio = entry.imageDimensions.width / entry.imageDimensions.height
259+
layout = aspectRatio > 2.0 ? 'top' : 'right'
260+
}
261+
262+
// Create popup with pre-calculated layout
263+
const popupContent = imageHtml
264+
? `<div class="glossary-popup ${layout}-image-layout black small">
265+
<div class="image-container">
266+
${imageHtml}
267+
</div>
268+
<div class="text-content">
269+
<div class="small-bold text-no-wrap">${entry.term}</div>
270+
<div class="definition small">${entry.contents}</div>
271+
${link || ''}
272+
</div>
273+
</div>`
274+
: `<div class="glossary-popup black small">
275+
<div class="text-content full-width">
276+
<div class="small-bold text-no-wrap">${entry.term}</div>
277+
<div class="definition small">${entry.contents}</div>
278+
${link || ''}
279+
</div>
280+
</div>`
281+
201282
addPopup(
202283
e as HTMLSpanElement,
203284
`glossary-${entry.term}`,
204-
`<div class="glossary-popup flex-container black small">
205-
<div class="contents ${image ? '' : 'full-width'}">
206-
<div class="small-bold text-no-wrap">${entry.term}</div>
207-
<div class="definition small">${entry.contents}</div>
208-
${link || ''}
209-
</div>
210-
${image || ''}
211-
</div>`,
212-
mobile
285+
popupContent,
286+
mobile,
287+
imageHtml ? layout : undefined
213288
)
214289
})
215290

app/components/Article/article.css

Lines changed: 109 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,90 @@ article .glossary-entry {
155155
article .link-popup {
156156
visibility: hidden;
157157
z-index: 4;
158-
width: 512px;
158+
width: auto;
159+
min-width: 350px;
160+
}
161+
162+
/* Right image layout - image on the right side */
163+
article .glossary-popup.right-image-layout {
164+
display: flex;
165+
flex-direction: row;
166+
min-width: 530px; /* Minimum width to contain text + image (350 + 180) */
167+
width: fit-content;
168+
}
169+
170+
/* Top image layout - image on top */
171+
article .glossary-popup.top-image-layout {
172+
display: flex;
173+
flex-direction: column;
174+
}
175+
176+
/* Right image layout - image container */
177+
article .glossary-popup.right-image-layout .image-container {
178+
flex: 0 0 180px;
179+
order: 2;
180+
display: flex;
181+
align-items: center;
182+
justify-content: center;
183+
padding: var(--spacing-8);
184+
overflow: hidden;
185+
}
186+
187+
/* Top image layout - image container */
188+
article .glossary-popup.top-image-layout .image-container {
189+
flex: 0 0 100px; /* Fixed height for top images */
190+
order: 1;
191+
width: 100%;
192+
display: flex;
193+
align-items: center;
194+
justify-content: center;
195+
padding: var(--spacing-8) var(--spacing-16);
196+
}
197+
198+
/* Right image layout - text content */
199+
article .glossary-popup.right-image-layout .text-content {
200+
flex: 0 0 350px;
201+
order: 1;
202+
padding: var(--spacing-24);
203+
}
204+
205+
/* Top image layout - text content */
206+
article .glossary-popup.top-image-layout .text-content {
207+
flex: 1;
208+
order: 2;
209+
padding: var(--spacing-24);
210+
}
211+
212+
/* Image styling for all glossary popup images */
213+
article .glossary-popup .image-container img {
214+
border-radius: var(--border-radius);
215+
}
216+
217+
/* Right image layout - resize images to fit in fixed container */
218+
article .glossary-popup.right-image-layout .image-container img {
219+
width: auto;
220+
height: auto;
221+
max-width: 164px;
222+
max-height: 250px;
223+
}
224+
225+
/* Top image layout - resize images to fit in full width container */
226+
article .glossary-popup.top-image-layout .image-container img {
227+
max-height: 100px;
228+
width: auto;
229+
max-width: 100%;
230+
object-fit: scale-down;
231+
}
232+
233+
/* Text content for popups without images */
234+
article .glossary-popup .text-content.full-width {
235+
padding: var(--spacing-24);
236+
}
237+
238+
/* Remove margins from buttons in glossary popups */
239+
article .glossary-popup .button {
240+
margin: 0;
241+
margin-top: var(--spacing-8);
159242
}
160243

161244
article .link-popup .footnote {
@@ -179,6 +262,7 @@ article .footnote-ref {
179262
article .glossary-popup {
180263
border-radius: var(--border-radius);
181264
overflow: hidden;
265+
182266
height: 304px;
183267
/* Reset any inherited formatting from the glossary term */
184268
font-style: normal !important;
@@ -193,15 +277,15 @@ article .link-popup .glossary-popup > .contents {
193277
padding: var(--spacing-24) var(--spacing-40) var(--spacing-24);
194278
}
195279
article .definition {
196-
height: 140px;
280+
max-height: 300px;
197281
display: -webkit-box;
198282
/* These are webkit specific things, so might not work in all browsers (firefox handles them fine) */
199283
-webkit-box-orient: vertical;
200284
-webkit-line-clamp: 5; /* Number of lines you want to display */
201285
overflow: hidden;
202286
text-overflow: ellipsis;
203-
margin-bottom: var(--spacing-32);
204287
}
288+
205289
article .link-popup .glossary-popup > img {
206290
border-top-right-radius: var(--border-radius);
207291
border-bottom-right-radius: var(--border-radius);
@@ -289,14 +373,33 @@ article blockquote + p {
289373
@media (min-width: 1136px) {
290374
article .link-popup {
291375
position: absolute;
292-
max-width: 480px;
376+
max-width: 400px;
293377
display: inline-block;
294378
z-index: 2;
295379
left: calc(50% - 200px);
296380
transform: translateY(var(--spacing-40));
297381
transition:
298-
visibility 0s 300ms,
299-
opacity cubic-bezier(1, 0, 1, 1) 300ms;
382+
visibility 0s 100ms,
383+
opacity cubic-bezier(1, 0, 1, 1) 100ms;
384+
}
385+
386+
/* Right-image layout popups - wider to accommodate image */
387+
article .link-popup.right-image-layout {
388+
max-width: 530px; /* 350px text + 180px image */
389+
left: calc(50% - 265px);
390+
transform: translateY(var(--spacing-40));
391+
}
392+
393+
/* Top-image layout popups - standard width */
394+
article .link-popup.top-image-layout {
395+
max-width: 400px; /* Same as text-only popups */
396+
left: calc(50% - 200px);
397+
transform: translateY(var(--spacing-40));
398+
}
399+
400+
/* Position above adjustments */
401+
article .link-popup.position-above {
402+
transform: translateY(-100%) translateY(-40px);
300403
}
301404
}
302405

@@ -373,50 +476,3 @@ article .contents tbody tr:nth-child(odd) {
373476
article .contents tbody tr:nth-child(even) {
374477
background-color: var(--background);
375478
}
376-
377-
/* Share dropdown menu */
378-
.share-dropdown {
379-
position: absolute;
380-
top: 100%;
381-
right: 0;
382-
margin-top: var(--spacing-8);
383-
background-color: var(--card-background);
384-
border: 1px solid var(--border);
385-
border-radius: var(--spacing-8);
386-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
387-
z-index: 1000;
388-
min-width: 160px;
389-
overflow: hidden;
390-
}
391-
392-
html.dark .share-dropdown {
393-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
394-
}
395-
396-
.share-menu-item {
397-
display: flex;
398-
align-items: center;
399-
gap: var(--spacing-12);
400-
padding: var(--spacing-12) var(--spacing-16);
401-
color: var(--text);
402-
text-decoration: none;
403-
font-size: 14px;
404-
border: none;
405-
background: none;
406-
width: 100%;
407-
text-align: left;
408-
cursor: pointer;
409-
font-family: inherit;
410-
}
411-
412-
.share-menu-item:hover {
413-
background-color: var(--hover-background);
414-
}
415-
416-
.share-menu-item-icon {
417-
display: flex;
418-
align-items: center;
419-
width: var(--spacing-16);
420-
height: var(--spacing-16);
421-
color: var(--text-secondary);
422-
}

app/server-utils/stampy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export type GlossaryEntry = {
6060
pageid: PageId
6161
contents: string
6262
image: string
63+
imageDimensions?: {width: number; height: number}
6364
}
6465
export type Glossary = {
6566
[key: string]: GlossaryEntry
@@ -375,10 +376,13 @@ export const loadGlossary = withCache('loadGlossary', async () => {
375376
.map((v) => v.trim())
376377
.filter(Boolean),
377378
]
379+
const img = values.image && JSON.parse(values.image as unknown as string)
378380
const item = {
379381
pageid,
380382
term: extractText(values.phrase),
381-
image: values.image?.url,
383+
image: img?.url,
384+
...(img?.width &&
385+
img?.height && {imageDimensions: {width: img.width, height: img.height}}),
382386
contents: renderText(pageid, extractText(values.definition)).html,
383387
}
384388
return phrases

0 commit comments

Comments
 (0)