Files
Aislo/ui_template/ui_template_float_panel.ts
T
eomsangdonandClaude Opus 5.5 a3365946fd feat(ui-kit): 틀 부품 셋 — 도구줄 · 떠 있는 판 · 오른쪽 서랍 겉 (PLAN 552-4)
- 도구줄 createToolbar: data-size 24 · 28 · 32 · 나눔선 · 밀기 · 유리 꼴 · 단 없는 단추에 같은 단을 붙임(552-2 setUiSize)
- 떠 있는 판 createFloatPanel: 머리 32 · 닫기 = 아이콘 단추 24(x) · 끌기 머리 · 팝오버
- 오른쪽 서랍: 흰 판 + 1 px 선 · 자리 · 폭 · 열고 닫기 그대로 · 머리 규칙 8
- 부품 모음 틀 아홉 견본 다 참 · 지킴 시험 아홉 부품

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:33:32 +09:00

80 lines
3.4 KiB
TypeScript

/* =============================================================================
* ui_template_float_panel.ts
* 떠 있는 판 `float-panel`(552-4 틀) — 지도 · 그림 위에 뜨는 작은 판: 머리(제목 · 닫기 ✕) + 몸 · 뒷막 없음.
* 꼴 = 판(머리 + 몸) · 끌기 머리(draggable · 부모 안에서 옮김) · 팝오버(popover · 머리 없음 · 몸만)
* 크기 = 머리 data-size 32 · 닫기 = 아이콘 단추 24 · 폭 = width(기본 내용 폭)
* 상태 = 열림 · 닫힘(hidden) · 끄는 중(`is-dragging`)
* 쓰는 법 = const fp = createFloatPanel({ title: "측정", onClose: () => fp.root.remove() }); fp.body.append(table); map.append(fp.root)
* 대신하는 옛 변종 = B05 `b05-routeedit__hint/__info/__measure` · `b05-crossmodal` · `masshaul-overlay` · B06 `b06-structure-panel` ·
* B08 `b08-milestone__tools` · B09 `.b09-popup` · CAD `.cad-quick-properties` · E03 `e03-view3d` · M02 `m02-pop` · D03 `d03-plan__cross` …
* ========================================================================== */
import { createIconButton } from "./ui_template_button";
import "./ui_template_float_panel.css";
export interface FloatPanelOptions {
title?: string;
/** 있으면 머리에 닫기 ✕ */
onClose?: () => void;
/** 머리를 잡아 부모 안에서 옮김 */
draggable?: boolean;
/** 머리 없는 팝오버 */
popover?: boolean;
width?: number;
}
export interface FloatPanel {
root: HTMLElement;
body: HTMLElement;
}
/** 머리 끌기 — 부모(위치 잡힌 칸) 안에서 left · top 만 바꿈 */
function dragBy(root: HTMLElement, head: HTMLElement): void {
head.addEventListener("pointerdown", (down) => {
if ((down.target as HTMLElement).closest("button")) return;
const x0 = down.clientX - root.offsetLeft;
const y0 = down.clientY - root.offsetTop;
head.setPointerCapture(down.pointerId);
root.classList.add("is-dragging");
const move = (ev: PointerEvent): void => {
root.style.left = `${Math.max(0, ev.clientX - x0)}px`;
root.style.top = `${Math.max(0, ev.clientY - y0)}px`;
};
const up = (): void => {
root.classList.remove("is-dragging");
head.removeEventListener("pointermove", move);
};
head.addEventListener("pointermove", move);
head.addEventListener("pointerup", up, { once: true });
});
}
export function createFloatPanel(opts: FloatPanelOptions): FloatPanel {
const root = document.createElement("section");
root.className = `ui-float-panel${opts.popover ? " ui-float-panel--popover" : ""}`;
if (opts.width) root.style.width = `${opts.width}px`;
if (!opts.popover) {
const head = document.createElement("header");
head.className = "ui-float-panel__head";
head.dataset.size = "32";
const title = document.createElement("strong");
title.className = "ui-float-panel__title";
title.textContent = opts.title ?? "";
head.append(title);
if (opts.onClose)
head.append(
createIconButton({ icon: "x", title: "닫기", size: 24, onClick: () => opts.onClose?.() }),
);
if (opts.draggable) {
head.classList.add("is-draggable");
dragBy(root, head);
}
root.append(head);
if (opts.title) root.setAttribute("aria-label", opts.title);
}
const body = document.createElement("div");
body.className = "ui-float-panel__body";
root.append(body);
return { root, body };
}