mirror of
https://github.com/litruv/UE-MaterialSnippets.git
synced 2026-07-24 01:06:02 +10:00
added some QoL like url sharing/count. Fixed CSS typo, added loading, empty state
This commit is contained in:
16
index.html
16
index.html
@@ -3,7 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Unreal Engine Material Library</title>
|
<meta name="description" content="A curated library of copy-paste ready Unreal Engine material snippets. Find vignettes, scanlines, UV effects, distortions and more for your UE projects.">
|
||||||
|
<meta name="keywords" content="Unreal Engine, UE5, UE4, materials, shaders, HLSL, snippets, game development, visual effects">
|
||||||
|
<meta name="author" content="Max Litruv Boonzaayer">
|
||||||
|
<meta property="og:title" content="UE Material Snippets">
|
||||||
|
<meta property="og:description" content="A curated library of copy-paste ready Unreal Engine material snippets.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎨</text></svg>">
|
||||||
|
<title>UE Material Snippets</title>
|
||||||
<link rel="stylesheet" href="./style.css">
|
<link rel="stylesheet" href="./style.css">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
@@ -25,8 +32,14 @@
|
|||||||
<input type="text" data-search placeholder="Search materials...">
|
<input type="text" data-search placeholder="Search materials...">
|
||||||
</div>
|
</div>
|
||||||
<div class="categories-filter" data-categories></div>
|
<div class="categories-filter" data-categories></div>
|
||||||
|
<button class="clear-filters-button" data-clear-filters style="display: none;">Clear Filters</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<div class="loading-spinner" data-loading>
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Loading materials...</p>
|
||||||
|
</div>
|
||||||
|
<div class="result-count" data-result-count></div>
|
||||||
<main data-material-grid></main>
|
<main data-material-grid></main>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
@@ -38,6 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="./main.js"></script>
|
<script src="./main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
215
main.js
215
main.js
@@ -1,9 +1,114 @@
|
|||||||
|
/** Default placeholder image for missing thumbnails (base64 encoded) */
|
||||||
|
const FALLBACK_IMAGE = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE1MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE1MCIgZmlsbD0iIzJBMkEyQSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNjY2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCI+Tm8gUHJldmlldzwvdGV4dD48L3N2Zz4=';
|
||||||
|
|
||||||
|
/** Animation delay for card transitions in milliseconds */
|
||||||
|
const FILTER_TRANSITION_DELAY = 300;
|
||||||
|
|
||||||
|
/** Number of columns for masonry grid */
|
||||||
|
const GRID_COLUMN_COUNT = 4;
|
||||||
|
|
||||||
|
/** Duration for copy feedback notification in milliseconds */
|
||||||
|
const COPY_NOTIFICATION_DURATION = 1000;
|
||||||
|
|
||||||
|
/** URL separator for categories (pipe doesn't get encoded) */
|
||||||
|
const URL_CATEGORY_SEPARATOR = '|';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const grid = document.querySelector('[data-material-grid]');
|
const grid = document.querySelector('[data-material-grid]');
|
||||||
const searchInput = document.querySelector('[data-search]');
|
const searchInput = document.querySelector('[data-search]');
|
||||||
const categoriesContainer = document.querySelector('[data-categories]');
|
const categoriesContainer = document.querySelector('[data-categories]');
|
||||||
|
const loadingSpinner = document.querySelector('[data-loading]');
|
||||||
|
const clearFiltersButton = document.querySelector('[data-clear-filters]');
|
||||||
|
const resultCountEl = document.querySelector('[data-result-count]');
|
||||||
|
|
||||||
let materialLibrary = [];
|
let materialLibrary = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows or hides the loading spinner
|
||||||
|
* @param {boolean} show - Whether to show the spinner
|
||||||
|
*/
|
||||||
|
function setLoading(show) {
|
||||||
|
loadingSpinner.style.display = show ? 'flex' : 'none';
|
||||||
|
grid.style.display = show ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the result count display
|
||||||
|
* @param {number} count - Number of materials shown
|
||||||
|
* @param {number} total - Total number of materials
|
||||||
|
*/
|
||||||
|
function updateResultCount(count, total) {
|
||||||
|
if (count === total) {
|
||||||
|
resultCountEl.innerHTML = `Showing <span>${count}</span> snippets`;
|
||||||
|
} else {
|
||||||
|
resultCountEl.innerHTML = `Showing <span>${count}</span> of <span>${total}</span> snippets`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads URL parameters and returns filter state
|
||||||
|
* @returns {{search: string, categories: string[]}} Filter state from URL
|
||||||
|
*/
|
||||||
|
function getFiltersFromURL() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const search = params.get('q') || '';
|
||||||
|
const categories = params.get('categories') ? params.get('categories').split(URL_CATEGORY_SEPARATOR) : [];
|
||||||
|
return { search, categories };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the URL with current filter state
|
||||||
|
* @param {string} search - Current search term
|
||||||
|
* @param {string[]} categories - Currently selected categories
|
||||||
|
*/
|
||||||
|
function updateURL(search, categories) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (search) params.set('q', search);
|
||||||
|
if (categories.length > 0) params.set('categories', categories.join(URL_CATEGORY_SEPARATOR));
|
||||||
|
|
||||||
|
const newURL = params.toString()
|
||||||
|
? `${window.location.pathname}?${params.toString()}`
|
||||||
|
: window.location.pathname;
|
||||||
|
|
||||||
|
window.history.replaceState({}, '', newURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visibility of the clear filters button
|
||||||
|
*/
|
||||||
|
function updateClearFiltersVisibility() {
|
||||||
|
const hasActiveFilters = document.querySelectorAll('.category-button.active').length > 0;
|
||||||
|
const hasSearchText = searchInput.value.trim().length > 0;
|
||||||
|
clearFiltersButton.style.display = (hasActiveFilters || hasSearchText) ? 'inline-block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all active filters and search text
|
||||||
|
*/
|
||||||
|
function clearAllFilters() {
|
||||||
|
document.querySelectorAll('.category-button.active').forEach(btn => btn.classList.remove('active'));
|
||||||
|
searchInput.value = '';
|
||||||
|
updateClearFiltersVisibility();
|
||||||
|
filterMaterials();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a copy notification at the given position
|
||||||
|
* @param {DOMRect} rect - Bounding rectangle for positioning
|
||||||
|
*/
|
||||||
|
function showCopyNotification(rect) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'copy-notification';
|
||||||
|
notification.textContent = 'Copied!';
|
||||||
|
notification.style.left = `${rect.left + rect.width / 2}px`;
|
||||||
|
notification.style.top = `${rect.top}px`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
setTimeout(() => notification.remove(), COPY_NOTIFICATION_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('materials.json');
|
const response = await fetch('materials.json');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -21,25 +126,47 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
categoryButtons.forEach(button => {
|
categoryButtons.forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
button.classList.toggle('active');
|
button.classList.toggle('active');
|
||||||
|
updateClearFiltersVisibility();
|
||||||
filterMaterials();
|
filterMaterials();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearFiltersButton.addEventListener('click', clearAllFilters);
|
||||||
|
|
||||||
|
const urlFilters = getFiltersFromURL();
|
||||||
|
if (urlFilters.search) {
|
||||||
|
searchInput.value = urlFilters.search;
|
||||||
|
}
|
||||||
|
if (urlFilters.categories.length > 0) {
|
||||||
|
urlFilters.categories.forEach(cat => {
|
||||||
|
const btn = categoriesContainer.querySelector(`[data-category="${cat}"]`);
|
||||||
|
if (btn) btn.classList.add('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateClearFiltersVisibility();
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading materials:', error);
|
console.error('Error loading materials:', error);
|
||||||
|
setLoading(false);
|
||||||
grid.innerHTML = '<p class="error-message">Error loading materials library.</p>';
|
grid.innerHTML = '<p class="error-message">Error loading materials library.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the material cards to the grid
|
||||||
|
* @param {Array} materials - Array of material objects to render
|
||||||
|
*/
|
||||||
function renderMaterials(materials) {
|
function renderMaterials(materials) {
|
||||||
grid.innerHTML = materials.map(material => `
|
grid.innerHTML = materials.map(material => `
|
||||||
<div class="material-card show" data-material="${material.name.toLowerCase().replace(/\s+/g, '-')}">
|
<div class="material-card show" data-material="${material.name.toLowerCase().replace(/\s+/g, '-')}" tabindex="0" role="button" aria-label="Copy ${material.name} snippet">
|
||||||
<img class="material-preview" src="${material.preview}" alt="${material.name}">
|
<img class="material-preview" src="${material.preview}" alt="Preview of ${material.name} material effect" onerror="this.src='${FALLBACK_IMAGE}'">
|
||||||
<div class="material-info">
|
<div class="material-info">
|
||||||
<h3>${material.name}</h3>
|
<h3>${material.name}</h3>
|
||||||
<p class="author">${material.author}</p>
|
<p class="author">${material.author}</p>
|
||||||
<p>${material.description}</p>
|
<p>${material.description}</p>
|
||||||
<div class="material-tags">
|
<div class="material-tags">
|
||||||
${material.categories.map(cat => `<span class="tag" data-tag="${cat}">${cat}</span>`).join(' ')}
|
${material.categories.map(cat => `<span class="tag" data-tag="${cat}" tabindex="0" role="button" aria-label="Filter by ${cat}">${cat}</span>`).join(' ')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,21 +174,35 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
const tags = document.querySelectorAll('.tag');
|
const tags = document.querySelectorAll('.tag');
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
tag.addEventListener('click', (e) => {
|
/**
|
||||||
|
* Handles tag click to activate category filter
|
||||||
|
* @param {Event} e - Click or keydown event
|
||||||
|
*/
|
||||||
|
const handleTagActivation = (e) => {
|
||||||
|
if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const category = tag.dataset.tag;
|
const category = tag.dataset.tag;
|
||||||
const categoryButton = [...document.querySelectorAll('.category-button')]
|
const categoryButton = [...document.querySelectorAll('.category-button')]
|
||||||
.find(button => button.dataset.category === category);
|
.find(button => button.dataset.category === category);
|
||||||
if (categoryButton) {
|
if (categoryButton) {
|
||||||
categoryButton.classList.add('active');
|
categoryButton.classList.add('active');
|
||||||
|
updateClearFiltersVisibility();
|
||||||
filterMaterials();
|
filterMaterials();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
tag.addEventListener('click', handleTagActivation);
|
||||||
|
tag.addEventListener('keydown', handleTagActivation);
|
||||||
});
|
});
|
||||||
|
|
||||||
reSortMasonry();
|
reSortMasonry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a snippet file from the given path
|
||||||
|
* @param {string} snippetPath - Path to the snippet file
|
||||||
|
* @returns {Promise<string>} The snippet content
|
||||||
|
*/
|
||||||
async function loadSnippet(snippetPath) {
|
async function loadSnippet(snippetPath) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(snippetPath);
|
const response = await fetch(snippetPath);
|
||||||
@@ -72,11 +213,16 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters materials based on search term and selected categories
|
||||||
|
*/
|
||||||
async function filterMaterials() {
|
async function filterMaterials() {
|
||||||
const searchTerm = searchInput.value.toLowerCase();
|
const searchTerm = searchInput.value.toLowerCase();
|
||||||
const selectedCategories = [...document.querySelectorAll('.category-button.active')]
|
const selectedCategories = [...document.querySelectorAll('.category-button.active')]
|
||||||
.map(button => button.dataset.category);
|
.map(button => button.dataset.category);
|
||||||
|
|
||||||
|
updateURL(searchInput.value, selectedCategories);
|
||||||
|
|
||||||
const filtered = materialLibrary.filter(material => {
|
const filtered = materialLibrary.filter(material => {
|
||||||
const matchesSearch = material.name.toLowerCase().includes(searchTerm) ||
|
const matchesSearch = material.name.toLowerCase().includes(searchTerm) ||
|
||||||
material.description.toLowerCase().includes(searchTerm) ||
|
material.description.toLowerCase().includes(searchTerm) ||
|
||||||
@@ -85,6 +231,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
return matchesSearch && matchesCategory;
|
return matchesSearch && matchesCategory;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateResultCount(filtered.length, materialLibrary.length);
|
||||||
|
|
||||||
const allCards = document.querySelectorAll('.material-card');
|
const allCards = document.querySelectorAll('.material-card');
|
||||||
allCards.forEach(card => {
|
allCards.forEach(card => {
|
||||||
card.classList.add('hide');
|
card.classList.add('hide');
|
||||||
@@ -92,9 +240,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
renderMaterials(filtered);
|
renderMaterials(filtered);
|
||||||
}, 300);
|
}, FILTER_TRANSITION_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-sorts the masonry grid layout after content changes
|
||||||
|
*/
|
||||||
function reSortMasonry() {
|
function reSortMasonry() {
|
||||||
const masonryItems = document.querySelectorAll('.material-card');
|
const masonryItems = document.querySelectorAll('.material-card');
|
||||||
let transitionCount = masonryItems.length;
|
let transitionCount = masonryItems.length;
|
||||||
@@ -105,31 +256,39 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
transitionCount--;
|
transitionCount--;
|
||||||
if (transitionCount === 0) {
|
if (transitionCount === 0) {
|
||||||
grid.style.columnCount = 0;
|
grid.style.columnCount = 0;
|
||||||
grid.style.columnCount = 4;
|
grid.style.columnCount = GRID_COLUMN_COUNT;
|
||||||
}
|
}
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles copying a material snippet to clipboard
|
||||||
|
* @param {HTMLElement} card - The card element that was activated
|
||||||
|
*/
|
||||||
|
async function handleCardClick(card) {
|
||||||
|
const materialName = card.dataset.material;
|
||||||
|
const material = materialLibrary.find(m =>
|
||||||
|
m.name.toLowerCase().replace(/\s+/g, '-') === materialName
|
||||||
|
);
|
||||||
|
|
||||||
|
const snippet = await loadSnippet(material.snippet);
|
||||||
|
navigator.clipboard.writeText(snippet);
|
||||||
|
showCopyNotification(card.getBoundingClientRect());
|
||||||
|
}
|
||||||
|
|
||||||
grid.addEventListener('click', async (e) => {
|
grid.addEventListener('click', async (e) => {
|
||||||
const card = e.target.closest('.material-card');
|
const card = e.target.closest('.material-card');
|
||||||
if (card && !e.target.classList.contains('tag')) {
|
if (card && !e.target.classList.contains('tag')) {
|
||||||
const materialName = card.dataset.material;
|
await handleCardClick(card);
|
||||||
const material = materialLibrary.find(m =>
|
}
|
||||||
m.name.toLowerCase().replace(/\s+/g, '-') === materialName
|
});
|
||||||
);
|
|
||||||
|
grid.addEventListener('keydown', async (e) => {
|
||||||
const snippet = await loadSnippet(material.snippet);
|
const card = e.target.closest('.material-card');
|
||||||
navigator.clipboard.writeText(snippet);
|
if (card && (e.key === 'Enter' || e.key === ' ') && !e.target.classList.contains('tag')) {
|
||||||
|
e.preventDefault();
|
||||||
const notification = document.createElement('div');
|
await handleCardClick(card);
|
||||||
notification.className = 'copy-notification';
|
|
||||||
notification.textContent = 'Copied!';
|
|
||||||
notification.style.left = `${e.clientX}px`;
|
|
||||||
notification.style.top = `${e.clientY - 30}px`;
|
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
setTimeout(() => notification.remove(), 1000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,7 +296,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
searchInput.addEventListener('input', filterMaterials);
|
searchInput.addEventListener('input', () => {
|
||||||
|
updateClearFiltersVisibility();
|
||||||
|
filterMaterials();
|
||||||
|
});
|
||||||
|
|
||||||
|
updateResultCount(materialLibrary.length, materialLibrary.length);
|
||||||
renderMaterials(materialLibrary);
|
renderMaterials(materialLibrary);
|
||||||
|
|
||||||
|
if (getFiltersFromURL().search || getFiltersFromURL().categories.length > 0) {
|
||||||
|
filterMaterials();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
"materials": [
|
"materials": [
|
||||||
{
|
{
|
||||||
"author": "Litruv",
|
"author": "Litruv",
|
||||||
"name": "Vinette",
|
"name": "Vignette",
|
||||||
"categories": [
|
"categories": [
|
||||||
"photo",
|
"photo",
|
||||||
"fx",
|
"fx",
|
||||||
"vintage"
|
"vintage"
|
||||||
],
|
],
|
||||||
"preview": "./thumbnails/vinette.png",
|
"preview": "./thumbnails/vignette.png",
|
||||||
"snippet": "snippets/vinette.txt",
|
"snippet": "snippets/vignette.txt",
|
||||||
"description": "Adds a subtle darkened border around the edges of the image for a vintage effect."
|
"description": "Adds a subtle darkened border around the edges of the image for a vintage effect."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"name": "Screen Ratio Corrected UV's",
|
"name": "Screen Ratio Corrected UV's",
|
||||||
"categories": [
|
"categories": [
|
||||||
"screen",
|
"screen",
|
||||||
"utillity",
|
"utility",
|
||||||
"uv"
|
"uv"
|
||||||
],
|
],
|
||||||
"preview": "./thumbnails/screen_ratio_corrected_uvs.png",
|
"preview": "./thumbnails/screen_ratio_corrected_uvs.png",
|
||||||
|
|||||||
134
style.css
134
style.css
@@ -38,6 +38,7 @@ body {
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -106,7 +107,7 @@ h1 {
|
|||||||
|
|
||||||
.category-button:hover {
|
.category-button:hover {
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
color: var (--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-button.active:hover {
|
.category-button.active:hover {
|
||||||
@@ -117,7 +118,18 @@ h1 {
|
|||||||
column-count: 4;
|
column-count: 4;
|
||||||
column-gap: 20px;
|
column-gap: 20px;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-material-grid]:empty::after {
|
||||||
|
content: 'No snippets found';
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 60px 20px;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-card {
|
.material-card {
|
||||||
@@ -129,10 +141,16 @@ h1 {
|
|||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
transition: transform 0.2s, opacity var(--transition-duration);
|
transition: transform 0.2s, opacity var(--transition-duration), border-color 0.2s, box-shadow 0.2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
animation: fadeIn var(--transition-duration) ease-in;
|
animation: fadeIn var(--transition-duration) ease-in;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-card:focus {
|
||||||
|
outline: 2px solid var(--primary-color);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-card.hide {
|
.material-card.hide {
|
||||||
@@ -205,6 +223,11 @@ h1 {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag:focus {
|
||||||
|
outline: 2px solid var(--primary-color);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.copy-notification {
|
.copy-notification {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
@@ -321,3 +344,110 @@ footer {
|
|||||||
.add-snippet-button:hover {
|
.add-snippet-button:hover {
|
||||||
background-color: #218838;
|
background-color: #218838;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Loading Spinner */
|
||||||
|
.loading-spinner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid var(--border-color);
|
||||||
|
border-top-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clear Filters Button */
|
||||||
|
.clear-filters-button {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
padding: var(--padding);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: var(--font-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-filters-button:hover {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Result Count */
|
||||||
|
.result-count {
|
||||||
|
text-align: right;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-size);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-count span {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsiveness */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
[data-material-grid] {
|
||||||
|
column-count: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
[data-material-grid] {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
[data-material-grid] {
|
||||||
|
column-count: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-filter {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-snippet-button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 123 KiB |
Reference in New Issue
Block a user