mirror of
https://github.com/tiennm99/HugoBlox-kit.git
synced 2026-10-15 12:13:46 +00:00
1 parent
9c48341ee8
commit
0c41b518dc
16 files changed
+1376
-11
No files matched your search
@@ -0,0 +1,70 @@
|
||||
# Hugo Blox Tailwind CSS v4 Structure
|
||||
|
||||
## Overview
|
||||
|
||||
The CSS architecture is modularized for maintainability and organization. The entry point is `main.css` which orchestrates all imports.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
css/
|
||||
├── main.css # Main entry point
|
||||
├── color-utilities.css # HB Theme Engine
|
||||
├── config/ # Tailwind configuration
|
||||
│ ├── tailwind.css # Tailwind v4 directives (@source, @custom-variant)
|
||||
│ ├── theme.css # HB Theme variables (@theme block)
|
||||
│ └── safelist.css # Dynamic classes safelist
|
||||
├── framework/ # Hugo Blox framework styles
|
||||
│ ├── base.css # Base styles and typography
|
||||
│ └── components.css # Framework components
|
||||
├── components/ # Hugo Blox module components
|
||||
│ └── all.css # Imports all component files
|
||||
├── blox/ # Block-specific styles
|
||||
│ └── all.css # Imports all block styles
|
||||
└── views/ # Listing view styles
|
||||
└── all.css # Imports all listing view styles
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
### main.css
|
||||
The orchestrator that imports everything in the correct order:
|
||||
1. Tailwind base
|
||||
2. Configuration
|
||||
3. Base styles
|
||||
4. Hugo Blox theme engine
|
||||
5. Components
|
||||
6. Safelist
|
||||
|
||||
### config/tailwind.css
|
||||
Contains Tailwind v4-specific directives:
|
||||
- `@source` - Where to scan for classes
|
||||
- `@custom-variant` - Custom variants (dark, hover)
|
||||
- `@plugin` - Plugin imports
|
||||
|
||||
### config/theme.css
|
||||
Defines the theme using `@theme` block:
|
||||
- Color palettes (primary, secondary, neutral)
|
||||
- Font families and sizes
|
||||
- Custom properties
|
||||
|
||||
### color-utilities.css
|
||||
Hugo Blox theme engine with color utility classes:
|
||||
- All color shades (50-950)
|
||||
- All variants (hover, dark, focus)
|
||||
- Gradient utilities
|
||||
|
||||
## Tailwind v4 Notes
|
||||
|
||||
- `@source`, `@theme`, `@plugin` are new v4 directives
|
||||
- Linters may show warnings for these - they can be ignored
|
||||
- Hugo's `css.TailwindCSS` function processes this automatically
|
||||
- No PostCSS configuration needed anymore
|
||||
|
||||
## Maintenance
|
||||
|
||||
When adding new styles:
|
||||
1. Framework classes → `framework/components.css`
|
||||
2. Theme variables → `config/theme.css`
|
||||
3. Component styles → `components/[file].css`
|
||||
4. Dynamic classes → Add to `config/safelist.css`
|
||||
@@ -0,0 +1 @@
|
||||
@import "attachments.css";
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Citation clipboard handler for Hugo Blox Builder
|
||||
* Copies BibTeX citation to clipboard when cite button is clicked
|
||||
*/
|
||||
|
||||
import { hugoEnvironment, i18n } from '@params';
|
||||
import { showNotification } from './hb-notifier.js';
|
||||
import { copyToClipboardSync, ClipboardCache } from './hb-clipboard.js';
|
||||
|
||||
// Debug mode based on environment
|
||||
const isDebugMode = hugoEnvironment === 'development';
|
||||
|
||||
// Cache for citation content
|
||||
const citationCache = new ClipboardCache();
|
||||
|
||||
// Initialize citation handlers when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeCitation);
|
||||
} else {
|
||||
initializeCitation();
|
||||
}
|
||||
|
||||
function initializeCitation() {
|
||||
// Handle citation button clicks using event delegation
|
||||
document.addEventListener('click', handleCiteClick);
|
||||
|
||||
// Prefetch citations on hover/focus for better UX and Safari compatibility
|
||||
document.addEventListener('mouseover', prefetchOnHover);
|
||||
document.addEventListener('focusin', prefetchOnHover);
|
||||
|
||||
// Prefetch all citations on page load
|
||||
prefetchAllCitations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch all citations on page load
|
||||
*/
|
||||
function prefetchAllCitations() {
|
||||
const citeButtons = document.querySelectorAll('.js-cite-clipboard[data-filename]');
|
||||
citeButtons.forEach(button => {
|
||||
const filename = button.getAttribute('data-filename');
|
||||
if (filename && !citationCache.has(filename)) {
|
||||
// Fetch in background, don't await
|
||||
fetchAndCacheCitation(filename);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch citation on hover/focus
|
||||
* @param {Event} e - Hover or focus event
|
||||
*/
|
||||
function prefetchOnHover(e) {
|
||||
const citeButton = e.target.closest('.js-cite-clipboard');
|
||||
if (!citeButton) return;
|
||||
|
||||
const filename = citeButton.getAttribute('data-filename');
|
||||
if (filename && !citationCache.has(filename)) {
|
||||
// Fetch in background, don't await
|
||||
fetchAndCacheCitation(filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and cache citation content
|
||||
* @param {string} filename - Citation file URL
|
||||
* @returns {Promise<string|null>} - Citation content or null if failed
|
||||
*/
|
||||
async function fetchAndCacheCitation(filename) {
|
||||
try {
|
||||
const response = await fetch(filename);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch citation: ${response.statusText}`);
|
||||
}
|
||||
const citation = await response.text();
|
||||
citationCache.set(filename, citation);
|
||||
return citation;
|
||||
} catch (error) {
|
||||
if (isDebugMode) {
|
||||
console.error(`Failed to fetch citation ${filename}:`, error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cite button clicks - synchronous clipboard write for Safari compatibility
|
||||
* @param {Event} e - Click event
|
||||
*/
|
||||
function handleCiteClick(e) {
|
||||
// Check if clicked element or its parent is a cite button
|
||||
const citeButton = e.target.closest('.js-cite-clipboard');
|
||||
if (!citeButton) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const filename = citeButton.getAttribute('data-filename');
|
||||
if (!filename) {
|
||||
if (isDebugMode) {
|
||||
console.error('No filename specified for citation');
|
||||
}
|
||||
showNotification('Citation file not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if citation is cached
|
||||
const cachedCitation = citationCache.get(filename);
|
||||
|
||||
if (cachedCitation) {
|
||||
// Citation is cached, copy immediately (synchronous for Safari)
|
||||
copyToClipboardSync(cachedCitation).then(success => {
|
||||
if (success) {
|
||||
showNotification(i18n?.copied || 'Citation copied!', 'success');
|
||||
updateButtonText(citeButton);
|
||||
} else {
|
||||
showNotification('Failed to copy citation', 'error');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Not cached, need to fetch first (will fail in Safari with strict mode)
|
||||
fetchAndCopyWithFallback(filename, citeButton);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and copy with fallback (for browsers that allow async clipboard)
|
||||
* @param {string} filename - Citation file URL
|
||||
* @param {HTMLElement} button - Cite button element
|
||||
*/
|
||||
async function fetchAndCopyWithFallback(filename, button) {
|
||||
try {
|
||||
const citation = await fetchAndCacheCitation(filename);
|
||||
if (citation) {
|
||||
// Try to copy (might fail in Safari due to lost user activation)
|
||||
const success = await copyToClipboardSync(citation);
|
||||
if (success) {
|
||||
showNotification(i18n?.copied || 'Citation copied!', 'success');
|
||||
updateButtonText(button);
|
||||
} else {
|
||||
showNotification('Failed to copy citation', 'error');
|
||||
}
|
||||
} else {
|
||||
showNotification('Failed to load citation', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isDebugMode) {
|
||||
console.error('Failed to copy citation:', error);
|
||||
}
|
||||
// If it's a NotAllowedError, suggest hovering first
|
||||
if (error.name === 'NotAllowedError') {
|
||||
showNotification('Please hover over the button first, then click', 'info');
|
||||
} else {
|
||||
showNotification('Failed to copy citation', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update button text to show copied state
|
||||
* @param {HTMLElement} button - The cite button element
|
||||
*/
|
||||
function updateButtonText(button) {
|
||||
const copiedText = i18n?.copied || 'Copied!';
|
||||
|
||||
// Find text element to update (skip icon)
|
||||
const textElement = button.querySelector('span');
|
||||
if (!textElement) {
|
||||
if (isDebugMode) {
|
||||
console.warn('Could not find text element in cite button');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText = textElement.textContent;
|
||||
textElement.textContent = copiedText;
|
||||
|
||||
// Add visual feedback
|
||||
button.classList.add('opacity-70');
|
||||
|
||||
// Revert after 2 seconds
|
||||
setTimeout(() => {
|
||||
textElement.textContent = originalText;
|
||||
button.classList.remove('opacity-70');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Export functions for potential reuse
|
||||
export { handleCiteClick, prefetchAllCitations };
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Clipboard Utilities for Hugo Blox Builder
|
||||
* Provides cross-browser clipboard functionality
|
||||
*/
|
||||
|
||||
import { hugoEnvironment } from '@params';
|
||||
|
||||
// Debug mode based on environment
|
||||
const isDebugMode = hugoEnvironment === 'development';
|
||||
|
||||
/**
|
||||
* Copy text to clipboard with multiple fallback methods
|
||||
* @param {string} text - Text to copy
|
||||
* @returns {Promise<boolean>} - Whether the copy was successful
|
||||
*/
|
||||
export async function copyToClipboard(text) {
|
||||
// Method 1: Modern Clipboard API
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (isDebugMode) {
|
||||
console.log('Copied using Clipboard API');
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (isDebugMode) {
|
||||
console.warn('Clipboard API failed:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: execCommand fallback
|
||||
return copyUsingExecCommand(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to clipboard synchronously (Safari-compatible)
|
||||
* @param {string} text - Text to copy
|
||||
* @returns {Promise<boolean>} - Whether the copy was successful
|
||||
*/
|
||||
export function copyToClipboardSync(text) {
|
||||
return new Promise((resolve) => {
|
||||
// Try modern clipboard API first
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text)
|
||||
.then(() => {
|
||||
if (isDebugMode) {
|
||||
console.log('Copied using Clipboard API (sync)');
|
||||
}
|
||||
resolve(true);
|
||||
})
|
||||
.catch(err => {
|
||||
if (isDebugMode) {
|
||||
console.warn('Clipboard API failed:', err);
|
||||
}
|
||||
// Fallback to execCommand
|
||||
resolve(copyUsingExecCommand(text));
|
||||
});
|
||||
} else {
|
||||
// Use execCommand directly
|
||||
resolve(copyUsingExecCommand(text));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy copy method using execCommand
|
||||
* @param {string} text - Text to copy
|
||||
* @returns {boolean} - Success status
|
||||
*/
|
||||
export function copyUsingExecCommand(text) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
|
||||
// Make it invisible but copyable
|
||||
Object.assign(textarea.style, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '2em',
|
||||
height: '2em',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
boxShadow: 'none',
|
||||
background: 'transparent',
|
||||
fontSize: '16px', // Prevent zoom on iOS
|
||||
});
|
||||
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
// Select the text
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
// For iOS
|
||||
if (navigator.userAgent.match(/ipad|iphone/i)) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(textarea);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
textarea.setSelectionRange(0, 999999);
|
||||
}
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
success = document.execCommand('copy');
|
||||
if (isDebugMode) {
|
||||
console.log(`execCommand copy ${success ? 'succeeded' : 'failed'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isDebugMode) {
|
||||
console.error('execCommand failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
document.body.removeChild(textarea);
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if clipboard API is available and working
|
||||
* @returns {boolean} - Whether clipboard API is available
|
||||
*/
|
||||
export function isClipboardAPIAvailable() {
|
||||
return !!(navigator.clipboard && window.isSecureContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a prefetch cache for clipboard content
|
||||
* Useful for Safari compatibility where content must be copied synchronously
|
||||
*/
|
||||
export class ClipboardCache {
|
||||
constructor() {
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add content to cache
|
||||
* @param {string} key - Cache key
|
||||
* @param {string} content - Content to cache
|
||||
*/
|
||||
set(key, content) {
|
||||
this.cache.set(key, content);
|
||||
if (isDebugMode) {
|
||||
console.log(`Cached clipboard content for: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content from cache
|
||||
* @param {string} key - Cache key
|
||||
* @returns {string|null} - Cached content or null
|
||||
*/
|
||||
get(key) {
|
||||
return this.cache.get(key) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content is cached
|
||||
* @param {string} key - Cache key
|
||||
* @returns {boolean} - Whether content is cached
|
||||
*/
|
||||
has(key) {
|
||||
return this.cache.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache
|
||||
*/
|
||||
clear() {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache size
|
||||
* @returns {number} - Number of cached items
|
||||
*/
|
||||
get size() {
|
||||
return this.cache.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Export default clipboard copy function
|
||||
export default copyToClipboard;
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Notification/Toast System for Hugo Blox Builder
|
||||
* Provides reusable notification functionality
|
||||
*/
|
||||
|
||||
import { hugoEnvironment } from '@params';
|
||||
|
||||
// Debug mode based on environment
|
||||
const isDebugMode = hugoEnvironment === 'development';
|
||||
|
||||
/**
|
||||
* Show a notification message using Tailwind styles
|
||||
* @param {string} message - The message to display
|
||||
* @param {string} type - The type of notification ('success', 'error', 'info', 'warning')
|
||||
* @param {number} duration - Duration in milliseconds (default 3000)
|
||||
* @returns {HTMLElement} - The notification element
|
||||
*/
|
||||
export function showNotification(message, type = 'success', duration = 3000) {
|
||||
// Get or create the notification container
|
||||
const container = getOrCreateContainer();
|
||||
|
||||
// Remove any existing notifications with same message
|
||||
const existingNotification = container.querySelector('.hb-notification');
|
||||
if (existingNotification && existingNotification.textContent.includes(message)) {
|
||||
existingNotification.remove();
|
||||
}
|
||||
|
||||
// Create notification element
|
||||
const notification = createNotificationElement(message, type);
|
||||
|
||||
// Add to container
|
||||
container.appendChild(notification);
|
||||
|
||||
// Handle close button
|
||||
const closeBtn = notification.querySelector('.hb-notification-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => {
|
||||
removeNotification(notification);
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-remove after duration
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
removeNotification(notification);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the notification container
|
||||
* @returns {HTMLElement} - The container element
|
||||
*/
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById('hb-notification-container');
|
||||
|
||||
if (!container) {
|
||||
if (isDebugMode) {
|
||||
console.warn('Notification container not found, creating fallback');
|
||||
}
|
||||
container = document.createElement('div');
|
||||
container.id = 'hb-notification-container';
|
||||
container.className = 'fixed top-20 right-4 z-[9999] pointer-events-none';
|
||||
container.setAttribute('aria-live', 'polite');
|
||||
container.setAttribute('aria-atomic', 'true');
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a notification element
|
||||
* @param {string} message - The message to display
|
||||
* @param {string} type - The type of notification
|
||||
* @returns {HTMLElement} - The notification element
|
||||
*/
|
||||
function createNotificationElement(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.setAttribute('role', 'alert');
|
||||
|
||||
// Define colors for different types
|
||||
const colors = {
|
||||
success: { bg: '#10b981', bgClass: 'bg-green-500' },
|
||||
error: { bg: '#ef4444', bgClass: 'bg-red-500' },
|
||||
info: { bg: '#3b82f6', bgClass: 'bg-blue-500' },
|
||||
warning: { bg: '#f59e0b', bgClass: 'bg-amber-500' }
|
||||
};
|
||||
|
||||
const color = colors[type] || colors.info;
|
||||
|
||||
// Set classes
|
||||
notification.className = `hb-notification pointer-events-auto flex items-center gap-2 px-4 py-3 text-white rounded-lg shadow-lg ${color.bgClass} animate-slide-in`;
|
||||
|
||||
// Add inline styles as fallback
|
||||
notification.style.cssText = `
|
||||
background-color: ${color.bg};
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 0.5rem;
|
||||
pointer-events: auto;
|
||||
`;
|
||||
|
||||
// Add icon based on type
|
||||
const icons = {
|
||||
success: '<svg class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>',
|
||||
error: '<svg class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>',
|
||||
info: '<svg class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>',
|
||||
warning: '<svg class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>'
|
||||
};
|
||||
|
||||
const icon = icons[type] || icons.info;
|
||||
|
||||
notification.innerHTML = `
|
||||
${icon}
|
||||
<span class="text-sm font-medium">${message}</span>
|
||||
<button class="hb-notification-close ml-2 text-white/80 hover:text-white transition-colors" aria-label="Close">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Ensure animation styles exist
|
||||
ensureAnimationStyles();
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove notification with animation
|
||||
* @param {HTMLElement} notification - The notification element to remove
|
||||
*/
|
||||
export function removeNotification(notification) {
|
||||
if (!notification || !notification.parentNode) return;
|
||||
|
||||
notification.classList.remove('animate-slide-in');
|
||||
notification.classList.add('animate-slide-out');
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure animation styles are in the document
|
||||
*/
|
||||
function ensureAnimationStyles() {
|
||||
if (!document.querySelector('#hb-notification-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'hb-notification-styles';
|
||||
style.textContent = `
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slide-out {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.animate-slide-in {
|
||||
animation: slide-in 0.3s ease-out;
|
||||
}
|
||||
.animate-slide-out {
|
||||
animation: slide-out 0.3s ease-out;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
// Export default for convenience
|
||||
export default showNotification;
|
||||
@@ -0,0 +1,4 @@
|
||||
{{/* Notification Container for toasts/alerts */}}
|
||||
{{/* This container is placed at the page level to ensure proper z-index and visibility */}}
|
||||
{{/* Used by hb-notifier.js module for showing toast notifications */}}
|
||||
<div id="hb-notification-container" class="fixed top-20 right-4 z-[9999] pointer-events-none" aria-live="polite" aria-atomic="true"></div>
|
||||
@@ -32,10 +32,10 @@
|
||||
{{ end }}
|
||||
{{ $resource := $page.Resources.GetMatch "cite.bib" }}
|
||||
{{ with $resource }}
|
||||
<a class="hb-attachment-link {{ if $is_list }}hb-attachment-link-small{{else}}hb-attachment-link-large{{end}}" href="{{ .RelPermalink }}" target="_blank" data-filename="{{ .RelPermalink }}">
|
||||
<button class="hb-attachment-link {{ if $is_list }}hb-attachment-link-small{{else}}hb-attachment-link-large{{end}} js-cite-clipboard cursor-pointer" type="button" data-filename="{{ .RelPermalink }}">
|
||||
{{ partial "functions/get_icon" (dict "name" "document-duplicate" "attributes" "style=\"height: 1em\" class='inline-block'") }}
|
||||
{{ i18n "btn_cite" }}
|
||||
</a>
|
||||
<span>{{ i18n "btn_cite" }}</span>
|
||||
</button>
|
||||
{{ end }}
|
||||
{{ with $page.Params.url_code }}
|
||||
{{ $resource := $page.Resources.GetMatch . }}
|
||||
|
||||
@@ -267,7 +267,10 @@
|
||||
{{- $js_lang := resources.Get "js/hb-i18n.js" | resources.Minify -}}
|
||||
{{- $js_nav := resources.Get "js/hb-nav.js" | resources.Minify -}}
|
||||
{{- $js_sidebar := resources.Get "js/hb-sidebar.js" | resources.Minify -}}
|
||||
{{ $js_bundle := slice $js_bundle_head $js_academic $js_theme $js_lang $js_nav $js_sidebar | resources.Concat (printf "js/hugo-blox-%s.min.js" .Lang) }}
|
||||
{{- $js_notifier := resources.Get "js/hb-notifier.js" | js.Build (dict "targetPath" (printf "js/hb-notifier-%s.js" .Lang ) "params" $js_params) | resources.Minify -}}
|
||||
{{- $js_clipboard := resources.Get "js/hb-clipboard.js" | js.Build (dict "targetPath" (printf "js/hb-clipboard-%s.js" .Lang ) "params" $js_params) | resources.Minify -}}
|
||||
{{- $js_citation := resources.Get "js/hb-citation.js" | js.Build (dict "targetPath" (printf "js/hb-citation-%s.js" .Lang ) "params" $js_params) | resources.Minify -}}
|
||||
{{ $js_bundle := slice $js_bundle_head $js_academic $js_theme $js_lang $js_nav $js_sidebar $js_notifier $js_clipboard $js_citation | resources.Concat (printf "js/hugo-blox-%s.min.js" .Lang) }}
|
||||
{{- if hugo.IsProduction -}}
|
||||
{{ $js_bundle = $js_bundle | fingerprint "sha256" }}
|
||||
{{- end -}}
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
{{ partial "site_footer" . }}
|
||||
</div>
|
||||
|
||||
{{/* Notification Container for toasts/alerts */}}
|
||||
{{ partial "notification-container" . }}
|
||||
|
||||
{{/* EXTENSIBILITY HOOK: BODY-END */}}
|
||||
{{ partial "functions/get_hook" (dict "hook" "body-end" "context" .) }}
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user