UPDATE
This commit is contained in:
@@ -235,14 +235,15 @@ a {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 28px 16px 24px;
|
||||
align-items: stretch;
|
||||
text-align: left;
|
||||
gap: 12px;
|
||||
min-height: 248px;
|
||||
padding: 22px 18px 18px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
|
||||
cursor: pointer;
|
||||
animation: fadeInUp 0.5s ease both;
|
||||
}
|
||||
|
||||
@@ -253,32 +254,104 @@ a {
|
||||
background: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.service-card i {
|
||||
font-size: 32px;
|
||||
.service-card-link {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.service-card-link i {
|
||||
font-size: 28px;
|
||||
color: var(--accent);
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.service-card:hover i {
|
||||
transform: scale(1.1);
|
||||
.service-card:hover .service-card-link i {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.card-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
line-height: 1.3;
|
||||
color: var(--text-heading);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.card-sub {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 6px;
|
||||
margin-top: 4px;
|
||||
font-family: 'Inter', monospace;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid rgba(255, 196, 81, 0.16);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 196, 81, 0.08);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.service-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.service-action:hover {
|
||||
border-color: var(--border-hover);
|
||||
background: rgba(255, 196, 81, 0.1);
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
/* Staggered animation */
|
||||
.service-card:nth-child(1) { animation-delay: 0.02s; }
|
||||
.service-card:nth-child(2) { animation-delay: 0.04s; }
|
||||
@@ -522,7 +595,7 @@ a {
|
||||
|
||||
.search-result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
@@ -538,14 +611,31 @@ a {
|
||||
font-size: 18px;
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.search-result-item span {
|
||||
.search-result-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-result-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.search-result-desc,
|
||||
.search-result-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.search-result-empty {
|
||||
padding: 24px 20px;
|
||||
text-align: center;
|
||||
@@ -634,6 +724,14 @@ a {
|
||||
|
||||
/* ── Mobile Adjustments ── */
|
||||
@media (max-width: 768px) {
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.search-overlay {
|
||||
padding: 8vh 16px 0;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,30 @@
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function buildSearchText(svc) {
|
||||
const linkParts = (svc.links || []).flatMap(link => [link.label, link.url]);
|
||||
return [
|
||||
svc.name,
|
||||
svc.description,
|
||||
svc.sub,
|
||||
...(svc.tags || []),
|
||||
...linkParts
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function getServiceActions(svc) {
|
||||
const actions = [{ label: 'Open', url: svc.url }, ...(svc.links || [])];
|
||||
const seen = new Set();
|
||||
|
||||
return actions.filter(action => {
|
||||
if (!action?.label || !action?.url) return false;
|
||||
const key = `${action.label}::${action.url}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Nav Renderer ──
|
||||
function renderNav(config) {
|
||||
const nav = document.getElementById('nav');
|
||||
@@ -55,11 +79,11 @@
|
||||
grid.className = 'card-grid';
|
||||
|
||||
services.forEach((svc, i) => {
|
||||
const card = document.createElement('a');
|
||||
card.href = svc.url;
|
||||
const card = document.createElement('article');
|
||||
card.className = 'service-card';
|
||||
card.dataset.name = svc.name.toLowerCase();
|
||||
card.dataset.category = cat.id;
|
||||
card.dataset.searchText = buildSearchText(svc).toLowerCase();
|
||||
card.style.animationDelay = `${(i + 1) * 0.02}s`;
|
||||
|
||||
// Status dot
|
||||
@@ -67,20 +91,67 @@
|
||||
dot.className = 'card-status-dot unknown';
|
||||
card.appendChild(dot);
|
||||
|
||||
const primaryLink = document.createElement('a');
|
||||
primaryLink.href = svc.url;
|
||||
primaryLink.className = 'service-card-link';
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.className = svc.icon;
|
||||
card.appendChild(icon);
|
||||
primaryLink.appendChild(icon);
|
||||
|
||||
const heading = document.createElement('div');
|
||||
heading.className = 'card-heading';
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'card-name';
|
||||
name.textContent = svc.name;
|
||||
card.appendChild(name);
|
||||
heading.appendChild(name);
|
||||
|
||||
if (svc.sub) {
|
||||
const sub = document.createElement('span');
|
||||
sub.className = 'card-sub';
|
||||
sub.textContent = svc.sub;
|
||||
card.appendChild(sub);
|
||||
heading.appendChild(sub);
|
||||
}
|
||||
|
||||
primaryLink.appendChild(heading);
|
||||
card.appendChild(primaryLink);
|
||||
|
||||
if (svc.description) {
|
||||
const description = document.createElement('p');
|
||||
description.className = 'card-description';
|
||||
description.textContent = svc.description;
|
||||
card.appendChild(description);
|
||||
}
|
||||
|
||||
if (Array.isArray(svc.tags) && svc.tags.length > 0) {
|
||||
const tags = document.createElement('div');
|
||||
tags.className = 'card-tags';
|
||||
|
||||
svc.tags.forEach(tagText => {
|
||||
const tag = document.createElement('span');
|
||||
tag.className = 'card-tag';
|
||||
tag.textContent = tagText;
|
||||
tags.appendChild(tag);
|
||||
});
|
||||
|
||||
card.appendChild(tags);
|
||||
}
|
||||
|
||||
const actions = getServiceActions(svc);
|
||||
if (actions.length > 0) {
|
||||
const actionsWrap = document.createElement('div');
|
||||
actionsWrap.className = 'card-actions';
|
||||
|
||||
actions.forEach(action => {
|
||||
const link = document.createElement('a');
|
||||
link.href = action.url;
|
||||
link.className = 'service-action';
|
||||
link.textContent = action.label;
|
||||
actionsWrap.appendChild(link);
|
||||
});
|
||||
|
||||
card.appendChild(actionsWrap);
|
||||
}
|
||||
|
||||
grid.appendChild(card);
|
||||
@@ -169,7 +240,6 @@
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
console.log(`[StatusMonitor] ${mon.label} raw:`, JSON.stringify(data).slice(0, 500));
|
||||
const result = this.parsers[mon.parseType](data);
|
||||
dot.className = 'status-dot ok';
|
||||
value.textContent = result;
|
||||
@@ -201,19 +271,23 @@
|
||||
const dot = card.querySelector('.card-status-dot');
|
||||
if (!dot) return;
|
||||
|
||||
const url = svc.healthCheckUrl;
|
||||
const isRelative = url.startsWith('/');
|
||||
const isHttps = url.startsWith('https://');
|
||||
|
||||
// HTTPS cross-origin can't be reliably checked from the browser
|
||||
// (redirects, CORS headers, etc.) — leave as unknown
|
||||
if (isHttps) return;
|
||||
|
||||
dot.className = 'card-status-dot loading';
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
const url = svc.healthCheckUrl;
|
||||
const isRelative = url.startsWith('/');
|
||||
const isHttps = url.startsWith('https://');
|
||||
|
||||
// Relative URLs and HTTPS: normal fetch. HTTP cross-origin: no-cors.
|
||||
// Relative URLs (/proxy/...): normal fetch. HTTP same-network: no-cors
|
||||
const fetchOpts = { signal: controller.signal };
|
||||
if (!isRelative && !isHttps) {
|
||||
if (!isRelative) {
|
||||
fetchOpts.mode = 'no-cors';
|
||||
}
|
||||
|
||||
@@ -320,14 +394,44 @@
|
||||
this.results.innerHTML = '';
|
||||
if (!query) return;
|
||||
|
||||
const matches = this.services.filter(svc => this.fuzzyMatch(query, svc.name));
|
||||
const matches = this.services.filter(svc => this.fuzzyMatch(query, buildSearchText(svc))).slice(0, 10);
|
||||
|
||||
matches.forEach((svc, i) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'search-result-item' + (i === this.selectedIndex ? ' active' : '');
|
||||
item.dataset.url = svc.url;
|
||||
|
||||
item.innerHTML = `<i class="${svc.icon}"></i><span>${svc.name}</span>`;
|
||||
const icon = document.createElement('i');
|
||||
icon.className = svc.icon;
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'search-result-body';
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.className = 'search-result-title';
|
||||
title.textContent = svc.name;
|
||||
body.appendChild(title);
|
||||
|
||||
if (svc.description) {
|
||||
const desc = document.createElement('span');
|
||||
desc.className = 'search-result-desc';
|
||||
desc.textContent = svc.description;
|
||||
body.appendChild(desc);
|
||||
}
|
||||
|
||||
const metaParts = [];
|
||||
if (svc.sub) metaParts.push(svc.sub);
|
||||
if (Array.isArray(svc.tags) && svc.tags.length > 0) metaParts.push(svc.tags.join(' · '));
|
||||
|
||||
if (metaParts.length > 0) {
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'search-result-meta';
|
||||
meta.textContent = metaParts.join(' | ');
|
||||
body.appendChild(meta);
|
||||
}
|
||||
|
||||
item.appendChild(icon);
|
||||
item.appendChild(body);
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
window.location.href = svc.url;
|
||||
@@ -368,8 +472,8 @@
|
||||
}
|
||||
|
||||
allCards.forEach(card => {
|
||||
const name = card.dataset.name || '';
|
||||
const match = this.fuzzyMatch(query, name);
|
||||
const searchText = card.dataset.searchText || card.dataset.name || '';
|
||||
const match = this.fuzzyMatch(query, searchText);
|
||||
card.classList.toggle('search-hidden', !match);
|
||||
card.classList.toggle('search-highlight', match);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user