This commit is contained in:
2026-07-17 15:11:31 +09:00
parent 4ac681bd10
commit cf459f97a6
41 changed files with 1686 additions and 1026 deletions
+45 -91
View File
@@ -1,5 +1,6 @@
import "./ui_template_workflow_layout.css";
import { t } from "./ui_template_locale";
import { createWorkflowOverlays } from "./ui_template_overlay";
export interface WorkflowStage {
stage_no: number;
@@ -11,20 +12,18 @@ export interface WorkflowLayoutOptions {
title: string;
steps: string[];
activeStep: number;
leftPanel: HTMLElement;
leftPanel?: HTMLElement;
mainContent: HTMLElement;
stages?: WorkflowStage[];
currentStage?: number;
routes?: readonly 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;
setOptionsOpen: (isOpen: boolean) => void;
setProgressOpen: (isOpen: boolean) => void;
}
export interface StepBarOptions {
@@ -32,9 +31,13 @@ export interface StepBarOptions {
currentStage?: number;
routes?: readonly string[];
compact?: boolean;
orientation?: "horizontal" | "vertical";
icons?: readonly string[];
onStepClick?: (stepIndex: number, route: string) => void;
}
export const WORKFLOW_STEP_ICONS = ["📁", "🗺️", "🛣️", "📐", "🏗️", "∑", "📄"] as const;
export function createStepBar(
steps: readonly string[],
activeStep: number,
@@ -43,18 +46,31 @@ export function createStepBar(
const bar = document.createElement("div");
bar.className = "ui-workflow-layout__steps";
if (options?.compact) bar.classList.add("is-compact");
if (options?.orientation === "vertical") bar.classList.add("is-vertical");
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;
const iconText = options?.icons?.[index];
if (iconText) {
const icon = document.createElement("span");
icon.className = "ui-workflow-layout__step-icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = iconText;
const label = document.createElement("span");
label.className = "ui-workflow-layout__step-label";
label.textContent = step;
button.append(icon, label);
} else {
button.textContent = step;
}
const stage =
options?.stages?.find((item) => item.stage_no === index) ?? options?.stages?.[index];
const hasStages = Boolean(options?.stages?.length);
const isEnabled =
!hasStages || stage?.state !== "NOT_STARTED" || stage?.stage_no === options?.currentStage;
const isBlockedState = stage?.state === "NOT_STARTED" || stage?.state === "STALE";
const isEnabled = !hasStages || !isBlockedState || stage?.stage_no === options?.currentStage;
button.classList.toggle("is-enabled", isEnabled);
button.disabled = !isEnabled;
@@ -90,95 +106,33 @@ export function createWorkflowLayout(options: WorkflowLayoutOptions): WorkflowLa
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,
currentStage: options.currentStage,
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);
main.append(options.mainContent);
body.append(main);
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"));
const progressContent = createStepBar(options.steps, options.activeStep, {
stages: options.stages,
currentStage: options.currentStage,
routes: options.routes,
orientation: "vertical",
icons: WORKFLOW_STEP_ICONS,
onStepClick: options.onStepClick,
});
const overlays = createWorkflowOverlays({
title: options.title,
optionsContent: options.leftPanel,
progressContent,
onOptionsOpenChange: (isOpen) => root.classList.toggle("is-options-open", isOpen),
});
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 };
root.append(body, overlays.root);
return {
root,
setOptionsOpen: overlays.setTitleOpen,
setProgressOpen: overlays.setProgressOpen,
};
}