- 새 부품 파일: ui_template_table · setting_row · drop · createRowMenu(context_menu) · 모달 폭 단 · askConfirm · askText(window.prompt 대신) · confirmPanel - 단추 = 552-2 단추 부품(⋯ 아이콘 단추 28 · 띠 알림 닫기 20 · 놓기 칸 [파일 고르기] 28) - 대시보드 PageKit 의 dataTable · tableShell · tableRow · rowMenu · settingRow · settingCard · valueText = 부품을 다시 내보냄(한 벌 · 부름 꼴 그대로) · B01 옛 CSS 걷음 - showConfirmDialog 는 모달 부품으로 옮기고 본체가 다시 내보냄 · 모달 가림막 옛 보라 → --color-scrim - 시험: test_552_3_* 다섯 더함 · 옮긴 함수 자리를 읽던 test_290_1 · test_505_2 고침 · 부품 CSS import 를 못 읽던 시험 도우미 넷(130 · 153 · 280_5 · 311)이 .css 를 건너뜀 - 전후 촬영: B01 여덟 화면 자리 같음 · ⋯ 아이콘만 1 px 왼쪽(단추 부품 가운데 맞춤) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
154 lines
5.0 KiB
TypeScript
154 lines
5.0 KiB
TypeScript
/* =============================================================================
|
|
* B01_Dashboard_UI_PageKit.ts
|
|
* 대시보드 세부 페이지 공용 조각(549) — 페이지 머리 · 표 · 줄 끝 ⋯ · 배지 · 탭 · 빈 상태.
|
|
* 색 · 간격 = 셸 변수(`--b01-*`) 만 · 모양은 `B01_Dashboard_UI_Style_Pages.css`.
|
|
* 아이콘 = `ui_template_icons`(552-2 에 여섯을 옮김).
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createTag } from "@ui/ui_template_elements";
|
|
import { createEmpty } from "@ui/ui_template_empty";
|
|
import { icon, type IconName } from "@ui/ui_template_icons";
|
|
import type { BadgeTone } from "./logic/B01_Dashboard_Logic_Pages";
|
|
import "./B01_Dashboard_UI_Style_Pages.css";
|
|
|
|
/** 아이콘 여섯 — 552-2 에 `ui_template_icons` 로 옮김(같은 Tabler 선) · 이름 · 기본 크기 16 그대로 */
|
|
export type KitIconName = Extract<
|
|
IconName,
|
|
"dots" | "plus" | "pencil" | "upload" | "mail" | "inbox"
|
|
>;
|
|
|
|
export function kitIcon(name: KitIconName, size = 16): HTMLSpanElement {
|
|
return icon(name, size);
|
|
}
|
|
|
|
function node<K extends keyof HTMLElementTagNameMap>(
|
|
tag: K,
|
|
className: string,
|
|
text?: string,
|
|
): HTMLElementTagNameMap[K] {
|
|
const n = document.createElement(tag);
|
|
n.className = className;
|
|
if (text !== undefined) n.textContent = text;
|
|
return n;
|
|
}
|
|
|
|
/** 배지 — 한글 글 + 색 */
|
|
export function badge(text: string, tone: BadgeTone = "neutral"): HTMLElement {
|
|
const tag = createTag(text, tone);
|
|
tag.classList.add("b01-pg-badge");
|
|
return tag;
|
|
}
|
|
|
|
/** 주 단추 — 오른쪽 위 하나 · 앞 아이콘 */
|
|
export function primaryButton(label: string, onClick: () => void, name: KitIconName = "plus") {
|
|
return createButton({ label, onClick, iconStart: kitIcon(name) });
|
|
}
|
|
|
|
export interface PageFrame {
|
|
root: HTMLElement;
|
|
/** 머리 아래 몸통 */
|
|
body: HTMLElement;
|
|
/** 머리 오른쪽 주 단추 자리 */
|
|
setAction: (action: HTMLElement | null) => void;
|
|
}
|
|
|
|
/** 페이지 제목 + 한 줄 설명 + 오른쪽 위 주 단추 */
|
|
export function pageFrame(host: HTMLElement, title: string, desc: string): PageFrame {
|
|
const root = node("div", "b01-pg");
|
|
const head = node("header", "b01-pg__head");
|
|
const text = node("div", "b01-pg__title-wrap");
|
|
text.append(node("h2", "b01-pg__title", title), node("p", "b01-pg__desc", desc));
|
|
const slot = node("div", "b01-pg__action");
|
|
head.append(text, slot);
|
|
const body = node("div", "b01-pg__body");
|
|
root.append(head, body);
|
|
host.append(root);
|
|
return {
|
|
root,
|
|
body,
|
|
setAction: (action) => slot.replaceChildren(...(action ? [action] : [])),
|
|
};
|
|
}
|
|
|
|
// 부품(552-3) — 표 · 줄 끝 ⋯ · 설정 줄 = ui_template 부품 · 부름 꼴 그대로
|
|
export { createRowMenu as rowMenu, type RowMenuItem } from "@ui/ui_template_context_menu";
|
|
export {
|
|
createTable as dataTable,
|
|
tableRow,
|
|
tableShell,
|
|
type Cell,
|
|
type Column,
|
|
} from "@ui/ui_template_table";
|
|
export { settingCard, settingRow, valueText } from "@ui/ui_template_setting_row";
|
|
|
|
export interface EmptyOptions {
|
|
icon: IconName | KitIconName;
|
|
text: string;
|
|
hint?: string;
|
|
action?: HTMLElement;
|
|
/** 가이드 링크 — 누르면 */
|
|
onGuide?: () => void;
|
|
}
|
|
|
|
/** 빈 상태 — 부품 「빈 상태」(ui_template_empty.ts) */
|
|
export function emptyState(opts: EmptyOptions): HTMLElement {
|
|
return createEmpty(opts);
|
|
}
|
|
|
|
export interface TabsHandle {
|
|
root: HTMLElement;
|
|
setBadge: (key: string, count: number) => void;
|
|
}
|
|
|
|
/** 탭 줄 — 눌러 바꿈 · 탭 오른쪽 수 배지(0 이면 숨김) */
|
|
export function tabBar(
|
|
tabs: { key: string; label: string }[],
|
|
initial: string,
|
|
onChange: (key: string) => void,
|
|
): TabsHandle {
|
|
const root = node("div", "b01-pg-tabs");
|
|
root.setAttribute("role", "tablist");
|
|
const badges = new Map<string, HTMLElement>();
|
|
const buttons = new Map<string, HTMLButtonElement>();
|
|
const select = (key: string): void => {
|
|
for (const [k, b] of buttons) {
|
|
b.classList.toggle("is-active", k === key);
|
|
b.setAttribute("aria-selected", String(k === key));
|
|
}
|
|
};
|
|
for (const tab of tabs) {
|
|
const b = node("button", "b01-pg-tabs__tab");
|
|
b.type = "button";
|
|
b.setAttribute("role", "tab");
|
|
b.dataset.tab = tab.key;
|
|
const count = node("span", "b01-pg-tabs__count");
|
|
count.hidden = true;
|
|
b.append(node("span", "", tab.label), count);
|
|
b.addEventListener("click", () => {
|
|
select(tab.key);
|
|
onChange(tab.key);
|
|
});
|
|
buttons.set(tab.key, b);
|
|
badges.set(tab.key, count);
|
|
root.append(b);
|
|
}
|
|
select(initial);
|
|
return {
|
|
root,
|
|
setBadge: (key, n) => {
|
|
const c = badges.get(key);
|
|
if (!c) return;
|
|
c.textContent = String(n);
|
|
c.hidden = n <= 0;
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 줄 첫 칸 — 이름 + (작은 글) 보조 */
|
|
export function nameCell(name: string, sub?: string | null): HTMLElement {
|
|
const cell = node("div", "b01-pg-name");
|
|
cell.append(node("span", "b01-pg-name__main", name));
|
|
if (sub) cell.append(node("span", "b01-pg-name__sub", sub));
|
|
return cell;
|
|
}
|