fix(project): 자체 스크롤 패널에 항상 보이는 스크롤 막대를 JS로 직접 그림
DevTools 박스모델 확인 결과 내 업무 패널은 정상 스크롤 컨테이너였지만 스크롤바 두께가 0 — 이 환경(엣지 오버레이 스크롤바)에선 ::-webkit-scrollbar 커스텀도 scrollbar-width 표준 속성도 모두 무시돼 CSS 로는 막대를 항상 보이게 할 방법이 없었다. 네이티브 막대는 숨기고 attachScrollIndicator() 가 내 업무/좌측 프로젝트 목록/홈 프로젝트 목록/보드 패널 안에 스크롤 위치를 따라 움직이는 막대를 그린다(sticky anchor + 절대배치 track/thumb, 드래그·트랙 클릭 지원, MutationObserver/ResizeObserver 로 갱신). 막대 자리로 각 패널 padding-right 14px 확보, 보드의 scrollbar-gutter 제거. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260916q" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260916r" />
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
{% endblock %}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script src="/static/project.js?v=20260916n" defer></script>
|
||||
<script src="/static/project.js?v=20260916o" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260916q" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260916r" />
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
{% endblock %}
|
||||
|
||||
@@ -380,5 +380,5 @@
|
||||
<script>
|
||||
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
||||
</script>
|
||||
<script src="/static/project.js?v=20260916n" defer></script>
|
||||
<script src="/static/project.js?v=20260916o" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260916q" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260916r" />
|
||||
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
<!-- 타임라인 vis-timeline — self-host -->
|
||||
@@ -392,5 +392,5 @@
|
||||
</script>
|
||||
|
||||
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
||||
<script src="/static/project.js?v=20260916n" defer></script>
|
||||
<script src="/static/project.js?v=20260916o" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
+26
-32
@@ -53,7 +53,7 @@
|
||||
display: grid; grid-template-columns: minmax(0, 1fr) 300px; grid-template-rows: minmax(0, 1fr);
|
||||
gap: 28px; align-items: stretch;
|
||||
}
|
||||
.pj-home-main { min-width: 0; min-height: 0; height: 100%; overflow-y: auto; padding-right: 4px; }
|
||||
.pj-home-main { min-width: 0; min-height: 0; height: 100%; overflow-y: auto; padding-right: 14px; }
|
||||
@media (max-width: 980px) {
|
||||
.erp-page > .pj-wrap:not(.pj-project-view) { display: block; }
|
||||
.pj-home { display: block; }
|
||||
@@ -133,7 +133,7 @@
|
||||
확정하고 자기 안에서만 스크롤. 300px 는 padding 포함(border-box). */
|
||||
.pj-home-side {
|
||||
width: 300px; box-sizing: border-box; min-height: 0; height: 100%; overflow-y: auto;
|
||||
border-left: 1px solid #eef0f2; padding-left: 24px; padding-right: 4px;
|
||||
border-left: 1px solid #eef0f2; padding-left: 24px; padding-right: 14px;
|
||||
}
|
||||
@media (max-width: 980px) { .pj-home-side { border-left: none; padding-left: 0; height: auto; overflow: visible; } }
|
||||
.pj-mytask-list { list-style: none; margin: 0; padding: 0; }
|
||||
@@ -162,33 +162,28 @@
|
||||
display: grid; grid-template-columns: 230px 1fr 300px; grid-template-rows: minmax(0, 1fr);
|
||||
gap: 22px; height: 100%; min-height: 0; align-items: stretch;
|
||||
}
|
||||
.pj-side { min-width: 0; min-height: 0; height: 100%; overflow-y: auto; padding-right: 4px; }
|
||||
.pj-side { min-width: 0; min-height: 0; height: 100%; overflow-y: auto; padding-right: 14px; }
|
||||
.pj-main { min-width: 0; min-height: 0; height: 100%; display: flex; flex-direction: column; }
|
||||
.pj-project-mytasks { min-width: 0; min-height: 0; width: 300px; height: 100%; overflow-y: auto; padding-right: 4px; }
|
||||
.pj-project-mytasks { min-width: 0; min-height: 0; width: 300px; height: 100%; overflow-y: auto; padding-right: 14px; }
|
||||
|
||||
/* 휠 스크롤은 되는데 막대가 안 보인다 — OS/브라우저의 오버레이(자동숨김)
|
||||
스크롤바 때문(호버해야만 잠깐 보이는 스타일). 여기 자체 스크롤 패널들은
|
||||
막대가 항상 보이도록 커스텀 스크롤바를 그린다(윈도우 설정과 무관하게
|
||||
고정 표시). */
|
||||
/* 주의: Chromium(크롬/엣지) 121+ 는 scrollbar-width/scrollbar-color 가 지정돼
|
||||
있으면 ::-webkit-scrollbar 커스텀 스타일을 통째로 무시하고 OS 오버레이
|
||||
(자동숨김) 스크롤바로 돌아간다 — 그래서 표준 속성은 ::-webkit-scrollbar 를
|
||||
지원하지 않는 브라우저(Firefox)에만 준다. */
|
||||
@supports not selector(::-webkit-scrollbar) {
|
||||
.pj-home-side, .pj-home-main, .pj-project-mytasks, .pj-side, .pj-board {
|
||||
scrollbar-width: thin; scrollbar-color: #b9bcc4 #f1f2f4;
|
||||
}
|
||||
}
|
||||
.pj-home-side::-webkit-scrollbar, .pj-home-main::-webkit-scrollbar, .pj-project-mytasks::-webkit-scrollbar,
|
||||
.pj-side::-webkit-scrollbar, .pj-board::-webkit-scrollbar { width: 9px; height: 9px; }
|
||||
.pj-home-side::-webkit-scrollbar-track, .pj-home-main::-webkit-scrollbar-track, .pj-project-mytasks::-webkit-scrollbar-track,
|
||||
.pj-side::-webkit-scrollbar-track, .pj-board::-webkit-scrollbar-track { background: #f1f2f4; border-radius: 999px; }
|
||||
.pj-home-side::-webkit-scrollbar-thumb, .pj-home-main::-webkit-scrollbar-thumb, .pj-project-mytasks::-webkit-scrollbar-thumb,
|
||||
.pj-side::-webkit-scrollbar-thumb, .pj-board::-webkit-scrollbar-thumb {
|
||||
background-color: #b9bcc4; border-radius: 999px;
|
||||
}
|
||||
.pj-home-side::-webkit-scrollbar-thumb:hover, .pj-home-main::-webkit-scrollbar-thumb:hover, .pj-project-mytasks::-webkit-scrollbar-thumb:hover,
|
||||
.pj-side::-webkit-scrollbar-thumb:hover, .pj-board::-webkit-scrollbar-thumb:hover { background-color: #979ba5; }
|
||||
/* ── 자체 스크롤 패널(내 업무/좌측 목록/홈 프로젝트 목록/보드)의 스크롤 막대 ──
|
||||
윈도우/엣지가 오버레이(자동숨김) 스크롤바를 쓰는 환경에선 네이티브 막대가
|
||||
마우스를 올리거나 스크롤 중일 때만 잠깐 보이고, ::-webkit-scrollbar /
|
||||
scrollbar-width 커스텀도 무시된다(DevTools 박스모델에서 두께 0 확인).
|
||||
그래서 네이티브 막대는 아예 숨기고, JS(attachScrollIndicator)가 패널 안에
|
||||
스크롤 위치에 맞춰 움직이는 막대를 직접 그려 어떤 환경에서도 항상
|
||||
보이게 한다. 휠·키보드·드래그 스크롤은 그대로 동작. 막대가 들어갈 자리로
|
||||
각 패널의 padding-right 를 14px 확보한다. */
|
||||
.pj-home-side, .pj-home-main, .pj-side, .pj-board { scrollbar-width: none; }
|
||||
.pj-home-side::-webkit-scrollbar, .pj-home-main::-webkit-scrollbar,
|
||||
.pj-side::-webkit-scrollbar, .pj-board::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
||||
/* anchor = 스크롤러의 첫 자식(높이 0, sticky) → 화면에 보이는 영역의 맨 위에
|
||||
항상 붙어 있고, 그 안의 track 이 보이는 높이만큼 오른쪽 padding 자리에 걸린다. */
|
||||
.pj-vbar-anchor { position: sticky; top: 0; height: 0; z-index: 5; }
|
||||
.pj-vbar-track { position: absolute; top: 0; right: -11px; width: 8px; border-radius: 999px; background: #e6e8ec; }
|
||||
.pj-vbar-thumb { position: absolute; left: 0; top: 0; width: 100%; border-radius: 999px; background: #b3b7bf; cursor: grab; }
|
||||
.pj-vbar-thumb:hover, .pj-vbar-thumb.is-dragging { background: #8f949e; }
|
||||
.pj-vbar-thumb.is-dragging { cursor: grabbing; }
|
||||
@media (max-width: 1280px) {
|
||||
.pj-layout { grid-template-columns: 230px 1fr; }
|
||||
.pj-project-mytasks { display: none; }
|
||||
@@ -276,13 +271,12 @@
|
||||
높이를 정해준다). pj-board-canvas = 매이슨리 절대배치 캔버스 — 스크롤
|
||||
컨테이너 자체 높이와 콘텐츠(캔버스) 높이 계산을 분리해야 스크롤이
|
||||
올바르게 생긴다(캔버스만 커지고 뷰포트는 flex 크기를 유지). */
|
||||
/* scrollbar-gutter:stable — 스크롤바 자리를 항상 미리 비워둔다. 이게 없으면
|
||||
렌더 시점에 스크롤바가 아직 안 생긴 상태로 clientWidth를 재서 열 너비를
|
||||
계산했다가, 막상 스크롤바가 생기면 그만큼 폭이 줄어 마지막 열이 스크롤바
|
||||
에 가려 보였다. */
|
||||
/* 네이티브 스크롤바는 숨기고 JS 가 그리는 막대를 쓴다(위 .pj-vbar-* 참고).
|
||||
막대 자리는 padding-right 14px — 매이슨리 열 너비는 canvas(=padding 안쪽)
|
||||
기준으로 계산되므로 열이 막대에 가려지지 않는다. */
|
||||
.pj-board {
|
||||
flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
|
||||
scrollbar-gutter: stable; padding-right: 4px; padding-bottom: 8px;
|
||||
padding-right: 14px; padding-bottom: 8px;
|
||||
}
|
||||
.pj-board-canvas { position: relative; }
|
||||
/* 세션(보드 컬럼) — 배경이 페이지와 거의 같으면 구분이 안 되므로(#f6f5fb ≈
|
||||
|
||||
@@ -2470,6 +2470,75 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── 항상 보이는 스크롤 막대 ──
|
||||
// 윈도우/엣지가 오버레이(자동숨김) 스크롤바를 쓰면 네이티브 막대는 마우스를
|
||||
// 올리거나 스크롤 중일 때만 잠깐 보이고, ::-webkit-scrollbar 커스텀도
|
||||
// 무시된다(DevTools 박스모델에서 두께 0 확인). 그래서 자체 스크롤하는 패널은
|
||||
// 네이티브 막대를 CSS 로 숨기고(.pj-vbar-* 참고) 여기서 막대를 직접 그린다.
|
||||
// 휠·키보드 스크롤은 그대로 동작하고, 막대 드래그/트랙 클릭도 지원한다.
|
||||
function attachScrollIndicator(scroller) {
|
||||
if (!scroller || scroller.dataset.pjVbar) return;
|
||||
scroller.dataset.pjVbar = "1";
|
||||
const anchor = document.createElement("div");
|
||||
anchor.className = "pj-vbar-anchor";
|
||||
anchor.innerHTML = '<div class="pj-vbar-track" hidden><div class="pj-vbar-thumb"></div></div>';
|
||||
scroller.insertBefore(anchor, scroller.firstChild);
|
||||
const track = anchor.firstChild;
|
||||
const thumb = track.firstChild;
|
||||
let thumbH = 0, thumbTop = 0;
|
||||
|
||||
function update() {
|
||||
const sh = scroller.scrollHeight, ch = scroller.clientHeight;
|
||||
if (ch <= 0 || sh <= ch + 1) { if (!track.hidden) track.hidden = true; return; }
|
||||
if (track.hidden) track.hidden = false;
|
||||
thumbH = Math.max(28, Math.round(ch * ch / sh));
|
||||
thumbTop = Math.round((ch - thumbH) * scroller.scrollTop / (sh - ch));
|
||||
// 같은 값이면 안 쓴다 — MutationObserver 가 자기 변경에 반응해 도는 걸 막는다.
|
||||
const trackH = ch + "px", th = thumbH + "px", tf = "translateY(" + thumbTop + "px)";
|
||||
if (track.style.height !== trackH) track.style.height = trackH;
|
||||
if (thumb.style.height !== th) thumb.style.height = th;
|
||||
if (thumb.style.transform !== tf) thumb.style.transform = tf;
|
||||
}
|
||||
scroller.addEventListener("scroll", update, { passive: true });
|
||||
scroller.addEventListener("load", update, true); // 아바타 이미지 로드로 높이가 바뀔 때
|
||||
window.addEventListener("resize", update);
|
||||
if (window.ResizeObserver) new ResizeObserver(update).observe(scroller);
|
||||
if (window.MutationObserver) {
|
||||
new MutationObserver(function (records) {
|
||||
for (let i = 0; i < records.length; i++) {
|
||||
if (!anchor.contains(records[i].target)) { update(); return; }
|
||||
}
|
||||
}).observe(scroller, { childList: true, subtree: true, attributes: true, characterData: true });
|
||||
}
|
||||
|
||||
let dragging = false, startY = 0, startTop = 0;
|
||||
thumb.addEventListener("mousedown", function (e) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
dragging = true; startY = e.clientY; startTop = scroller.scrollTop;
|
||||
thumb.classList.add("is-dragging");
|
||||
});
|
||||
document.addEventListener("mousemove", function (e) {
|
||||
if (!dragging) return;
|
||||
const sh = scroller.scrollHeight, ch = scroller.clientHeight, room = ch - thumbH;
|
||||
if (room <= 0) return;
|
||||
scroller.scrollTop = startTop + (e.clientY - startY) * (sh - ch) / room;
|
||||
});
|
||||
document.addEventListener("mouseup", function () {
|
||||
if (dragging) { dragging = false; thumb.classList.remove("is-dragging"); }
|
||||
});
|
||||
track.addEventListener("mousedown", function (e) {
|
||||
if (e.target !== track) return;
|
||||
e.preventDefault();
|
||||
const y = e.clientY - track.getBoundingClientRect().top;
|
||||
scroller.scrollTop += (y < thumbTop ? -1 : 1) * scroller.clientHeight * 0.9;
|
||||
});
|
||||
update();
|
||||
}
|
||||
function initScrollIndicators() {
|
||||
document.querySelectorAll(".pj-home-side, .pj-home-main, .pj-side, .pj-board")
|
||||
.forEach(attachScrollIndicator);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
initHome();
|
||||
initProject();
|
||||
@@ -2477,5 +2546,6 @@
|
||||
initInbox();
|
||||
initLiveRefresh();
|
||||
wireMyTreeToggles();
|
||||
initScrollIndicators();
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user