mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-02-09 21:13:09 +00:00
ui: refresh communities page
This commit is contained in:
parent
f5b53ec092
commit
11621339d9
2 changed files with 425 additions and 120 deletions
|
|
@ -330,6 +330,150 @@ const publicDemoSite = isEnabled((globalThis as any).process?.env?.PUBLIC_DEMO_S
|
||||||
:where(button):active {
|
:where(button):active {
|
||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--ui-container-max: 1200px;
|
||||||
|
--ui-page-pad-y: 2rem;
|
||||||
|
--ui-glass-bg: rgba(24, 24, 27, 0.65);
|
||||||
|
--ui-glass-border: rgba(255, 255, 255, 0.08);
|
||||||
|
--ui-soft-highlight: rgba(129, 140, 248, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-container {
|
||||||
|
max-width: var(--ui-container-max);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-page {
|
||||||
|
padding: var(--ui-page-pad-y) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-page-title h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-subtitle {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card-glass {
|
||||||
|
background: var(--ui-glass-bg);
|
||||||
|
border-color: var(--ui-glass-border);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-primary {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-on-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-primary:hover {
|
||||||
|
background: var(--color-primary-hover);
|
||||||
|
color: var(--color-on-primary);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 10px 30px rgba(99, 102, 241, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn-secondary:hover {
|
||||||
|
border-color: var(--color-border-hover);
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(input.ui-input, select.ui-select) {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-kpis {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-kpi {
|
||||||
|
padding: 0.75rem 0.875rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background: rgba(0, 0, 0, 0.18);
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-kpi-value {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-kpi-label {
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ui-container {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -5,67 +5,179 @@ import { API_BASE as apiBase } from '../lib/api';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Communities">
|
<Layout title="Communities">
|
||||||
<section class="communities">
|
<section class="ui-page">
|
||||||
<div class="header-row">
|
<div class="ui-container">
|
||||||
<div>
|
<div class="ui-page-header">
|
||||||
<h1>Communities</h1>
|
<div class="ui-page-title">
|
||||||
<p class="subtitle">Browse organizations and communities</p>
|
<h1>Communities</h1>
|
||||||
|
<p class="ui-subtitle">Browse organizations and communities</p>
|
||||||
|
</div>
|
||||||
|
<div class="ui-actions">
|
||||||
|
<a href="/communities/new" class="ui-btn ui-btn-primary" id="create-btn" style="display: none;">Create Community</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="/communities/new" class="btn-create" id="create-btn" style="display: none;">+ Create Community</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search-bar">
|
<div class="communities-toolbar ui-card ui-card-glass">
|
||||||
<input type="text" id="search-input" placeholder="Search communities..." />
|
<div class="toolbar-row">
|
||||||
</div>
|
<div class="toolbar-search">
|
||||||
|
<input type="text" id="search-input" class="ui-input" placeholder="Search communities…" autocomplete="off" />
|
||||||
<div id="communities-list" class="list">
|
</div>
|
||||||
<p class="loading">Loading communities...</p>
|
<div class="toolbar-sort">
|
||||||
</div>
|
<select id="sort-select" class="ui-select" aria-label="Sort communities">
|
||||||
|
<option value="name_asc">Name (A–Z)</option>
|
||||||
<script is:inline define:vars={{ apiBase }}>
|
<option value="name_desc">Name (Z–A)</option>
|
||||||
|
<option value="slug_asc">Slug (A–Z)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui-kpis" aria-label="Communities summary">
|
||||||
|
<div class="ui-kpi">
|
||||||
|
<div class="ui-kpi-value" id="kpi-total">—</div>
|
||||||
|
<div class="ui-kpi-label">Total</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui-kpi">
|
||||||
|
<div class="ui-kpi-value" id="kpi-showing">—</div>
|
||||||
|
<div class="ui-kpi-label">Showing</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui-kpi">
|
||||||
|
<div class="ui-kpi-value" id="kpi-query">All</div>
|
||||||
|
<div class="ui-kpi-label">Filter</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="communities-list" class="communities-grid" aria-live="polite">
|
||||||
|
<div class="state-card ui-card"><p class="loading">Loading communities…</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script is:inline define:vars={{ apiBase }}>
|
||||||
(function() {
|
(function() {
|
||||||
var API_BASE = apiBase;
|
var API_BASE = apiBase;
|
||||||
var allCommunities = [];
|
var allCommunities = [];
|
||||||
|
var currentQuery = '';
|
||||||
|
var currentSort = 'name_asc';
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || '').replace(/[&<>\"']/g, function(ch) {
|
||||||
|
switch (ch) {
|
||||||
|
case '&': return '&';
|
||||||
|
case '<': return '<';
|
||||||
|
case '>': return '>';
|
||||||
|
case '"': return '"';
|
||||||
|
case "'": return ''';
|
||||||
|
default: return ch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateKpis(total, showing, query) {
|
||||||
|
var totalEl = document.getElementById('kpi-total');
|
||||||
|
var showingEl = document.getElementById('kpi-showing');
|
||||||
|
var queryEl = document.getElementById('kpi-query');
|
||||||
|
if (totalEl) totalEl.textContent = String(total);
|
||||||
|
if (showingEl) showingEl.textContent = String(showing);
|
||||||
|
if (queryEl) queryEl.textContent = query ? '“' + query + '”' : 'All';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSkeleton(count) {
|
||||||
|
var container = document.getElementById('communities-list');
|
||||||
|
if (!container) return;
|
||||||
|
var html = '';
|
||||||
|
for (var i = 0; i < count; i++) {
|
||||||
|
html += '<div class="community-tile ui-card is-skeleton" aria-hidden="true">' +
|
||||||
|
'<div class="tile-top">' +
|
||||||
|
'<div class="skel skel-title"></div>' +
|
||||||
|
'<div class="skel skel-slug"></div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="skel skel-desc"></div>' +
|
||||||
|
'<div class="skel skel-desc short"></div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortCommunities(list) {
|
||||||
|
var sorted = list.slice();
|
||||||
|
sorted.sort(function(a, b) {
|
||||||
|
var an = (a && a.name ? String(a.name) : '').toLowerCase();
|
||||||
|
var bn = (b && b.name ? String(b.name) : '').toLowerCase();
|
||||||
|
var as = (a && a.slug ? String(a.slug) : '').toLowerCase();
|
||||||
|
var bs = (b && b.slug ? String(b.slug) : '').toLowerCase();
|
||||||
|
|
||||||
|
if (currentSort === 'name_desc') {
|
||||||
|
if (an < bn) return 1;
|
||||||
|
if (an > bn) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSort === 'slug_asc') {
|
||||||
|
if (as < bs) return -1;
|
||||||
|
if (as > bs) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (an < bn) return -1;
|
||||||
|
if (an > bn) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
function renderCommunities(communities) {
|
function renderCommunities(communities) {
|
||||||
var container = document.getElementById('communities-list');
|
var container = document.getElementById('communities-list');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
if (!communities || communities.length === 0) {
|
if (!communities || communities.length === 0) {
|
||||||
container.innerHTML = '<div class="empty"><p>No communities found.</p></div>';
|
container.innerHTML = '<div class="state-card ui-card"><p>No communities found.</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var html = '';
|
var html = '';
|
||||||
for (var i = 0; i < communities.length; i++) {
|
for (var i = 0; i < communities.length; i++) {
|
||||||
var c = communities[i];
|
var c = communities[i];
|
||||||
html += '<a href="/communities/' + c.slug + '" class="community-card">' +
|
|
||||||
'<h3>' + c.name + '</h3>' +
|
var name = escapeHtml(c && c.name ? c.name : 'Community');
|
||||||
'<p class="slug">/' + c.slug + '</p>' +
|
var slug = escapeHtml(c && c.slug ? c.slug : '');
|
||||||
'<p class="description">' + (c.description || 'No description') + '</p>' +
|
var desc = escapeHtml(c && c.description ? c.description : 'No description');
|
||||||
|
|
||||||
|
html += '<a href="/communities/' + slug + '" class="community-tile ui-card">' +
|
||||||
|
'<div class="tile-top">' +
|
||||||
|
'<h3 class="tile-title">' + name + '</h3>' +
|
||||||
|
'<div class="tile-slug">/' + slug + '</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p class="tile-desc">' + desc + '</p>' +
|
||||||
|
'<div class="tile-meta">' +
|
||||||
|
'<span class="meta-pill">Open community</span>' +
|
||||||
|
'</div>' +
|
||||||
'</a>';
|
'</a>';
|
||||||
}
|
}
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterCommunities(query) {
|
function applyFilterSortAndRender() {
|
||||||
var q = query.toLowerCase().trim();
|
var q = String(currentQuery || '').toLowerCase().trim();
|
||||||
if (!q) {
|
var filtered = allCommunities;
|
||||||
renderCommunities(allCommunities);
|
if (q) {
|
||||||
return;
|
filtered = allCommunities.filter(function(c) {
|
||||||
|
var name = (c && c.name ? String(c.name) : '').toLowerCase();
|
||||||
|
var slug = (c && c.slug ? String(c.slug) : '').toLowerCase();
|
||||||
|
var desc = (c && c.description ? String(c.description) : '').toLowerCase();
|
||||||
|
return name.indexOf(q) >= 0 || slug.indexOf(q) >= 0 || desc.indexOf(q) >= 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
var filtered = allCommunities.filter(function(c) {
|
|
||||||
return c.name.toLowerCase().indexOf(q) >= 0 ||
|
var sorted = sortCommunities(filtered);
|
||||||
c.slug.toLowerCase().indexOf(q) >= 0 ||
|
updateKpis(allCommunities.length, sorted.length, currentQuery);
|
||||||
(c.description && c.description.toLowerCase().indexOf(q) >= 0);
|
renderCommunities(sorted);
|
||||||
});
|
|
||||||
renderCommunities(filtered);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCommunities() {
|
function loadCommunities() {
|
||||||
var container = document.getElementById('communities-list');
|
var container = document.getElementById('communities-list');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
renderSkeleton(8);
|
||||||
|
updateKpis('—', '—', '');
|
||||||
|
|
||||||
fetch(API_BASE + '/api/communities')
|
fetch(API_BASE + '/api/communities')
|
||||||
.then(function(res) {
|
.then(function(res) {
|
||||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
|
|
@ -73,11 +185,11 @@ import { API_BASE as apiBase } from '../lib/api';
|
||||||
})
|
})
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
allCommunities = Array.isArray(data) ? data : (data.value || data.communities || []);
|
allCommunities = Array.isArray(data) ? data : (data.value || data.communities || []);
|
||||||
renderCommunities(allCommunities);
|
applyFilterSortAndRender();
|
||||||
})
|
})
|
||||||
.catch(function(error) {
|
.catch(function(error) {
|
||||||
console.error('Failed to load communities:', error);
|
console.error('Failed to load communities:', error);
|
||||||
container.innerHTML = '<div class="error"><p>Failed to load communities.</p><p class="hint">Make sure the backend is running on localhost:3000</p></div>';
|
container.innerHTML = '<div class="state-card ui-card"><p class="error">Failed to load communities.</p><p class="hint">Please try again later.</p></div>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,7 +200,16 @@ import { API_BASE as apiBase } from '../lib/api';
|
||||||
var searchInput = document.getElementById('search-input');
|
var searchInput = document.getElementById('search-input');
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
searchInput.addEventListener('input', function(e) {
|
searchInput.addEventListener('input', function(e) {
|
||||||
filterCommunities(e.target.value);
|
currentQuery = e.target.value;
|
||||||
|
applyFilterSortAndRender();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortSelect = document.getElementById('sort-select');
|
||||||
|
if (sortSelect) {
|
||||||
|
sortSelect.addEventListener('change', function(e) {
|
||||||
|
currentSort = e.target.value || 'name_asc';
|
||||||
|
applyFilterSortAndRender();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,127 +221,167 @@ import { API_BASE as apiBase } from '../lib/api';
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.communities {
|
.communities-toolbar {
|
||||||
padding: 1.5rem 0;
|
padding: 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-row {
|
.toolbar-row {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-create {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: var(--color-text-inverse);
|
|
||||||
padding: 0.625rem 1.25rem;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: all var(--motion-fast) var(--easing-standard);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-create:hover {
|
|
||||||
background: var(--color-primary-hover);
|
|
||||||
color: var(--color-text-inverse);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(129, 140, 248, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-create:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar input {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar input::placeholder {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 220px;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.communities-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.community-card {
|
@media (max-width: 1024px) {
|
||||||
display: block;
|
.communities-grid {
|
||||||
background: var(--color-surface);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
border: 1px solid var(--color-border);
|
}
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
color: var(--color-text);
|
|
||||||
transition: all var(--motion-normal) var(--easing-standard);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.community-card:hover {
|
@media (max-width: 640px) {
|
||||||
|
.toolbar-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.communities-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.community-tile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.25rem 1.25rem 1rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform var(--motion-normal) var(--easing-standard), box-shadow var(--motion-normal) var(--easing-standard), border-color var(--motion-fast) var(--easing-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.community-tile::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(700px 220px at 15% 0%, rgba(129, 140, 248, 0.12), transparent 60%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--motion-normal) var(--easing-standard);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.community-tile:hover {
|
||||||
border-color: var(--color-border-hover);
|
border-color: var(--color-border-hover);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.community-card h3 {
|
.community-tile:hover::before {
|
||||||
font-size: 1.125rem;
|
opacity: 1;
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slug {
|
.tile-top {
|
||||||
color: var(--color-primary);
|
display: grid;
|
||||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
gap: 0.25rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-title {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-slug {
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', ui-monospace, monospace;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
margin-bottom: 0.5rem;
|
color: var(--color-primary);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.tile-desc {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
line-height: 1.5;
|
line-height: 1.55;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
min-height: calc(0.9375rem * 1.55 * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading, .empty, .error {
|
.tile-meta {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
color: rgba(255, 255, 255, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
padding: 2.5rem 1.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4rem 2rem;
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
background: var(--color-surface);
|
margin: 0;
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
margin-top: 0.5rem;
|
margin: 0.5rem 0 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-skeleton {
|
||||||
|
border-color: rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skel {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(90deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.09) 35%, rgba(255,255,255,0.05) 70%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skel-title { height: 18px; width: 68%; }
|
||||||
|
.skel-slug { height: 12px; width: 36%; }
|
||||||
|
.skel-desc { height: 12px; width: 100%; margin-top: 0.75rem; }
|
||||||
|
.skel-desc.short { width: 78%; margin-top: 0.5rem; }
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: 0% 0%; }
|
||||||
|
100% { background-position: -200% 0%; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue