import "./ui_template_general_layout.css"; import { createButton, createCard } from "./ui_template_elements"; import { currentLanguageIndex, ui_locales } from "./ui_template_locale"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } export function buildSectionHeader( titleText: string, actionButtons: HTMLElement[] = [], ): HTMLElement { const header = document.createElement("div"); header.className = "ui-general-block__section-header b01-dashboard__section-header"; const title = document.createElement("h3"); title.className = "ui-card__title"; title.style.margin = "0"; title.textContent = titleText; const actions = document.createElement("div"); actions.className = "ui-general-block__actions b01-dashboard__actions"; actions.append(...actionButtons); header.append(title, actions); return header; } export function section( title: string, body: HTMLElement, wide = false, actions: HTMLElement[] = [], ): HTMLElement { const header = buildSectionHeader(title, actions); const card = createCard({ body: [header, body], raised: true }); card.classList.add("ui-general-block__section", "b01-dashboard__section"); if (wide) card.classList.add("ui-general-block__section--wide", "b01-dashboard__section--wide"); return card; } /** * 목록 컨테이너가 항목 수만큼 계속 길어지지 않도록 `visible`개 높이에서 자르고 안쪽 스크롤로 넘긴다. * * 행 높이를 상수로 박지 않는다 — 프로젝트 표는 셀 안에 단계 막대가 들어가고, 보관함 묶음은 접힘 * 상태에 따라 높이가 제각각이다. 그래서 `visible`번째 항목의 실제 아래쪽 좌표를 재서 정한다. * 표·묶음 어느 쪽이든 같은 코드로 맞는다. * * 측정은 DOM에 붙은 뒤라야 뜻이 있으므로 `requestAnimationFrame`으로 미룬다. */ const scrollLimitHandlers = new WeakMap void>(); export function limitVisibleRows( host: HTMLElement, items: () => HTMLElement[], visible: number, ): () => void { const apply = (): void => { if (!host.isConnected) return; const rows = items(); if (rows.length <= visible) { // 넘치지 않으면 스크롤바를 만들지 않는다 — 원래 모습 그대로 둔다. host.style.maxHeight = ""; host.classList.remove("ui-general-block__scroll-limit"); return; } host.classList.add("ui-general-block__scroll-limit"); // 재측정할 때 앞서 넣은 max-height가 남아 있으면 잘린 위치를 다시 재게 된다. host.style.maxHeight = ""; const cut = rows[visible - 1].getBoundingClientRect().bottom; // 테두리와 가로 스크롤바는 max-height(border-box) 안에 함께 들어가므로 그만큼 더 준다. const chrome = host.offsetHeight - host.clientHeight; host.style.maxHeight = `${cut - host.getBoundingClientRect().top + chrome}px`; }; const schedule = (): void => void requestAnimationFrame(apply); // 목록을 다시 그릴 때마다 이 함수가 불린다. 이전 청취자를 걷어내지 않으면 계속 쌓인다. const previous = scrollLimitHandlers.get(host); if (previous) window.removeEventListener("resize", previous); scrollLimitHandlers.set(host, schedule); // 창 폭이 바뀌면 줄바꿈으로 행 높이가 달라진다. window.addEventListener("resize", schedule); schedule(); return schedule; } export function table(headers: string[], rows: HTMLElement[][], maxRows = 0): HTMLElement { if (!rows.length) { const empty = document.createElement("p"); empty.className = "ui-general-block__empty b01-dashboard__empty"; empty.textContent = L("Common_Status_Empty"); return empty; } const wrap = document.createElement("div"); wrap.className = "ui-general-block__table-wrap b01-dashboard__table-wrap"; const tableEl = document.createElement("table"); tableEl.className = "ui-general-block__table b01-dashboard__table"; const thead = document.createElement("thead"); const headRow = document.createElement("tr"); for (const header of headers) { const th = document.createElement("th"); th.textContent = header; headRow.append(th); } thead.append(headRow); const tbody = document.createElement("tbody"); for (const row of rows) { const tr = document.createElement("tr"); for (const cell of row) { const td = document.createElement("td"); td.append(cell); tr.append(td); } tbody.append(tr); } tableEl.append(thead, tbody); wrap.append(tableEl); if (maxRows > 0) { // 머리행은 스크롤해도 붙어 있어야 어느 열인지 안다. wrap.classList.add("ui-general-block__table-wrap--sticky-head"); limitVisibleRows(wrap, () => [...tbody.rows], maxRows); } return wrap; } export function text(value: unknown): HTMLElement { const span = document.createElement("span"); span.textContent = value == null || value === "" ? "-" : String(value); return span; } export function actionPair(approve: () => void, reject: () => void): HTMLElement { const row = document.createElement("div"); row.className = "ui-general-block__actions b01-dashboard__actions"; row.append( createButton({ label: L("B01_Dashboard_Approve"), variant: "ghost", onClick: approve }), createButton({ label: L("B01_Dashboard_Reject"), variant: "danger", onClick: reject }), ); return row; }