테마: 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") : "";
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
{{ghost_head}}
|
||||
</head>
|
||||
<body class="{{body_class}} bg-bgr text-typ antialiased">
|
||||
<body class="{{body_class}} bg-bgr text-typ antialiased"{{#if @site.accent_color}} style="--accent: {{@site.accent_color}}; --accent-strong: color-mix(in srgb, {{@site.accent_color}} 84%, #000);"{{/if}}>
|
||||
{{> "site/topbar"}}
|
||||
<div class="left-sidebar-backdrop" data-left-sidebar-backdrop hidden></div>
|
||||
<div class="site-shell-wrap border-t border-brd bg-bgr">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 배포 가이드
|
||||
|
||||
## 현재 버전
|
||||
- `v0.2.7`
|
||||
- `v0.2.8`
|
||||
|
||||
## Git 기본 설정
|
||||
- 저장소 작성자 정보는 아래 값으로 통일한다.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 파일-화면 매핑 가이드
|
||||
|
||||
## 현재 버전
|
||||
- `v0.2.7`
|
||||
- `v0.2.8`
|
||||
|
||||
## 공통 레이아웃
|
||||
- [default.hbs](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/default.hbs): 전체 3열 셸, 1296px 공통 폭 계산, 공통 자산 로드
|
||||
|
||||
14
docs/spec.md
14
docs/spec.md
@@ -1,7 +1,7 @@
|
||||
# 기술 명세
|
||||
|
||||
## 현재 버전
|
||||
- `v0.2.7`
|
||||
- `v0.2.8`
|
||||
|
||||
## 테마 개요
|
||||
- Ghost `v5` 대응 커스텀 테마
|
||||
@@ -13,7 +13,11 @@
|
||||
- `home`, `index`, `tag`, `author`, `post`, `page` 템플릿
|
||||
- 검색 오버레이, 탭 전환, 다크모드 토글용 프런트 스크립트
|
||||
- Ghost `navigation`, `get`, `subscribe_form`, `comments`, `pagination` 헬퍼 사용
|
||||
- `@site.accent_color`가 설정된 경우 `default.hbs`의 body 인라인 변수로 전역 `--accent`/`--accent-strong`를 덮어써 테마 포인트 색상을 동기화함
|
||||
- `topbar` 브랜드는 `@site.logo`를 우선 렌더링하고, 로고 미설정 시 `@site.title` 텍스트를 fallback으로 사용함
|
||||
- `home` Hero는 `@site.cover_image`가 있을 때 배경 이미지로 적용하며 오버레이와 텍스트 대비를 함께 조정함
|
||||
- 좌측 사이드바 `menu-groups`: Primary는 `Home pages` 아코디언 안에 `{{navigation}}`, Secondary(관리자에 항목이 있을 때만)는 그 아래 `More links` 아코디언 안에 `{{navigation type="secondary"}}`로 동일 마크업(`ul.nav`)을 노출함
|
||||
- 상단 사용자 메뉴는 멤버 로그인 시에만 드롭다운 상단에 이름/아바타 행을 노출하며, 서버 렌더링 값으로 초기 표시한 뒤 `/members/api/member/` 재조회로 실시간 동기화하고 아바타 미등록 시 fallback 문자(이름/이메일 첫 글자)를 사용함. 비로그인 시 해당 행은 렌더하지 않음
|
||||
- Tailwind CSS 빌드 결과물(`assets/built/tailwind.css`)을 기존 `screen.css`와 함께 로드
|
||||
- Tailwind 기본 초기화(`preflight`)를 활성화해 브라우저 기본 마진과 폼 스타일을 리셋
|
||||
- Alpine.js 로컬 자산(`assets/built/alpine.js`)을 전역 로드
|
||||
@@ -23,6 +27,12 @@
|
||||
- 좌측 카테고리 영역은 Alpine.js로 제어되며 `1024px` 이상에서 기본 열림, 미만에서 기본 닫힘
|
||||
- 좌측 카테고리 목록은 `data-category-priority-order`에 지정한 태그 slug를 우선순위로 먼저 배치하고, 나머지는 `count.posts desc` 기본 순서를 유지한 뒤 제한 개수만 노출함
|
||||
- 좌측 네비게이션 마커와 카테고리 마커는 동일한 세로 바 → 원형 hover 패턴 사용
|
||||
- 상단 사용자 버튼/메뉴 아바타는 로그인 멤버일 때 댓글 UI와 유사한 형태(이니셜 배경 + 이미지 오버레이)로 표시하며, 이니셜과 배경색은 멤버 이름/이메일 기반으로 동기화함. 비로그인 상태는 `icon-user-circle` 아이콘을 사용함
|
||||
- 로그인 상태 사용자 버튼/메뉴 아바타의 배경색은 투명으로 유지하며, 비로그인 상태 아이콘은 32px 버튼 영역에 맞는 크기로 렌더링함
|
||||
- 상단 사용자 메뉴의 멤버 동기화는 `/members/api/member/` 재조회(`cache: no-store`)로 수행하며 `member`/`members[0]` 응답 형태를 모두 처리함
|
||||
- 검색 모달은 `partials/site/topbar.hbs`의 `data-search-source`(posts/tags/authors) 데이터를 사용하고, 결과를 `Authors`, `Tags`, `Posts` 섹션으로 분리해 렌더링함
|
||||
- 검색 모달 입력 왼쪽 `X` 버튼은 입력값 초기화 용도이며, 모달 닫기는 배경 클릭/ESC로 처리함
|
||||
- 검색 모달 입력의 브라우저 기본 우측 cancel 버튼은 숨김 처리하고, `search-result__excerpt`는 한 줄 말줄임으로 고정함
|
||||
- 전역 `ol`, `ul`, `menu` 기본 패딩과 리스트 스타일 리셋 적용
|
||||
- `author.hbs`는 페이지 컨텍스트의 작성자 데이터를 직접 사용
|
||||
- `page-tags.hbs`는 `slug=tags` 페이지에 연결 가능
|
||||
@@ -30,6 +40,8 @@
|
||||
- `tags-index.hbs`는 Ghost `routes.yaml` 커스텀 라우트로 `/tags/`에 연결됨
|
||||
- 로컬 개발 환경의 실제 라우트 설정은 `.docker/ghost/content/settings/routes.yaml`을 기준으로 사용함
|
||||
- 홈 메인 피드는 히어로, Featured 수평 슬라이드, Latest 리스트 구성을 사용함
|
||||
- 홈 Hero는 `@site.cover_image`가 있을 때만 커버 이미지 영역으로 노출하며, 텍스트/구독 폼은 렌더링하지 않음
|
||||
- 홈 Hero 커버는 이미지 로딩 완료 전까지 스켈레톤(shimmer) 애니메이션을 표시하고, 로드 후 실제 이미지로 페이드 전환함
|
||||
- 홈 Latest 블록 아래에는 `home-categories` partial로 태그별 섹션을 두며, 좌측 사이드바와 동일한 `data-category-priority-order`·`data-category-priority-limit`(10)로 정렬·개수 제한 후 태그당 최신 글 최대 5개를 번호 링크로만 표시함. 세로 액센트는 태그 `accent_color`를 `--color-accent`에 넣고 `assets/styles/tailwind.css`의 `.home-categories__row`에서 `border-left: 3px solid var(--color-accent)`로 표시한다(미설정 시 중립색). 설명이 없으면 빈 칸으로 두며, 좌·우 열은 `minmax(0,2fr)`/`minmax(0,3fr)` 그리드로 고정한다
|
||||
- 태그·작성자 아카이브(`tag.hbs`, `author.hbs`)는 홈과 동일한 `post-feed`를 쓰지 않고, `post-feed-archive`로 글 목록과 페이지네이션만 노출함
|
||||
- 우측 사이드바 `Recommended` 섹션은 Ghost `recommendations` 데이터를 우선 사용하며, 항목별 외부 링크와 favicon 표시를 지원함
|
||||
|
||||
@@ -1,8 +1,34 @@
|
||||
# 업데이트 로그
|
||||
|
||||
## v0.2.8 - 2026-04-17
|
||||
- `package.json` 버전을 `0.2.8`로 증가.
|
||||
- `partials/site/topbar.hbs`: 비로그인 시 사용자 드롭다운 상단 프로필 행(아바타·이름)을 렌더하지 않도록 `{{#if @member}}`로 분리.
|
||||
- `docs/spec.md`, `docs/map.md`, `docs/deploy.md` 현재 버전을 `v0.2.8`로 동기화.
|
||||
|
||||
## v0.2.7 - 2026-04-17
|
||||
- `package.json` 버전을 `0.2.7`로 증가.
|
||||
- `partials/site/sidebar-left.hbs`: `@site.secondary_navigation`이 있을 때 `Home pages` 아래에 `More links` 아코디언 그룹을 추가하고 `{{navigation type="secondary"}}`로 Secondary 링크를 노출.
|
||||
- `partials/home/hero.hbs`: 로그인 멤버(`@member`)일 때 홈 Hero 구독 폼을 숨기도록 조건 처리.
|
||||
- `partials/site/topbar.hbs`: 멤버 아바타 미등록 시 사용자 아이콘(`icon-user-circle`)을 노출하고, Gravatar 외부 링크를 제거.
|
||||
- `partials/site/topbar.hbs`: 이름 비어있을 때 기본 이름(`Member`)을 표시하도록 보정.
|
||||
- `assets/built/theme.js`: 사용자 메뉴 오픈/포털 진입/포커스 복귀 시 `/members/api/member/`를 `no-store`로 재조회하고 `member`/`members[0]` 응답을 모두 처리해 닉네임 변경이 새로고침 없이 반영되도록 보정.
|
||||
- `partials/site/topbar.hbs`: 검색 모달 전용 데이터 소스(`data-search-source`)를 추가해 포스트/태그/작성자 전체 목록을 검색 대상으로 통합.
|
||||
- `assets/built/theme.js`: 검색 로직을 섹션형 결과(`Authors`, `Tags`, `Posts`) 렌더링으로 교체하고 포스트 제목/요약 검색을 지원.
|
||||
- `assets/built/screen.css`: 검색 모달 레이아웃을 단일 패널 구조로 정리하고 섹션/아이템 스타일을 추가.
|
||||
- `partials/site/topbar.hbs`, `assets/built/theme.js`: 검색창 좌측 `X` 버튼을 모달 닫기 대신 입력 초기화 동작으로 변경.
|
||||
- `assets/built/screen.css`: 검색 input의 기본 우측 cancel(`::-webkit-search-cancel-button`)을 숨김 처리.
|
||||
- `assets/built/screen.css`: `search-result__excerpt`를 한 줄 말줄임(`text-overflow: ellipsis`)으로 변경.
|
||||
- `partials/site/topbar.hbs`: 로그인 여부와 무관하게 사용자 버튼/메뉴 아바타를 `icon-user-circle` 아이콘으로 고정하고 `@member.avatar_image` 렌더링 분기를 제거.
|
||||
- `default.hbs`: `@site.accent_color`가 있으면 `--accent`, `--accent-strong` CSS 변수를 런타임으로 덮어쓰도록 적용.
|
||||
- `partials/site/topbar.hbs`: `@site.logo`가 있을 때 헤더 브랜드에 로고 이미지를 우선 표시하고, 없으면 사이트 제목 텍스트를 표시.
|
||||
- `partials/home/hero.hbs`: `@site.cover_image`가 있을 때 홈 Hero 배경으로 커버 이미지를 적용하고 텍스트 대비를 보정.
|
||||
- `partials/site/topbar.hbs`, `assets/built/theme.js`: 로그인 멤버 아바타를 댓글 UI와 유사한 형태(이니셜 배경 + 이미지 오버레이)로 표시하고, 이름/이메일 기반 초기문자·배경색을 동기화.
|
||||
- `partials/site/topbar.hbs`, `assets/built/theme.js`: 멤버 아바타 초기문자를 템플릿 시드(`@member.name` 또는 `@member.email`)로 먼저 렌더해 `M` 고정 표시가 남지 않도록 보정.
|
||||
- `partials/home/hero.hbs`: 홈 Hero를 커버 전용 영역으로 정리해 `@site.cover_image`가 있을 때만 렌더링하고, 하드코딩 텍스트/구독 폼을 제거.
|
||||
- `partials/home/hero.hbs`, `assets/built/screen.css`, `assets/built/theme.js`: 홈 Hero 커버 이미지 로딩 중 그라데이션 대신 스켈레톤(shimmer) 애니메이션을 표시하고, 이미지 로드 완료 시 페이드 전환되도록 처리.
|
||||
- `partials/site/topbar.hbs`, `assets/built/theme.js`: 로그인 상태 사용자 버튼/팝업 아바타 뒤 배경색을 제거(transparent)하고, 비로그인 사용자 아이콘 크기를 버튼/팝업 영역에 맞게 조정.
|
||||
- `partials/site/topbar.hbs`: 비로그인 사용자 아이콘의 내부 `svg`를 부모 크기(`size-*`)에 맞춰 렌더링하도록 보정해 24px 고정으로 작게 보이던 문제를 수정.
|
||||
- `partials/site/topbar.hbs`: 비로그인 사용자 아이콘 크기 규칙을 `w/h-full + svg 62%` 고정 비율로 통일해 팝업 아바타 영역(32/40px)에서 위치·크기 불균형이 생기지 않도록 조정.
|
||||
- `docs/spec.md`, `docs/map.md`, `docs/deploy.md` 현재 버전을 `v0.2.7`로 동기화.
|
||||
- `docs/history.md`에 Secondary 네비 사이드바 노출(`v0.2.7`) 기록.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghost-theme-thred-clone",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.8",
|
||||
"private": true,
|
||||
"description": "A Ghost theme inspired by the Thred reference layout.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,47 +1,9 @@
|
||||
<section class="home-hero px-5 sm:px-6 py-6 md:py-8 relative" data-home-hero="">
|
||||
<div class="max-w-content mx-auto flex gap-6">
|
||||
<div class="home-hero__content z-2 flex-2 flex flex-col gap-2 items-center justify-center text-center">
|
||||
<h1 class="text-xl md:text-2xl font-semibold text-balance leading-[1.125]">
|
||||
Ideas <em>published</em> for meaningful conversation, <em>discussed</em> and shaped by the community
|
||||
</h1>
|
||||
<p class="text-base text-balance text-typ-tone max-w-md leading-snug">A modern Ghost theme for curated, community-driven publishing, where members join the conversation.</p>
|
||||
<form class="group relative flex w-full max-w-xs flex-col items-start mt-1" data-members-form="subscribe">
|
||||
<fieldset class="w-full flex gap-2 flex-wrap text-sm">
|
||||
<legend class="sr-only">Personal information</legend>
|
||||
<input data-members-email="" class="text-sm bg-bgr-tone border border-brd text-typ flex-2 py-1.5 px-3 rounded-md focus:ring-0 focus:bg-bgr" type="email" autocomplete="email" placeholder="Your email" aria-label="Your email" required="" aria-required="true">
|
||||
<button class="flex-1 px-3 py-1.5 text-bgr font-medium rounded-md hover:opacity-90 bg-gradient-to-b from-typ/75 to-typ/95 border border-typ cursor-pointer" type="submit">
|
||||
<span class="hidden group-[.loading]:flex items-center justify-center">
|
||||
<i class="icon icon-loader size-5 [&_svg]:animate-spin stroke-2" role="presentation">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 3a9 9 0 1 0 9 9"></path>
|
||||
</svg>
|
||||
</i>
|
||||
</span>
|
||||
<span class="group-[.loading]:hidden">Subscribe</span>
|
||||
</button>
|
||||
</fieldset>
|
||||
<div data-notification="" class="absolute left-0 -bottom-16 z-50 invisible opacity-0 -translate-y-4 transition-all text-[0.8rem] text-left font-medium leading-none flex items-center w-full max-w-md rounded-md gap-2 bg-white text-black p-3 shadow-lg group-[.success]:opacity-100 group-[.success]:visible group-[.success]:translate-y-0 group-[.error]:opacity-100 group-[.error]:visible group-[.error]:translate-y-0">
|
||||
<div class="hidden group-[.success]:flex items-center gap-2 flex-1">
|
||||
<i class="icon icon-success size-6 text-emerald-600 fill-current" role="presentation">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z"></path>
|
||||
</svg>
|
||||
</i>
|
||||
<p>Great! Check your inbox and click the link.</p>
|
||||
</div>
|
||||
<div class="hidden group-[.error]:flex items-center gap-2 flex-1">
|
||||
<i class="icon icon-error size-6 text-red-600 fill-current" role="presentation">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 1.67c.955 0 1.845 .467 2.39 1.247l.105 .16l8.114 13.548a2.914 2.914 0 0 1 -2.307 4.363l-.195 .008h-16.225a2.914 2.914 0 0 1 -2.582 -4.2l.099 -.185l8.11 -13.538a2.914 2.914 0 0 1 2.491 -1.403zm.01 13.33l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -7a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z"></path>
|
||||
</svg>
|
||||
</i>
|
||||
<p>Sorry, something went wrong. Please try again.</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{#if @site.cover_image}}
|
||||
<section
|
||||
class="home-hero min-h-[216px] relative overflow-hidden"
|
||||
data-home-hero=""
|
||||
>
|
||||
<div class="home-hero__skeleton absolute inset-0" data-home-hero-skeleton aria-hidden="true"></div>
|
||||
<img class="home-hero__cover absolute inset-0 w-full h-full object-cover" src="{{@site.cover_image}}" alt="{{@site.title}}" loading="eager" fetchpriority="high" data-home-hero-image>
|
||||
</section>
|
||||
{{/if}}
|
||||
@@ -1,5 +1,5 @@
|
||||
<aside class="sidebar sidebar--left h-full flex flex-col">
|
||||
<nav class="menu-groups border-b border-brd pl-4 pr-3 sm:pl-5 xl:pl-0 py-3" data-nav="menu" data-primary-nav x-data="{ homePagesOpen: true, secondaryNavOpen: true, membersOpen: false }">
|
||||
<nav class="menu-groups border-b border-brd pl-4 pr-3 sm:pl-5 xl:pl-0 py-3" data-nav="menu" data-primary-nav x-data="{ homePagesOpen: true, secondaryNavOpen: false, membersOpen: false }">
|
||||
<ul class="menu-groups__list flex flex-col gap-0.75 text-typ text-sm">
|
||||
<li class="menu-group menu-group--nav nav-toggle is-mainitem flex items-center flex-wrap w-full relative group" :class="{ 'is-open': homePagesOpen }" data-label="Home pages" data-slug="home-pages" data-length="10" aria-haspopup="true">
|
||||
<a class="menu-group__link menu-group__link--toggle flex gap-2 items-center flex-1 py-1.5 rounded-theme transition-[padding]" href="#" role="button" @click.prevent="homePagesOpen = !homePagesOpen" :aria-expanded="homePagesOpen.toString()" aria-haspopup="true">
|
||||
|
||||
@@ -130,8 +130,8 @@
|
||||
{{/is}}
|
||||
<span class="flex-1"></span>
|
||||
<footer class="px-5 sm:px-6 xl:pr-1">
|
||||
<div class="py-3 flex flex-col items-start justify-start text-typ-tone text-xs font-blod text-gray-800">
|
||||
©{{date format="YYYY"}} {{@site.title}}. Published with Ghost.
|
||||
<div class="py-3 flex flex-col items-start justify-start text-xs font-blod text-gray-800">
|
||||
©{{date format="YYYY"}} {{@site.title}}. All rights reserved.
|
||||
</div>
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
@@ -24,7 +24,11 @@
|
||||
</span>
|
||||
</button>
|
||||
<a class="brand brand--topbar inline-flex items-center gap-2.5 whitespace-nowrap" href="{{@site.url}}">
|
||||
<span class="brand__name">{{@site.title}}</span>
|
||||
{{#if @site.logo}}
|
||||
<img class="max-h-8 w-auto object-contain" src="{{@site.logo}}" alt="{{@site.title}}">
|
||||
{{else}}
|
||||
<span class="brand__name">{{@site.title}}</span>
|
||||
{{/if}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="topbar__search flex h-full items-center justify-center px-4">
|
||||
@@ -41,44 +45,41 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="topbar__actions relative flex h-full items-center justify-end gap-2 px-4 lg:px-0">
|
||||
<button class="icon-button icon-button--user-menu inline-flex items-center justify-center rounded-theme border border-brd bg-bgr hover:bg-bgr-tone overflow-hidden" type="button" aria-label="Open user menu" data-user-menu-toggle>
|
||||
<button class="w-8 h-8 cursor-pointer icon-button--user-menu inline-flex items-center justify-center rounded-theme overflow-hidden {{#unless @member}}bg-bgr hover:bg-bgr-tone{{/unless}}" type="button" aria-label="Open user menu" data-user-menu-toggle>
|
||||
{{#if @member}}
|
||||
{{#if @member.avatar_image}}
|
||||
<img class="w-full h-full object-cover" src="{{@member.avatar_image}}" alt="{{@member.name}}">
|
||||
{{else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" role="presentation" aria-hidden="true">
|
||||
<figure class="relative w-8 h-8 pointer-events-none" data-member-avatar-figure>
|
||||
<div class="flex items-center justify-center rounded-full w-8 h-8" data-member-avatar-background>
|
||||
<p class="font-sans text-base font-semibold text-typ" data-member-avatar-initial data-member-avatar-seed="{{#if @member.name}}{{@member.name}}{{else}}{{#if @member.email}}{{@member.email}}{{else}}member{{/if}}{{/if}}">{{#if @member.name}}{{@member.name}}{{else}}{{#if @member.email}}{{@member.email}}{{else}}M{{/if}}{{/if}}</p>
|
||||
</div>
|
||||
<img alt="Avatar" class="absolute left-0 top-0 rounded-full w-8 h-8 object-cover {{#unless @member.avatar_image}}hidden{{/unless}}" data-member-avatar-image src="{{@member.avatar_image}}">
|
||||
</figure>
|
||||
{{else}}
|
||||
<i class="icon icon-user pointer-events-none flex items-center justify-center w-full h-full [&_svg]:w-[62%] [&_svg]:h-[62%]" role="presentation" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||
<path d="M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||
<path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855"></path>
|
||||
</svg>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" role="presentation" aria-hidden="true">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||
<path d="M12 10m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||
<path d="M6.168 18.849a4 4 0 0 1 3.832 -2.849h4a4 4 0 0 1 3.834 2.855"></path>
|
||||
</svg>
|
||||
</i>
|
||||
{{/if}}
|
||||
</button>
|
||||
<div class="z-50 p-3 pb-2 flex flex-col text-sm bg-bgr border border-brd rounded-theme absolute top-12 right-0 -translate-y-4 opacity-0 invisible pointer-events-none transition-[transform,opacity,visibility,scale] min-w-[200px] max-w-xs overflow-hidden font-medium scale-95 shadow-md" style="background-color: var(--bg);" data-user-menu>
|
||||
{{#if @member}}
|
||||
<div class="flex items-center gap-2 border-b border-brd pb-3 mb-2">
|
||||
<div class="size-8 md:size-10 rounded-full overflow-hidden bg-bgr-tone">
|
||||
{{#if @member}}
|
||||
{{#if @member.avatar_image}}
|
||||
<img class="size-8 md:size-10 object-cover" src="{{@member.avatar_image}}" alt="{{@member.name}}">
|
||||
{{else}}
|
||||
<span class="size-8 md:size-10 flex items-center justify-center uppercase font-normal text-base md:text-lg">@</span>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<span class="size-8 md:size-10 flex items-center justify-center uppercase font-normal text-base md:text-lg">@</span>
|
||||
{{/if}}
|
||||
<div class="size-8 md:size-10 rounded-full overflow-hidden flex items-center justify-center">
|
||||
<figure class="relative w-8 h-8 md:w-10 md:h-10 pointer-events-none" data-member-avatar-figure>
|
||||
<div class="flex items-center justify-center rounded-full w-8 h-8 md:w-10 md:h-10" data-member-avatar-background>
|
||||
<p class="font-sans text-base font-semibold text-typ" data-member-avatar-initial data-member-avatar-seed="{{#if @member.name}}{{@member.name}}{{else}}{{#if @member.email}}{{@member.email}}{{else}}member{{/if}}{{/if}}">{{#if @member.name}}{{@member.name}}{{else}}{{#if @member.email}}{{@member.email}}{{else}}M{{/if}}{{/if}}</p>
|
||||
</div>
|
||||
<img alt="Avatar" class="absolute left-0 top-0 rounded-full w-8 h-8 md:w-10 md:h-10 object-cover {{#unless @member.avatar_image}}hidden{{/unless}}" data-member-avatar-image src="{{@member.avatar_image}}">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="flex gap-0.5 flex-col">
|
||||
<div class="line-clamp-1 text-ellipsis leading-[1.15] max-w-xs">{{#if @member}}{{@member.name}}{{else}}Anonymous{{/if}}</div>
|
||||
<div class="line-clamp-1 text-ellipsis leading-[1.15] max-w-xs" data-member-name-display>{{#if @member.name}}{{@member.name}}{{else}}Member{{/if}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if @member}}
|
||||
<a href="#/portal/account" class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-theme hover:bg-bgr-tone">
|
||||
<i class="icon icon-login size-5 stroke-2" role="presentation">
|
||||
@@ -142,7 +143,7 @@
|
||||
<div class="search-modal__backdrop" data-search-close></div>
|
||||
<div class="search-modal__panel">
|
||||
<div class="search-modal__input">
|
||||
<button type="button" class="icon-button icon-button--plain" data-search-close aria-label="Close search">×</button>
|
||||
<button type="button" class="icon-button icon-button--plain" data-search-reset aria-label="Clear search">×</button>
|
||||
<input type="search" placeholder="게시물, 태그, 작성자 검색" data-search-input>
|
||||
</div>
|
||||
<div class="search-modal__body" data-search-results>
|
||||
@@ -150,3 +151,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden data-search-source>
|
||||
{{#get "authors" limit="100"}}
|
||||
{{#foreach authors}}
|
||||
<span data-search-item data-search-type="author" data-search-title="{{name}}" data-search-url="{{url}}" data-search-image="{{#if profile_image}}{{img_url profile_image size='s'}}{{/if}}"></span>
|
||||
{{/foreach}}
|
||||
{{/get}}
|
||||
{{#get "tags" limit="100"}}
|
||||
{{#foreach tags}}
|
||||
<span data-search-item data-search-type="tag" data-search-title="{{name}}" data-search-url="{{url}}"></span>
|
||||
{{/foreach}}
|
||||
{{/get}}
|
||||
{{#get "posts" limit="150"}}
|
||||
{{#foreach posts}}
|
||||
<span data-search-item data-search-type="post" data-search-title="{{title}}" data-search-url="{{url}}" data-search-excerpt="{{excerpt words='24'}}"></span>
|
||||
{{/foreach}}
|
||||
{{/get}}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user