Skip to content

Commit 0c41b51

Browse files
committed
feat: copy bibtex citations rather than downloading them
Close #3141
1 parent 9c48341 commit 0c41b51

File tree

16 files changed

+1376
-11
lines changed

16 files changed

+1376
-11
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ node_modules/
1616
go.sum
1717
resources/
1818
public/
19+
**/assets/jsconfig.json
1920

2021
# Pagefind search index (generated)
2122
**/pagefind/
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Hugo Blox Tailwind CSS v4 Structure
2+
3+
## Overview
4+
5+
The CSS architecture is modularized for maintainability and organization. The entry point is `main.css` which orchestrates all imports.
6+
7+
## Directory Structure
8+
9+
```
10+
css/
11+
├── main.css # Main entry point
12+
├── color-utilities.css # HB Theme Engine
13+
├── config/ # Tailwind configuration
14+
│ ├── tailwind.css # Tailwind v4 directives (@source, @custom-variant)
15+
│ ├── theme.css # HB Theme variables (@theme block)
16+
│ └── safelist.css # Dynamic classes safelist
17+
├── framework/ # Hugo Blox framework styles
18+
│ ├── base.css # Base styles and typography
19+
│ └── components.css # Framework components
20+
├── components/ # Hugo Blox module components
21+
│ └── all.css # Imports all component files
22+
├── blox/ # Block-specific styles
23+
│ └── all.css # Imports all block styles
24+
└── views/ # Listing view styles
25+
└── all.css # Imports all listing view styles
26+
```
27+
28+
## Key Files
29+
30+
### main.css
31+
The orchestrator that imports everything in the correct order:
32+
1. Tailwind base
33+
2. Configuration
34+
3. Base styles
35+
4. Hugo Blox theme engine
36+
5. Components
37+
6. Safelist
38+
39+
### config/tailwind.css
40+
Contains Tailwind v4-specific directives:
41+
- `@source` - Where to scan for classes
42+
- `@custom-variant` - Custom variants (dark, hover)
43+
- `@plugin` - Plugin imports
44+
45+
### config/theme.css
46+
Defines the theme using `@theme` block:
47+
- Color palettes (primary, secondary, neutral)
48+
- Font families and sizes
49+
- Custom properties
50+
51+
### color-utilities.css
52+
Hugo Blox theme engine with color utility classes:
53+
- All color shades (50-950)
54+
- All variants (hover, dark, focus)
55+
- Gradient utilities
56+
57+
## Tailwind v4 Notes
58+
59+
- `@source`, `@theme`, `@plugin` are new v4 directives
60+
- Linters may show warnings for these - they can be ignored
61+
- Hugo's `css.TailwindCSS` function processes this automatically
62+
- No PostCSS configuration needed anymore
63+
64+
## Maintenance
65+
66+
When adding new styles:
67+
1. Framework classes → `framework/components.css`
68+
2. Theme variables → `config/theme.css`
69+
3. Component styles → `components/[file].css`
70+
4. Dynamic classes → Add to `config/safelist.css`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "attachments.css";
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Citation clipboard handler for Hugo Blox Builder
3+
* Copies BibTeX citation to clipboard when cite button is clicked
4+
*/
5+
6+
import { hugoEnvironment, i18n } from '@params';
7+
import { showNotification } from './hb-notifier.js';
8+
import { copyToClipboardSync, ClipboardCache } from './hb-clipboard.js';
9+
10+
// Debug mode based on environment
11+
const isDebugMode = hugoEnvironment === 'development';
12+
13+
// Cache for citation content
14+
const citationCache = new ClipboardCache();
15+
16+
// Initialize citation handlers when DOM is ready
17+
if (document.readyState === 'loading') {
18+
document.addEventListener('DOMContentLoaded', initializeCitation);
19+
} else {
20+
initializeCitation();
21+
}
22+
23+
function initializeCitation() {
24+
// Handle citation button clicks using event delegation
25+
document.addEventListener('click', handleCiteClick);
26+
27+
// Prefetch citations on hover/focus for better UX and Safari compatibility
28+
document.addEventListener('mouseover', prefetchOnHover);
29+
document.addEventListener('focusin', prefetchOnHover);
30+
31+
// Prefetch all citations on page load
32+
prefetchAllCitations();
33+
}
34+
35+
/**
36+
* Prefetch all citations on page load
37+
*/
38+
function prefetchAllCitations() {
39+
const citeButtons = document.querySelectorAll('.js-cite-clipboard[data-filename]');
40+
citeButtons.forEach(button => {
41+
const filename = button.getAttribute('data-filename');
42+
if (filename && !citationCache.has(filename)) {
43+
// Fetch in background, don't await
44+
fetchAndCacheCitation(filename);
45+
}
46+
});
47+
}
48+
49+
/**
50+
* Prefetch citation on hover/focus
51+
* @param {Event} e - Hover or focus event
52+
*/
53+
function prefetchOnHover(e) {
54+
const citeButton = e.target.closest('.js-cite-clipboard');
55+
if (!citeButton) return;
56+
57+
const filename = citeButton.getAttribute('data-filename');
58+
if (filename && !citationCache.has(filename)) {
59+
// Fetch in background, don't await
60+
fetchAndCacheCitation(filename);
61+
}
62+
}
63+
64+
/**
65+
* Fetch and cache citation content
66+
* @param {string} filename - Citation file URL
67+
* @returns {Promise<string|null>} - Citation content or null if failed
68+
*/
69+
async function fetchAndCacheCitation(filename) {
70+
try {
71+
const response = await fetch(filename);
72+
if (!response.ok) {
73+
throw new Error(`Failed to fetch citation: ${response.statusText}`);
74+
}
75+
const citation = await response.text();
76+
citationCache.set(filename, citation);
77+
return citation;
78+
} catch (error) {
79+
if (isDebugMode) {
80+
console.error(`Failed to fetch citation ${filename}:`, error);
81+
}
82+
return null;
83+
}
84+
}
85+
86+
/**
87+
* Handle cite button clicks - synchronous clipboard write for Safari compatibility
88+
* @param {Event} e - Click event
89+
*/
90+
function handleCiteClick(e) {
91+
// Check if clicked element or its parent is a cite button
92+
const citeButton = e.target.closest('.js-cite-clipboard');
93+
if (!citeButton) return;
94+
95+
e.preventDefault();
96+
e.stopPropagation();
97+
98+
const filename = citeButton.getAttribute('data-filename');
99+
if (!filename) {
100+
if (isDebugMode) {
101+
console.error('No filename specified for citation');
102+
}
103+
showNotification('Citation file not found', 'error');
104+
return;
105+
}
106+
107+
// Check if citation is cached
108+
const cachedCitation = citationCache.get(filename);
109+
110+
if (cachedCitation) {
111+
// Citation is cached, copy immediately (synchronous for Safari)
112+
copyToClipboardSync(cachedCitation).then(success => {
113+
if (success) {
114+
showNotification(i18n?.copied || 'Citation copied!', 'success');
115+
updateButtonText(citeButton);
116+
} else {
117+
showNotification('Failed to copy citation', 'error');
118+
}
119+
});
120+
} else {
121+
// Not cached, need to fetch first (will fail in Safari with strict mode)
122+
fetchAndCopyWithFallback(filename, citeButton);
123+
}
124+
}
125+
126+
/**
127+
* Fetch and copy with fallback (for browsers that allow async clipboard)
128+
* @param {string} filename - Citation file URL
129+
* @param {HTMLElement} button - Cite button element
130+
*/
131+
async function fetchAndCopyWithFallback(filename, button) {
132+
try {
133+
const citation = await fetchAndCacheCitation(filename);
134+
if (citation) {
135+
// Try to copy (might fail in Safari due to lost user activation)
136+
const success = await copyToClipboardSync(citation);
137+
if (success) {
138+
showNotification(i18n?.copied || 'Citation copied!', 'success');
139+
updateButtonText(button);
140+
} else {
141+
showNotification('Failed to copy citation', 'error');
142+
}
143+
} else {
144+
showNotification('Failed to load citation', 'error');
145+
}
146+
} catch (error) {
147+
if (isDebugMode) {
148+
console.error('Failed to copy citation:', error);
149+
}
150+
// If it's a NotAllowedError, suggest hovering first
151+
if (error.name === 'NotAllowedError') {
152+
showNotification('Please hover over the button first, then click', 'info');
153+
} else {
154+
showNotification('Failed to copy citation', 'error');
155+
}
156+
}
157+
}
158+
159+
/**
160+
* Update button text to show copied state
161+
* @param {HTMLElement} button - The cite button element
162+
*/
163+
function updateButtonText(button) {
164+
const copiedText = i18n?.copied || 'Copied!';
165+
166+
// Find text element to update (skip icon)
167+
const textElement = button.querySelector('span');
168+
if (!textElement) {
169+
if (isDebugMode) {
170+
console.warn('Could not find text element in cite button');
171+
}
172+
return;
173+
}
174+
175+
const originalText = textElement.textContent;
176+
textElement.textContent = copiedText;
177+
178+
// Add visual feedback
179+
button.classList.add('opacity-70');
180+
181+
// Revert after 2 seconds
182+
setTimeout(() => {
183+
textElement.textContent = originalText;
184+
button.classList.remove('opacity-70');
185+
}, 2000);
186+
}
187+
188+
// Export functions for potential reuse
189+
export { handleCiteClick, prefetchAllCitations };

0 commit comments

Comments
 (0)