상세 배수유역 정보는 페이지에 들어오면 바로 보여야 하는데 기본값이 접힘이었고, 접기/펼치기 화살표도 반대를 가리키고 있었다. - 기본값: 저장값이 없을 때 `!== "false"`(접힘)를 `=== "true"`(펼침)로 바꿨다. 같은 페이지 하단 종단 패널(B05_wf2_Route_UI_Profile_Panel.ts:592)이 이미 쓰던 규칙과 같아졌다. - 화살표: 공용 `createWorkflowPanelHandle`은 왼쪽에 도킹해 왼쪽으로 접히는 패널 (제목 카드)을 전제로 회전값을 고정하고 있었다. 배수유역 패널은 오른쪽에 도킹해 오른쪽으로 접히므로 같은 회전을 쓰면 열기/닫기 표시가 정확히 거꾸로 나온다. `collapseToward` 인자를 추가하고 이 패널만 "right"를 넘긴다. 기본값 "left"라 나머지 호출처는 회전값이 이전과 동일하다: - ui_template_overlay.ts:81 제목 카드 — "side" 기본값, 열림 180° / 닫힘 0° 그대로 - B05_wf2_Route_UI_Profile_Panel.ts:244 하단 패널 — "bottom" 배치라 애초에 무관 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
166 lines
5.5 KiB
TypeScript
166 lines
5.5 KiB
TypeScript
import "./ui_template_overlay.css";
|
||
import { t } from "./ui_template_locale";
|
||
|
||
const TITLE_OVERLAY_STATE_KEY = "frd_workflow_title_overlay_open";
|
||
const PROGRESS_OVERLAY_STATE_KEY = "frd_workflow_progress_overlay_open";
|
||
|
||
export interface WorkflowOverlayOptions {
|
||
title: string;
|
||
progressContent: HTMLElement;
|
||
optionsContent?: HTMLElement;
|
||
showTitlePanel?: boolean;
|
||
onOptionsOpenChange?: (isOpen: boolean) => void;
|
||
}
|
||
|
||
export interface WorkflowOverlayHandle {
|
||
root: HTMLElement;
|
||
setTitleOpen: (isOpen: boolean) => void;
|
||
setProgressOpen: (isOpen: boolean) => void;
|
||
}
|
||
|
||
export interface WorkflowPanelHandle {
|
||
root: HTMLButtonElement;
|
||
setOpen: (isOpen: boolean) => void;
|
||
}
|
||
|
||
export function createWorkflowPanelHandle(
|
||
placement: "side" | "bottom",
|
||
/**
|
||
* 패널이 접히면서 사라지는 쪽. 화살표가 항상 그 방향을 가리키게 만든다.
|
||
*
|
||
* 기본값 `"left"`는 왼쪽에 도킹해 왼쪽으로 접히는 패널(제목 카드)용이다.
|
||
* 오른쪽에 도킹해 오른쪽으로 접히는 패널(B05 배수유역도)은 `"right"`를 넘겨야
|
||
* 화살표가 뒤집히지 않는다 — 도킹 방향이 반대인데 같은 회전을 쓰면 열기/닫기
|
||
* 표시가 정확히 거꾸로 보인다.
|
||
*/
|
||
collapseToward: "left" | "right" = "left",
|
||
): WorkflowPanelHandle {
|
||
const root = document.createElement("button");
|
||
root.type = "button";
|
||
root.className = `ui-workflow-overlay__handle ui-workflow-overlay__handle--${placement}`;
|
||
const icon = document.createElement("span");
|
||
icon.className = "ui-workflow-overlay__handle-icon";
|
||
root.append(icon);
|
||
|
||
function setOpen(isOpen: boolean): void {
|
||
// 삼각형은 회전 0°에서 오른쪽(▶)을 가리킨다. 펼친 상태면 접히는 쪽을,
|
||
// 접힌 상태면 펼쳐질 쪽을 가리키도록 180°를 붙였다 뗀다.
|
||
const pointsRight = isOpen === (collapseToward === "right");
|
||
const rotation = placement === "side" ? (pointsRight ? 0 : 180) : isOpen ? 90 : -90;
|
||
icon.style.transform = `rotate(${rotation}deg)`;
|
||
root.title = t(isOpen ? "Workflow_Overlay_Collapse" : "Workflow_Overlay_Expand");
|
||
root.setAttribute("aria-label", root.title);
|
||
root.setAttribute("aria-expanded", String(isOpen));
|
||
}
|
||
|
||
return { root, setOpen };
|
||
}
|
||
|
||
function readOpenState(key: string): boolean {
|
||
return sessionStorage.getItem(key) !== "false";
|
||
}
|
||
|
||
function createPanel(
|
||
variant: "title" | "progress",
|
||
titleText: string,
|
||
body: HTMLElement,
|
||
storageKey: string,
|
||
onOpenChange?: (isOpen: boolean) => void,
|
||
): { root: HTMLElement; setOpen: (isOpen: boolean) => void } {
|
||
const root = document.createElement("aside");
|
||
root.className = `ui-workflow-overlay__panel ui-workflow-overlay__panel--${variant}`;
|
||
|
||
// title 패널은 사이드바 도킹형 — 토글이 패널 우측 가장자리 세로 중앙 핸들로 나감
|
||
const isSidebar = variant === "title";
|
||
|
||
const header = document.createElement("div");
|
||
header.className = "ui-workflow-overlay__header";
|
||
const title = document.createElement("h2");
|
||
title.className = "ui-workflow-overlay__title";
|
||
title.textContent = titleText;
|
||
const panelHandle = isSidebar ? createWorkflowPanelHandle("side") : null;
|
||
const toggle = panelHandle?.root ?? document.createElement("button");
|
||
if (!panelHandle) {
|
||
toggle.type = "button";
|
||
toggle.className = "ui-workflow-overlay__toggle";
|
||
}
|
||
|
||
if (isSidebar) {
|
||
header.append(title);
|
||
root.append(header, toggle);
|
||
} else {
|
||
header.append(title, toggle);
|
||
root.append(header);
|
||
}
|
||
if (body.childElementCount > 0) {
|
||
body.classList.add("ui-workflow-overlay__body");
|
||
root.append(body);
|
||
}
|
||
|
||
function setOpen(isOpen: boolean): void {
|
||
root.classList.toggle("is-collapsed", !isOpen);
|
||
if (panelHandle) panelHandle.setOpen(isOpen);
|
||
else {
|
||
toggle.textContent = isOpen ? "−" : "+";
|
||
toggle.title = t(isOpen ? "Workflow_Overlay_Collapse" : "Workflow_Overlay_Expand");
|
||
toggle.setAttribute("aria-label", toggle.title);
|
||
toggle.setAttribute("aria-expanded", String(isOpen));
|
||
}
|
||
sessionStorage.setItem(storageKey, String(isOpen));
|
||
onOpenChange?.(isOpen);
|
||
}
|
||
|
||
function toggleOpen(): void {
|
||
setOpen(root.classList.contains("is-collapsed"));
|
||
}
|
||
|
||
title.tabIndex = 0;
|
||
title.setAttribute("role", "button");
|
||
title.addEventListener("click", toggleOpen);
|
||
title.addEventListener("keydown", (event) => {
|
||
if (event.key === "Enter" || event.key === " ") {
|
||
event.preventDefault();
|
||
toggleOpen();
|
||
}
|
||
});
|
||
toggle.addEventListener("click", toggleOpen);
|
||
setOpen(readOpenState(storageKey));
|
||
return { root, setOpen };
|
||
}
|
||
|
||
export function createWorkflowOverlays(options: WorkflowOverlayOptions): WorkflowOverlayHandle {
|
||
const root = document.createElement("div");
|
||
root.className = "ui-workflow-overlay";
|
||
|
||
const progressBody = document.createElement("div");
|
||
progressBody.append(options.progressContent);
|
||
|
||
const progressPanel = createPanel(
|
||
"progress",
|
||
t("Workflow_Progress_Title"),
|
||
progressBody,
|
||
PROGRESS_OVERLAY_STATE_KEY,
|
||
);
|
||
let setTitleOpen = (_isOpen: boolean): void => {};
|
||
if (options.showTitlePanel !== false) {
|
||
const titleBody = document.createElement("div");
|
||
if (options.optionsContent) titleBody.append(options.optionsContent);
|
||
const titlePanel = createPanel(
|
||
"title",
|
||
options.title,
|
||
titleBody,
|
||
TITLE_OVERLAY_STATE_KEY,
|
||
options.onOptionsOpenChange,
|
||
);
|
||
root.append(titlePanel.root);
|
||
setTitleOpen = titlePanel.setOpen;
|
||
}
|
||
root.append(progressPanel.root);
|
||
|
||
return {
|
||
root,
|
||
setTitleOpen,
|
||
setProgressOpen: progressPanel.setOpen,
|
||
};
|
||
}
|