테마: v0.2.8 및 상단 사용자 메뉴·검색·히어로·사이드바 누적 보정
Made-with: Cursor
This commit is contained in:
@@ -755,8 +755,6 @@ body:not(.left-sidebar-collapsed) .topbar__sidebar-toggle:hover .topbar__sidebar
|
||||
width: min(100%, 320px);
|
||||
padding: 10px 14px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
@@ -896,6 +894,46 @@ body:not(.left-sidebar-collapsed) .topbar__sidebar-toggle:hover .topbar__sidebar
|
||||
padding: 20px 18px 10px;
|
||||
}
|
||||
|
||||
.home-hero {
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.home-hero__cover {
|
||||
opacity: 0;
|
||||
transition: opacity 0.45s ease;
|
||||
}
|
||||
|
||||
.home-hero__skeleton {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
color-mix(in srgb, var(--surface-muted) 88%, var(--surface)),
|
||||
color-mix(in srgb, var(--surface-muted) 60%, #fff) 45%,
|
||||
color-mix(in srgb, var(--surface-muted) 88%, var(--surface))
|
||||
);
|
||||
background-size: 220% 100%;
|
||||
animation: home-hero-skeleton 1.2s linear infinite;
|
||||
transition: opacity 0.35s ease;
|
||||
}
|
||||
|
||||
.home-hero.is-loaded .home-hero__cover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.home-hero.is-loaded .home-hero__skeleton {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes home-hero-skeleton {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -20% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hero__title {
|
||||
max-width: 760px;
|
||||
margin: 0;
|
||||
@@ -1734,9 +1772,20 @@ body:not(.left-sidebar-collapsed) .topbar__sidebar-toggle:hover .topbar__sidebar
|
||||
|
||||
.search-modal__panel {
|
||||
position: relative;
|
||||
width: min(100%, 720px);
|
||||
margin: 56px auto;
|
||||
padding: 0 18px;
|
||||
width: min(95vw, 620px);
|
||||
margin: 72px auto 0;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-modal__input {
|
||||
width: 100%;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.search-modal__input input {
|
||||
@@ -1745,32 +1794,102 @@ body:not(.left-sidebar-collapsed) .topbar__sidebar-toggle:hover .topbar__sidebar
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
outline: none;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.search-modal__input input::-webkit-search-cancel-button,
|
||||
.search-modal__input input::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-modal__body {
|
||||
margin-top: 14px;
|
||||
padding: 18px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
box-shadow: var(--shadow);
|
||||
max-height: 70vh;
|
||||
padding: 10px 0;
|
||||
max-height: min(70vh, 640px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.search-modal__hint,
|
||||
.search-empty {
|
||||
color: var(--text-soft);
|
||||
padding: 14px 20px;
|
||||
}
|
||||
|
||||
.search-result {
|
||||
display: block;
|
||||
padding: 10px 4px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.search-result:last-child {
|
||||
border-bottom: 0;
|
||||
.search-result:hover {
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.search-result-group + .search-result-group {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-result-group__title {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-soft);
|
||||
padding: 12px 20px 6px;
|
||||
}
|
||||
|
||||
.search-result-group__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.search-author__avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
object-fit: cover;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.search-author__avatar--fallback {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.search-result__prefix {
|
||||
color: var(--text-soft);
|
||||
font-weight: 700;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.search-result__title {
|
||||
display: block;
|
||||
line-height: 1.35;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.search-result__excerpt {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
color: var(--text-soft);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.search-result--post {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -153,6 +153,30 @@
|
||||
|
||||
syncLeftSidebarState();
|
||||
|
||||
function initializeHomeHero() {
|
||||
document.querySelectorAll("[data-home-hero]").forEach(function (heroSection) {
|
||||
var heroImage = heroSection.querySelector("[data-home-hero-image]");
|
||||
|
||||
if (!heroImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
function markHeroLoaded() {
|
||||
heroSection.classList.add("is-loaded");
|
||||
}
|
||||
|
||||
if (heroImage.complete && heroImage.naturalWidth > 0) {
|
||||
markHeroLoaded();
|
||||
return;
|
||||
}
|
||||
|
||||
heroImage.addEventListener("load", markHeroLoaded, { once: true });
|
||||
heroImage.addEventListener("error", markHeroLoaded, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
initializeHomeHero();
|
||||
|
||||
var tabRoot = document.querySelector("[data-tabs]");
|
||||
if (tabRoot) {
|
||||
var triggers = tabRoot.querySelectorAll("[data-tab-trigger]");
|
||||
@@ -176,43 +200,136 @@
|
||||
var searchModal = document.querySelector("[data-search-modal]");
|
||||
var searchInput = document.querySelector("[data-search-input]");
|
||||
var searchResults = document.querySelector("[data-search-results]");
|
||||
var searchResetButtons = document.querySelectorAll("[data-search-reset]");
|
||||
|
||||
function escapeSearchHtml(value) {
|
||||
return String(value || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function getSearchItems() {
|
||||
var sourceItems = document.querySelectorAll("[data-search-source] [data-search-item]");
|
||||
var items = [];
|
||||
|
||||
document.querySelectorAll(".post-card h2 a, .recommended-list a, .category-chip, .author-list__item").forEach(function (link) {
|
||||
sourceItems.forEach(function (itemNode) {
|
||||
var title = (itemNode.dataset.searchTitle || "").trim();
|
||||
var url = (itemNode.dataset.searchUrl || "").trim();
|
||||
|
||||
if (!title || !url) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: (link.textContent || "").trim(),
|
||||
url: link.getAttribute("href") || "#"
|
||||
type: itemNode.dataset.searchType || "post",
|
||||
title: title,
|
||||
url: url,
|
||||
excerpt: (itemNode.dataset.searchExcerpt || "").trim(),
|
||||
image: (itemNode.dataset.searchImage || "").trim()
|
||||
});
|
||||
});
|
||||
|
||||
return items.filter(function (item, index, array) {
|
||||
return item.title && array.findIndex(function (candidate) {
|
||||
return candidate.title === item.title && candidate.url === item.url;
|
||||
return array.findIndex(function (candidate) {
|
||||
return candidate.type === item.type && candidate.title === item.title && candidate.url === item.url;
|
||||
}) === index;
|
||||
});
|
||||
}
|
||||
|
||||
function renderSearchSection(heading, items, renderer) {
|
||||
if (!items.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
'<section class="search-result-group">',
|
||||
'<h3 class="search-result-group__title">' + heading + "</h3>",
|
||||
'<div class="search-result-group__items">',
|
||||
items.map(renderer).join(""),
|
||||
"</div>",
|
||||
"</section>"
|
||||
].join("");
|
||||
}
|
||||
|
||||
function renderSearchResults(keyword) {
|
||||
var normalized = keyword.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
searchResults.innerHTML = '<p class="search-modal__hint">현재 페이지에 표시되는 게시물, 태그 및 작성자를 필터링하려면 입력을 시작하세요.</p>';
|
||||
searchResults.innerHTML = '<p class="search-modal__hint">검색어를 입력하면 Authors, Tags, Posts를 함께 보여줍니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var items = getSearchItems().filter(function (item) {
|
||||
return item.title.toLowerCase().indexOf(normalized) !== -1;
|
||||
var matchedItems = getSearchItems().filter(function (item) {
|
||||
var title = item.title.toLowerCase();
|
||||
var excerpt = item.excerpt.toLowerCase();
|
||||
return title.indexOf(normalized) !== -1 || excerpt.indexOf(normalized) !== -1;
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
searchResults.innerHTML = '<p class="search-empty">현재 보기에 일치하는 항목이 없습니다.</p>';
|
||||
if (!matchedItems.length) {
|
||||
searchResults.innerHTML = '<p class="search-empty">일치하는 항목이 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
searchResults.innerHTML = items.map(function (item) {
|
||||
return '<a class="search-result" href="' + item.url + '">' + item.title + "</a>";
|
||||
}).join("");
|
||||
var authorItems = matchedItems.filter(function (item) {
|
||||
return item.type === "author";
|
||||
});
|
||||
var tagItems = matchedItems.filter(function (item) {
|
||||
return item.type === "tag";
|
||||
});
|
||||
var postItems = matchedItems.filter(function (item) {
|
||||
return item.type === "post";
|
||||
});
|
||||
|
||||
var resultMarkup = "";
|
||||
|
||||
resultMarkup += renderSearchSection("Authors", authorItems, function (item) {
|
||||
var imageMarkup = item.image
|
||||
? '<img class="search-author__avatar" src="' + escapeSearchHtml(item.image) + '" alt="' + escapeSearchHtml(item.title) + '">'
|
||||
: '<span class="search-author__avatar search-author__avatar--fallback">@</span>';
|
||||
|
||||
return [
|
||||
'<a class="search-result search-result--author" href="' + escapeSearchHtml(item.url) + '">',
|
||||
imageMarkup,
|
||||
'<span class="search-result__title">' + escapeSearchHtml(item.title) + "</span>",
|
||||
"</a>"
|
||||
].join("");
|
||||
});
|
||||
|
||||
resultMarkup += renderSearchSection("Tags", tagItems, function (item) {
|
||||
return [
|
||||
'<a class="search-result search-result--tag" href="' + escapeSearchHtml(item.url) + '">',
|
||||
'<span class="search-result__prefix">#</span>',
|
||||
'<span class="search-result__title">' + escapeSearchHtml(item.title) + "</span>",
|
||||
"</a>"
|
||||
].join("");
|
||||
});
|
||||
|
||||
resultMarkup += renderSearchSection("Posts", postItems, function (item) {
|
||||
var excerptMarkup = item.excerpt
|
||||
? '<p class="search-result__excerpt">' + escapeSearchHtml(item.excerpt) + "</p>"
|
||||
: "";
|
||||
|
||||
return [
|
||||
'<a class="search-result search-result--post" href="' + escapeSearchHtml(item.url) + '">',
|
||||
'<h4 class="search-result__title">' + escapeSearchHtml(item.title) + "</h4>",
|
||||
excerptMarkup,
|
||||
"</a>"
|
||||
].join("");
|
||||
});
|
||||
|
||||
searchResults.innerHTML = resultMarkup;
|
||||
}
|
||||
|
||||
function clearSearchInput() {
|
||||
if (!searchInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
searchInput.value = "";
|
||||
renderSearchResults("");
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
function toggleSearch(open) {
|
||||
@@ -242,6 +359,13 @@
|
||||
});
|
||||
});
|
||||
|
||||
searchResetButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
clearSearchInput();
|
||||
});
|
||||
});
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", function (event) {
|
||||
renderSearchResults(event.target.value);
|
||||
@@ -267,6 +391,153 @@
|
||||
var userThemeThumb = document.querySelector("[data-user-theme-thumb]");
|
||||
var userMenuTrack = document.querySelector("[data-user-menu-track]");
|
||||
var userMenuThumb = document.querySelector("[data-user-menu-thumb]");
|
||||
var memberNameDisplays = document.querySelectorAll("[data-member-name-display]");
|
||||
var memberAvatarImages = document.querySelectorAll("[data-member-avatar-image]");
|
||||
var memberAvatarBackgrounds = document.querySelectorAll("[data-member-avatar-background]");
|
||||
var memberAvatarInitials = document.querySelectorAll("[data-member-avatar-initial]");
|
||||
var memberPortalLinks = document.querySelectorAll("a[href^='#/portal/']");
|
||||
var memberRefreshInterval = null;
|
||||
|
||||
function getMemberDisplayName(member) {
|
||||
if (!member) {
|
||||
return "Anonymous";
|
||||
}
|
||||
|
||||
var name = (member.name || "").trim();
|
||||
return name || "Member";
|
||||
}
|
||||
|
||||
function getMemberInitial(member, seed) {
|
||||
var fallbackSeed = (seed || "").trim();
|
||||
var source = "";
|
||||
|
||||
if (member) {
|
||||
source = ((member.name || "").trim() || (member.email || "").trim());
|
||||
}
|
||||
|
||||
source = source || fallbackSeed || "M";
|
||||
return source.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
function applyMemberUi(member) {
|
||||
if (!memberNameDisplays.length && !memberAvatarImages.length && !memberAvatarBackgrounds.length && !memberAvatarInitials.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var displayName = getMemberDisplayName(member);
|
||||
var avatarImage = (member && member.avatar_image ? member.avatar_image : "").trim();
|
||||
var hasAvatarImage = !!avatarImage;
|
||||
|
||||
memberNameDisplays.forEach(function (element) {
|
||||
element.textContent = displayName;
|
||||
});
|
||||
|
||||
memberAvatarImages.forEach(function (element) {
|
||||
if (hasAvatarImage) {
|
||||
element.src = avatarImage;
|
||||
element.alt = displayName;
|
||||
element.classList.remove("hidden");
|
||||
|
||||
if (!element.dataset.errorBound) {
|
||||
element.addEventListener("error", function () {
|
||||
element.classList.add("hidden");
|
||||
});
|
||||
element.dataset.errorBound = "true";
|
||||
}
|
||||
} else {
|
||||
element.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
memberAvatarBackgrounds.forEach(function (element) {
|
||||
element.style.backgroundColor = "transparent";
|
||||
});
|
||||
|
||||
memberAvatarInitials.forEach(function (element) {
|
||||
var seed = element.dataset.memberAvatarSeed || "";
|
||||
element.textContent = getMemberInitial(member, seed);
|
||||
});
|
||||
}
|
||||
|
||||
function initializeMemberAvatarFromSeed() {
|
||||
memberAvatarInitials.forEach(function (element) {
|
||||
var seed = element.dataset.memberAvatarSeed || "";
|
||||
element.textContent = getMemberInitial(null, seed);
|
||||
});
|
||||
|
||||
memberAvatarBackgrounds.forEach(function (element) {
|
||||
element.style.backgroundColor = "transparent";
|
||||
});
|
||||
}
|
||||
|
||||
function getMemberFromPayload(payload) {
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.member) {
|
||||
return payload.member;
|
||||
}
|
||||
|
||||
if (Array.isArray(payload.members) && payload.members.length) {
|
||||
return payload.members[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function refreshMemberUi() {
|
||||
if (!memberNameDisplays.length && !memberAvatarImages.length && !memberAvatarBackgrounds.length && !memberAvatarInitials.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return window
|
||||
.fetch("/members/api/member/?_=" + Date.now(), {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Accept: "application/json"
|
||||
}
|
||||
})
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch member");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then(function (payload) {
|
||||
var member = getMemberFromPayload(payload);
|
||||
|
||||
if (member) {
|
||||
applyMemberUi(member);
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
|
||||
function scheduleMemberUiRefreshLoop() {
|
||||
var attempts = 0;
|
||||
var maxAttempts = 20;
|
||||
|
||||
if (memberRefreshInterval) {
|
||||
window.clearInterval(memberRefreshInterval);
|
||||
memberRefreshInterval = null;
|
||||
}
|
||||
|
||||
refreshMemberUi();
|
||||
|
||||
memberRefreshInterval = window.setInterval(function () {
|
||||
attempts += 1;
|
||||
refreshMemberUi();
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
window.clearInterval(memberRefreshInterval);
|
||||
memberRefreshInterval = null;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function syncUserMenuToggles() {
|
||||
var isDark = body.classList.contains("theme-dark");
|
||||
@@ -316,6 +587,7 @@
|
||||
event.stopPropagation();
|
||||
setUserMenu(userMenu.classList.contains("invisible"));
|
||||
syncUserMenuToggles();
|
||||
refreshMemberUi();
|
||||
});
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
@@ -349,7 +621,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
initializeMemberAvatarFromSeed();
|
||||
syncUserMenuToggles();
|
||||
refreshMemberUi();
|
||||
|
||||
memberPortalLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function () {
|
||||
scheduleMemberUiRefreshLoop();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
if (document.visibilityState === "visible") {
|
||||
refreshMemberUi();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("focus", function () {
|
||||
refreshMemberUi();
|
||||
});
|
||||
|
||||
var recommendationsPortalTrigger = document.querySelector("[data-portal='recommendations']");
|
||||
var recommendationsPortalTitle = recommendationsPortalTrigger ? recommendationsPortalTrigger.getAttribute("data-portal-title") : "";
|
||||
|
||||
Reference in New Issue
Block a user