Files
Aislo/ui_template/ui_template_workflow_layout.ts
2026-07-15 18:33:33 +09:00

175 lines
5.8 KiB
TypeScript

import "./ui_template_workflow_layout.css";
export interface WorkflowStage {
stage_no: number;
stage_key: string;
state: string;
}
export interface WorkflowLayoutOptions {
title: string;
steps: string[];
activeStep: number;
leftPanel: HTMLElement;
mainContent: HTMLElement;
stages?: WorkflowStage[];
routes?: string[];
onStepClick?: (stepIndex: number, route: string) => void;
onMenuToggle?: (isOpen: boolean) => void;
onHeaderToggle?: (isVisible: boolean) => void;
}
export interface WorkflowLayoutHandle {
root: HTMLElement;
setMenuOpen: (isOpen: boolean) => void;
setHeaderVisible: (isVisible: boolean) => void;
}
function createStepBar(
steps: readonly string[],
activeStep: number,
options?: {
stages?: WorkflowStage[];
routes?: string[];
onStepClick?: (stepIndex: number, route: string) => void;
},
): HTMLElement {
const bar = document.createElement("div");
bar.className = "ui-workflow-layout__steps";
steps.forEach((step, index) => {
const button = document.createElement("button");
button.type = "button";
button.className = "ui-workflow-layout__step";
if (index === activeStep) button.classList.add("is-active");
button.textContent = step;
// 스텝바는 항상 자유롭게 이동 가능(게이팅하지 않음).
// 단계 완료/무효화 판정은 사용자가 각 페이지의 액션 버튼(업로드·분석 실행 등)을
// 눌렀을 때 백엔드가 계산·DB 갱신하며, 여기서는 그 결과를 색상/툴팁으로 표시만 한다.
button.classList.add("is-enabled");
// state 기반 스타일링 (표시 전용)
if (options?.stages && options.stages[index]) {
const state = options.stages[index].state;
button.classList.add(`state-${state.toLowerCase()}`);
if (state === "STALE") {
button.title = "Stale (하위 단계 변경으로 무효화됨)";
} else if (state === "FAILED") {
button.title = "Failed (실패)";
} else if (state === "COMPLETE") {
button.title = "Complete (완료)";
} else if (state === "IN_PROGRESS") {
button.title = "In Progress (진행 중)";
} else {
button.title = "Not Started (미실행)";
}
}
// 클릭 이벤트 — 모든 스텝에서 이동 허용
if (options?.routes && options?.routes[index]) {
button.addEventListener("click", () => {
options.onStepClick?.(index, options.routes![index]);
});
}
bar.append(button);
});
return bar;
}
export function createWorkflowLayout(options: WorkflowLayoutOptions): WorkflowLayoutHandle {
const root = document.createElement("div");
root.className = "ui-workflow-layout";
const header = document.createElement("header");
header.className = "ui-workflow-layout__header";
const menuButton = document.createElement("button");
menuButton.type = "button";
menuButton.className = "ui-workflow-layout__menu-button";
menuButton.setAttribute("aria-label", "Toggle settings panel");
menuButton.innerHTML = "🛠️ <span>Option ▾</span>";
const titleWrap = document.createElement("div");
titleWrap.className = "ui-workflow-layout__title-wrap";
const title = document.createElement("h2");
title.className = "ui-workflow-layout__title";
title.textContent = options.title;
titleWrap.append(
title,
createStepBar(options.steps, options.activeStep, {
stages: options.stages,
routes: options.routes,
onStepClick: options.onStepClick,
}),
);
const headerHideButton = document.createElement("button");
headerHideButton.type = "button";
headerHideButton.className = "ui-workflow-layout__header-hide";
headerHideButton.setAttribute("aria-label", "Hide header");
headerHideButton.textContent = "▲";
header.append(titleWrap, headerHideButton);
// 헤더 숨김 시 표시되는 복원 화살표
const headerRevealButton = document.createElement("button");
headerRevealButton.type = "button";
headerRevealButton.className = "ui-workflow-layout__header-reveal";
headerRevealButton.setAttribute("aria-label", "Show header");
headerRevealButton.textContent = "▼";
const body = document.createElement("div");
body.className = "ui-workflow-layout__body";
const dropdownPanel = document.createElement("div");
dropdownPanel.className = "ui-workflow-layout__dropdown-panel";
dropdownPanel.append(options.leftPanel);
const main = document.createElement("main");
main.className = "ui-workflow-layout__main";
main.style.position = "relative";
main.append(menuButton, options.mainContent);
body.append(dropdownPanel, main);
root.append(header, headerRevealButton, body);
function setMenuOpen(isOpen: boolean): void {
root.classList.toggle("is-menu-open", isOpen);
menuButton.setAttribute("aria-expanded", String(isOpen));
const icon = menuButton.querySelector("span");
if (icon) {
icon.textContent = isOpen ? "Option ▴" : "Option ▾";
}
options.onMenuToggle?.(isOpen);
}
function setHeaderVisible(isVisible: boolean): void {
root.classList.toggle("is-header-hidden", !isVisible);
headerHideButton.setAttribute("aria-expanded", String(isVisible));
options.onHeaderToggle?.(isVisible);
}
menuButton.addEventListener("click", (e) => {
e.stopPropagation();
setMenuOpen(!root.classList.contains("is-menu-open"));
});
document.addEventListener("click", (e) => {
if (root.classList.contains("is-menu-open")) {
const target = e.target as HTMLElement;
if (!dropdownPanel.contains(target) && !menuButton.contains(target)) {
setMenuOpen(false);
}
}
});
headerHideButton.addEventListener("click", () => setHeaderVisible(false));
headerRevealButton.addEventListener("click", () => setHeaderVisible(true));
setMenuOpen(false);
setHeaderVisible(true);
return { root, setMenuOpen, setHeaderVisible };
}