feat(b05·b06): 종단 누가토량 알약 — 바탕 불투명 · 두 테마 글자 대비 4.5 넘음(붉은 계통 그대로) · 그래프 영역 안 끌어 옮김(마우스 · 손가락 pointer 한 길 · 끝 막힘 · 자리 = 영역 안 비율 · 화면 설정 한 칸 B05 · B06 공용) · 끈 뒤 click 삼킴 · 바닥 서브패널 위 (309)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -382,29 +382,35 @@
|
||||
/* Y축 띠(고정 축) 오른쪽에 놓는다 — 8px 로 두면 축 라벨 위에 겹쳐 값이 잘린다
|
||||
(2026-09-06 실측). 축 폭이 다른 화면은 이 변수를 덮어쓴다. */
|
||||
left: var(--mass-haul-badge-left, 86px);
|
||||
z-index: 4;
|
||||
/* 바닥 서브패널(유토곡선 · 테이블 5) · 손잡이(6) 위 — 어디로 끌어도 보이고 다시 잡힘(309-2) */
|
||||
z-index: 7;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4, 4px);
|
||||
/* 큰 알약(289) — 글자 · 여백 한 단 위 */
|
||||
padding: var(--spacing-4, 4px) var(--spacing-12, 12px);
|
||||
/* 붉은 계통(289-2) — 테두리 · 글자 · 옅은 바탕 = 위험색 섞음(밝음 · 어두움 같이) */
|
||||
/* 붉은 계통(289-2) — 테두리 · 글자 · 옅은 바탕 = 위험색 섞음(밝음 · 어두움 같이)
|
||||
바탕은 불투명(309-1) — 그래프 선 · 채움이 비치지 않게 · 글자 대비 = 두 테마 모두 4.5 : 1 넘음 */
|
||||
border: 1px solid color-mix(in srgb, var(--color-danger) 55%, transparent);
|
||||
border-radius: var(--radius-pills, 999px);
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--color-danger) 6%,
|
||||
color-mix(in srgb, var(--color-surface-raised) 88%, transparent)
|
||||
);
|
||||
background: color-mix(in srgb, var(--color-danger) 8%, var(--color-surface-raised));
|
||||
color: color-mix(in srgb, var(--color-danger) 70%, var(--color-text-body));
|
||||
font-size: var(--pill-font-size-lg);
|
||||
line-height: var(--leading-body-sm);
|
||||
white-space: nowrap;
|
||||
pointer-events: auto;
|
||||
/* 그래프 안 끌어 옮기기(309-2) — 손가락 끌기가 화면 밀기 · 글 고름으로 새지 않게 */
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mass-haul-badge.is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.mass-haul-badge em {
|
||||
color: color-mix(in srgb, var(--color-danger) 55%, var(--color-text-secondary));
|
||||
color: color-mix(in srgb, var(--color-danger) 55%, var(--color-text-body));
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,3 +84,100 @@ export function createMassHaulBadge(): MassHaulBadge {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 알약 자리 저장 창구(화면 설정 한 칸) — 값 = `{"x":0~1,"y":0~1}` 글. */
|
||||
export interface MassHaulBadgePlace {
|
||||
read: () => string | null;
|
||||
write: (value: string) => void;
|
||||
}
|
||||
|
||||
/** 끈 것으로 보는 움직임(px) — 이 안이면 그냥 누름(지금 동작 그대로). */
|
||||
const DRAG_SLOP_PX = 4;
|
||||
|
||||
/**
|
||||
* 알약 끌어 옮기기(309-2) — 마우스 · 손가락 같은 pointer 길 · `area`(그래프 스크롤러)의 보이는 상자 안에서만.
|
||||
* 영역 = 알약 처음 자리(Y축 띠 오른쪽 · 측점 ▲ 줄 아래)가 왼쪽 위 끝 · 스크롤러 보이는 오른쪽 · 아래 끝까지.
|
||||
* 자리 = 영역 안 비율 — 폭 · 높이가 바뀌거나 다시 그려도 같은 비율. 알약은 그래프 형제라 끌기가 그래프 고름에 안 닿음.
|
||||
*/
|
||||
export function attachMassHaulBadgeDrag(
|
||||
root: HTMLElement,
|
||||
area: HTMLElement,
|
||||
place: MassHaulBadgePlace,
|
||||
): void {
|
||||
let origin: { x: number; y: number } | null = null;
|
||||
const bounds = () => {
|
||||
if (!origin) {
|
||||
const [left, top] = [root.style.left, root.style.top];
|
||||
root.style.left = root.style.top = "";
|
||||
origin = { x: root.offsetLeft, y: root.offsetTop };
|
||||
[root.style.left, root.style.top] = [left, top];
|
||||
}
|
||||
const w = area.offsetLeft + area.clientWidth - root.offsetWidth - origin.x;
|
||||
const h = area.offsetTop + area.clientHeight - root.offsetHeight - origin.y;
|
||||
return { ...origin, w: Math.max(0, w), h: Math.max(0, h) };
|
||||
};
|
||||
const clamp = (v: number): number => Math.min(1, Math.max(0, v));
|
||||
const put = (x: number, y: number): void => {
|
||||
const b = bounds();
|
||||
root.style.left = `${Math.round(b.x + clamp(x) * b.w)}px`;
|
||||
root.style.top = `${Math.round(b.y + clamp(y) * b.h)}px`;
|
||||
};
|
||||
const restore = (): void => {
|
||||
if (root.hidden || !root.isConnected) return;
|
||||
try {
|
||||
const v = JSON.parse(place.read() ?? "null") as { x?: unknown; y?: unknown } | null;
|
||||
if (typeof v?.x === "number" && typeof v.y === "number") put(v.x, v.y);
|
||||
} catch {
|
||||
/* 못 읽는 값 = 처음 자리 */
|
||||
}
|
||||
};
|
||||
new ResizeObserver(restore).observe(area);
|
||||
new MutationObserver(restore).observe(root, { attributeFilter: ["hidden"] });
|
||||
|
||||
let drag: { id: number; px: number; py: number; left: number; top: number } | null = null;
|
||||
let moved: { x: number; y: number } | null = null;
|
||||
root.addEventListener("pointerdown", (e) => {
|
||||
if (e.button !== 0) return;
|
||||
drag = {
|
||||
id: e.pointerId,
|
||||
px: e.clientX,
|
||||
py: e.clientY,
|
||||
left: root.offsetLeft,
|
||||
top: root.offsetTop,
|
||||
};
|
||||
moved = null;
|
||||
root.setPointerCapture(e.pointerId);
|
||||
});
|
||||
root.addEventListener("pointermove", (e) => {
|
||||
if (!drag || e.pointerId !== drag.id) return;
|
||||
const dx = e.clientX - drag.px;
|
||||
const dy = e.clientY - drag.py;
|
||||
if (!moved && Math.hypot(dx, dy) < DRAG_SLOP_PX) return;
|
||||
const b = bounds();
|
||||
moved = {
|
||||
x: b.w ? clamp((drag.left + dx - b.x) / b.w) : 0,
|
||||
y: b.h ? clamp((drag.top + dy - b.y) / b.h) : 0,
|
||||
};
|
||||
root.classList.add("is-dragging");
|
||||
put(moved.x, moved.y);
|
||||
});
|
||||
const end = (e: PointerEvent): void => {
|
||||
if (!drag || e.pointerId !== drag.id) return;
|
||||
drag = null;
|
||||
root.classList.remove("is-dragging");
|
||||
if (moved) place.write(JSON.stringify({ x: +moved.x.toFixed(4), y: +moved.y.toFixed(4) }));
|
||||
};
|
||||
root.addEventListener("pointerup", end);
|
||||
root.addEventListener("pointercancel", end);
|
||||
// 끈 뒤 따라오는 click 은 삼킴 — 안 끈 누름은 그대로 지나감
|
||||
root.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
if (!moved) return;
|
||||
moved = null;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user