diff --git a/M01_MasterData/M01_MasterData_UI_Cells.ts b/M01_MasterData/M01_MasterData_UI_Cells.ts index 65915c46e..892238baa 100644 --- a/M01_MasterData/M01_MasterData_UI_Cells.ts +++ b/M01_MasterData/M01_MasterData_UI_Cells.ts @@ -107,7 +107,7 @@ export function buildPager( prev.addEventListener("click", () => goto(page - 1)); next.addEventListener("click", () => goto(page + 1)); return el("div", { - className: "m01-master__pager", + className: "ui-toolbar m01-master__pager", children: [ el("span", { text: `${page} / ${last} ${L("M01_Page")} · ${total.toLocaleString("ko-KR")} ${L("M01_Rows")}`, diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index 123b3ec9d..f8cd1b47b 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -56,6 +56,7 @@ import { buildHeadBar } from "./M01_MasterData_UI_Head"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model"; import "./M01_MasterData_UI_Logic_Style.css"; +import { createEmpty } from "@ui/ui_template_empty"; /** 저장 안 한 로직 하나 — origKey null = 새 로직 · row null = 지움 */ interface Draft { @@ -129,12 +130,15 @@ export async function mountM01LogicLab( const editor = el("div", { className: "m01-logic__editor" }); // 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 둘(흐름도 · 원문 · 하나만 열림 · 폭 공유) const panes = buildLogicSide(editor); - const errors = el("div", { className: "m01-logic__reasons", attrs: { hidden: "" } }); + const errors = el("div", { + className: "ui-banner ui-banner--danger m01-logic__reasons", + attrs: { hidden: "" }, + }); const calc = el("div", { className: "m01lab__calc" }); const filterHost = el("div"); - const waiting = el("p", { className: "m01-logic__muted", text: tx("Loading") }); + const waiting = el("p", { className: "ui-text ui-text--muted", text: tx("Loading") }); let loaded = false; // 첫 목록이 오기 전에는 「0 줄」 빈 표를 보이지 않음 - const pending = el("span", { className: "m01-logic__muted" }); + const pending = el("span", { className: "ui-text ui-text--muted" }); const list = buildList( (item) => void open(item.id, item.sub, item.key), (item, alias) => void onAlias(item, alias), @@ -235,9 +239,7 @@ export async function mountM01LogicLab( back.hidden = listing; if (!opened) return; if (!opened.row) { - editor.replaceChildren( - el("p", { className: "m01-logic__empty", text: `● ${tx("List_Deleted")}` }), - ); + editor.replaceChildren(createEmpty({ text: `● ${tx("List_Deleted")}`, small: true })); return; } const current = opened; @@ -321,7 +323,7 @@ export async function mountM01LogicLab( made.setExamples(o.values); // 띄우는 사이에 온 자동값까지 } catch (e) { panes.flowSplit.right.replaceChildren( - el("p", { className: "m01-logic__muted", text: tf("Failed", String(e)) }), + el("p", { className: "ui-text ui-text--muted", text: tf("Failed", String(e)) }), ); } }; diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Basic.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Basic.ts index e4783675e..c5234cc99 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Basic.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Basic.ts @@ -53,7 +53,7 @@ export function askBasicInfo(units: string[]): Promise { compact: true, }); const source = createInputField({ label: tb("Source"), value: "자체", compact: true }); - const why = el("p", { className: "m01-logic__muted", attrs: { role: "alert" } }); + const why = el("p", { className: "ui-text ui-text--muted", attrs: { role: "alert" } }); const modal = openModal({ title: tb("Title"), closeLabel: tb("Close"), @@ -84,7 +84,7 @@ export function askBasicInfo(units: string[]): Promise { source.root, why, el("div", { - className: "m01-logic__bar", + className: "ui-toolbar m01-logic__bar", children: [ createButton({ label: tb("Ok"), size: "sm", onClick: ok }), createButton({ diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Copy.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Copy.ts index 3f0ef15eb..6a8a981fa 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Copy.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Copy.ts @@ -31,6 +31,9 @@ import { fetchTables, type TableHead } from "./M01_MasterData_Api_Fetch"; import { tx } from "./M01_MasterData_UI_Logic_Text"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; import "./M01_MasterData_UI_LogicLab_Style.css"; +import { createBadge } from "@ui/ui_template_badge"; +import { createText } from "@ui/ui_template_text"; +import { openModal } from "@ui/ui_template_modal"; const OWNERS = ["현장", "공용"] as const; const TABLE_GROUPS = ["소요량", "계수"] as const; @@ -78,29 +81,16 @@ function swapKey(raw: string, newKey: string): string { /** 표 바꾸기 모달 — 소요량·계수 두 그룹을 함께 찾음 */ function openTablePick(onPick: (table: TableHead) => void): void { - const close = (): void => backdrop.remove(); const search = createInputField({ type: "search", placeholder: tl("Copy_PickSearch") }); const list = el("div", { className: "m01-logic__pick-list" }); - const dialog = el("div", { - className: "m01-logic__pick ui-popup", - attrs: { role: "dialog", "aria-label": tl("Copy_PickTitle") }, - children: [ - el("div", { - className: "m01lab__modal-top", - children: [ - el("h3", { text: tl("Copy_PickTitle") }), - createButton({ label: tl("Modal_Close"), variant: "ghost", onClick: close }), - ], - }), - search.root, - list, - ], + // 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정 · 찾기 칸에 커서 + const { close } = openModal({ + title: tl("Copy_PickTitle"), + closeLabel: tl("Modal_Close"), + dialogClass: "m01-logic__pick", + backdropClass: "m01-logic__backdrop", + mount: (body) => body.append(search.root, list), }); - const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] }); - backdrop.addEventListener("click", (ev) => ev.target === backdrop && close()); - backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close()); - document.body.append(backdrop); - search.input.focus(); let timer: number | undefined; const run = async (): Promise => { @@ -113,11 +103,11 @@ function openTablePick(onPick: (table: TableHead) => void): void { ...(hits.length ? hits.map((t) => { const b = el("button", { - className: "m01-logic__pick-row m01-logic__pick-row--button", - attrs: { type: "button" }, + className: "m01-logic__pick-row ui-list-row ui-sidebar-row", + attrs: { type: "button", "data-size": "32" }, children: [ el("span", { text: `${t.이름 || t.키} · ${t.원문번호 || ""}` }), - el("span", { className: "m01-logic__muted", text: t.키 }), + el("span", { className: "ui-text ui-text--muted", text: t.키 }), ], }); b.addEventListener("click", () => { @@ -126,7 +116,7 @@ function openTablePick(onPick: (table: TableHead) => void): void { }); return b; }) - : [el("p", { className: "m01-logic__muted", text: tl("Copy_PickNone") })]), + : [el("p", { className: "ui-text ui-text--muted", text: tl("Copy_PickNone") })]), ); }; search.input.addEventListener("input", () => { @@ -148,7 +138,7 @@ function aliasRow(a: FormulaAlias, onSwap: (raw: string) => void, sub: HTMLEleme className: "m01-logic__section-head", children: [ el("strong", { text: a.이름 }), - el("span", { className: "m01-logic__muted", text: about }), + el("span", { className: "ui-text ui-text--muted", text: about }), ], }), el("pre", { className: "m01lab__expr", text: a.원문 }), @@ -172,7 +162,7 @@ function logicAliasControls( ): { root: HTMLElement } { let forked = false; const original = a.원문; - const status = el("span", { className: "m01-logic__muted" }); + const status = el("span", { className: "ui-text ui-text--muted" }); const refBtn = createButton({ label: tl("Copy_Alias_Ref"), variant: "ghost", @@ -224,8 +214,10 @@ function lineBox(row: LogicRow, line: FormulaLine, onChange: () => void): HTMLEl className: "m01-logic__section-head", children: [ el("strong", { text: line.이름 || line.자리 }), - el("span", { className: "m01-logic__tag", text: tag }), - ...(line.단위 ? [el("span", { className: "m01-logic__muted", text: line.단위 })] : []), + createBadge(tag, "accent"), + ...(line.단위 + ? [el("span", { className: "ui-text ui-text--muted", text: line.단위 })] + : []), ], }), box, @@ -287,9 +279,7 @@ export async function openCopyScreen(opt: CopyOptions): Promise { onLines: () => undefined, }); } catch (error) { - calcHost.replaceChildren( - el("p", { className: "m01-logic__bad", text: error instanceof Error ? error.message : "" }), - ); + calcHost.replaceChildren(createText(error instanceof Error ? error.message : "", "error")); } }; @@ -316,7 +306,7 @@ export async function openCopyScreen(opt: CopyOptions): Promise { drawAliases(); refreshCalc(); - const close = (): void => backdrop.remove(); + let close = (): void => {}; const onSave = async (): Promise => { showLoadingOverlay(); try { @@ -337,55 +327,48 @@ export async function openCopyScreen(opt: CopyOptions): Promise { } }; - const sheet = el("div", { - className: "m01-logic__pick m01copy__sheet", - attrs: { role: "dialog", "aria-label": tl("Copy_Title") }, - children: [ - el("div", { - className: "m01lab__modal-top", - children: [ - el("h3", { text: `${tl("Copy_Title")} — ${row.이름}(${opt.key})` }), - createButton({ label: tl("Copy_Cancel"), variant: "ghost", onClick: close }), - ], - }), - el("div", { - className: "m01copy__head", - children: [nameField.root, ownerField.root], - }), - el("section", { - className: "m01-logic__section", - children: [el("h3", { text: tl("Copy_Aliases") }), aliasHost], - }), - el("section", { - className: "m01-logic__section", - children: [ - el("div", { - className: "m01-logic__section-head", - children: [ - el("h3", { text: tl("Copy_Lines") }), - createButton({ - label: tl("Copy_AddExtra"), - variant: "ghost", - onClick: () => addExtra(row, refreshCalc, drawLines), - }), - ], - }), - linesHost, - ], - }), - el("section", { className: "m01-logic__section", children: [calcHost] }), - el("div", { - className: "m01-logic__pick-bar", - children: [ - el("span", {}), - el("span", {}), - createButton({ label: tl("Copy_Save"), onClick: () => void onSave() }), - ], - }), - ], - }); - const backdrop = el("div", { className: "m01-logic__backdrop", children: [sheet] }); - backdrop.addEventListener("click", (ev) => ev.target === backdrop && close()); - backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close()); - document.body.append(backdrop); + // 모달 부품 — 제목줄 [취소] · 바깥 누르기 · Esc · 위 고정 · 폭만 복사 시트 곁값 + ({ close } = openModal({ + title: `${tl("Copy_Title")} — ${row.이름}(${opt.key})`, + closeLabel: tl("Copy_Cancel"), + dialogClass: "m01-logic__pick m01copy__sheet", + backdropClass: "m01-logic__backdrop", + mount: (body) => + body.append( + el("div", { + className: "m01copy__head", + children: [nameField.root, ownerField.root], + }), + el("section", { + className: "m01-logic__section", + children: [el("h3", { text: tl("Copy_Aliases") }), aliasHost], + }), + el("section", { + className: "m01-logic__section", + children: [ + el("div", { + className: "m01-logic__section-head", + children: [ + el("h3", { text: tl("Copy_Lines") }), + createButton({ + label: tl("Copy_AddExtra"), + variant: "ghost", + onClick: () => addExtra(row, refreshCalc, drawLines), + }), + ], + }), + linesHost, + ], + }), + el("section", { className: "m01-logic__section", children: [calcHost] }), + el("div", { + className: "m01-logic__pick-bar", + children: [ + el("span", {}), + el("span", {}), + createButton({ label: tl("Copy_Save"), onClick: () => void onSave() }), + ], + }), + ), + })); } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts index 8cbac7323..15b202ee0 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts @@ -35,6 +35,7 @@ import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill"; import { tl, type LabTextKey } from "./M01_MasterData_UI_LogicLab_Text"; import "./M01_MasterData_UI_LogicLab_Style.css"; import "./M01_MasterData_UI_LogicLab_Pill.css"; +import { createBadge } from "@ui/ui_template_badge"; /** 서버 조각(`/text` 줄의 `조각`) → 수량 칸 조각 — 요소·표·로직은 알약(글 = 조각 글) · 나머지는 글자 */ function toFragments(pieces: TextPiece[], ctx: DetailContext): Fragment[] { @@ -131,7 +132,7 @@ const section = (title: string, body: HTMLElement[], hint = ""): HTMLElement => className: "m01-logic__section-head", children: [ el("h3", { text: title }), - ...(hint ? [el("span", { className: "m01-logic__muted", text: hint })] : []), + ...(hint ? [el("span", { className: "ui-text ui-text--muted", text: hint })] : []), ], }), ...body, @@ -151,14 +152,14 @@ function infoBox(ctx: DetailContext): HTMLElement { }); return el("label", { className: "m01lab__cell", - children: [el("span", { className: "m01-logic__muted", text: label }), box], + children: [el("span", { className: "ui-text ui-text--muted", text: label }), box], }); }; const cell = (label: string, value: string): HTMLElement => el("div", { className: "m01lab__cell", children: [ - el("span", { className: "m01-logic__muted", text: label }), + el("span", { className: "ui-text ui-text--muted", text: label }), el("span", { text: value || "—" }), ], }); @@ -172,7 +173,7 @@ function infoBox(ctx: DetailContext): HTMLElement { const reasons = ctx.reasons.length ? [ el("div", { - className: "m01-logic__reasons", + className: "ui-banner ui-banner--danger m01-logic__reasons", children: [ el("strong", { text: tx("Head_Blocked") }), ...ctx.reasons.map((r) => el("div", { text: safeReason(r) })), @@ -182,7 +183,7 @@ function infoBox(ctx: DetailContext): HTMLElement { : []; return section(tl("Info_Title"), [ el("p", { - className: "m01-logic__muted", + className: "ui-text ui-text--muted", text: tl(own ? "Info_Own" : ctx.admin ? "Info_Base" : "Info_ReadOnly"), }), ...reasons, @@ -198,7 +199,10 @@ function infoBox(ctx: DetailContext): HTMLElement { cell(tx("Head_Owner"), row.소유 ?? ""), el("label", { className: "m01lab__cell m01lab__cell--wide", - children: [el("span", { className: "m01-logic__muted", text: tx("Head_Note") }), note], + children: [ + el("span", { className: "ui-text ui-text--muted", text: tx("Head_Note") }), + note, + ], }), ], }), @@ -342,27 +346,15 @@ function lineRow(e: Entry): HTMLElement { el("td", { children: [ el("div", { text: e.name }), - ...(e.share - ? [el("div", { className: "m01-logic__tag", text: `${e.share} ${tl("Share")}` })] - : []), - ...(e.spec ? [el("div", { className: "m01-logic__muted", text: e.spec })] : []), - ...(e.warn - ? [ - el("div", { - className: "m01-logic__tag m01lab__warn", - text: `${tx("Calc_UnitWarn")} · ${e.warn}`, - }), - ] - : []), + ...(e.share ? [createBadge(`${e.share} ${tl("Share")}`, "accent")] : []), + ...(e.spec ? [el("div", { className: "ui-text ui-text--muted", text: e.spec })] : []), + ...(e.warn ? [createBadge(`${tx("Calc_UnitWarn")} · ${e.warn}`, "warning")] : []), ], }), cellOf(e.unit), el("td", { className: "m01lab__qty-cell", - children: [ - el("span", { className: "m01-logic__tag", text: e.qtyTag }), - fragmentRow(e.qtyFrag), - ], + children: [createBadge(e.qtyTag, "accent"), fragmentRow(e.qtyFrag)], }), cellOf(e.price, "m01-logic__money"), cellOf(e.amount === null ? "" : formatMoney(e.amount), "m01-logic__money"), @@ -379,7 +371,7 @@ function groupTable(group: Group, mine: Entry[], calculated: boolean): HTMLEleme children: [ el("td", { attrs: { colspan: "5" }, - className: "m01-logic__muted", + className: "ui-text ui-text--muted", text: tl("Empty_Group"), }), ], @@ -405,7 +397,7 @@ function groupTable(group: Group, mine: Entry[], calculated: boolean): HTMLEleme className: "m01-logic__scroll", children: [ el("table", { - className: "m01-logic__grid m01lab__ho", + className: "ui-table ui-table--data m01-logic__grid m01lab__ho", children: [ el("thead", { children: [el("tr", { children: headers.map((h) => el("th", { text: h })) })], @@ -447,7 +439,7 @@ function hoBox(ctx: DetailContext): HTMLElement { const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원"); if (!money) { return section(tl("Ho_Title"), [ - el("p", { className: "m01-logic__muted", text: tl("Not_Money") }), + el("p", { className: "ui-text ui-text--muted", text: tl("Not_Money") }), el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }), ]); } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.css b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.css index 4a4f282e8..505f077b5 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.css @@ -22,9 +22,3 @@ color: var(--color-text-body); font-weight: 600; } - -.m01flow__warn { - margin: var(--spacing-4) 0 0; - color: var(--color-danger); - font-size: var(--text-body-sm); -} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts index 6b4acb35c..f4eb8eb86 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts @@ -34,6 +34,7 @@ import { type FlowGroupKey, } from "./M01_MasterData_UI_LogicLab_FlowRows"; import "./M01_MasterData_UI_LogicLab_FlowView.css"; +import { createText } from "@ui/ui_template_text"; const TEXT = { Detail: ["단가산출 상세", "Unit-price detail"], @@ -91,7 +92,7 @@ const groupTable = (g: FlowGroup): HTMLElement => { className: "m01-logic__scroll", children: [ el("table", { - className: "m01-logic__grid", + className: "ui-table ui-table--data m01-logic__grid", children: [ el("thead", { children: [ @@ -159,7 +160,7 @@ export function buildFlowView( ev = opts.result ?? evalLogic(lg, values, { logics: opts.logics }); } catch (e) { host.replaceChildren( - section(tf("Detail"), [el("p", { className: "m01-logic__muted", text: String(e) })]), + section(tf("Detail"), [el("p", { className: "ui-text ui-text--muted", text: String(e) })]), ); return; } @@ -168,17 +169,17 @@ export function buildFlowView( const detail = section(tf("Detail"), [ ...(rows.groups.length ? rows.groups.map(groupTable) - : [el("p", { className: "m01-logic__muted", text: tf("None") })]), + : [el("p", { className: "ui-text ui-text--muted", text: tf("None") })]), el("p", { className: "m01lab__total", attrs: { "data-sum": String(rows.total) }, text: `${tf("Total")} ${rows.totalText}`, }), el("p", { - className: "m01-logic__muted", + className: "ui-text ui-text--muted", text: `${tf("TotalNote")}: ${roundText(totalRound)}`, }), - ...rows.errors.slice(0, 3).map((t) => el("p", { className: "m01flow__warn", text: t })), + ...rows.errors.slice(0, 3).map((t) => createText(t, "error", 14)), ]); /** 함수 상자마다 식 두 줄(sub2 식 보기 · 받는 알약 값을 칸에 넣고 셈) — 못 그리면 이름 = 값만 */ const linesOf = (id: string): FormulaLine[] | null => { @@ -193,7 +194,7 @@ export function buildFlowView( const bodies = rows.funcs.map((f) => funcBlock(f, linesOf(f.box))); const text = section( tf("Formula"), - bodies.length ? bodies : [el("p", { className: "m01-logic__muted", text: tf("NoFunc") })], + bodies.length ? bodies : [el("p", { className: "ui-text ui-text--muted", text: tf("NoFunc") })], ); host.replaceChildren(detail, text); } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts index 9adccc09f..bec7c9a5d 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts @@ -19,7 +19,7 @@ export interface FormulaContext { text: PreviewTextAnswer | null; } -const muted = (text: string): HTMLElement => el("p", { className: "m01-logic__muted", text }); +const muted = (text: string): HTMLElement => el("p", { className: "ui-text ui-text--muted", text }); export function buildFormula(host: HTMLElement, ctx: FormulaContext): void { const text = ctx.text; @@ -30,7 +30,7 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void { if (!text.ok) { host.replaceChildren( el("div", { - className: "m01-logic__reasons", + className: "ui-banner ui-banner--danger m01-logic__reasons", children: [ el("strong", { text: tx("Calc_Stopped") }), el("div", { text: safeReason(text.reason) }), @@ -65,10 +65,15 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void { ? [ el("div", { text: `${line.이름} : ${line.글}` }), ...(line.까닭 - ? [el("div", { className: "m01-logic__muted", text: safeReason(line.까닭) })] + ? [ + el("div", { + className: "ui-text ui-text--muted", + text: safeReason(line.까닭), + }), + ] : []), ] - : [el("div", { className: "m01-logic__muted", text: tl("Formula_Slot") })], + : [el("div", { className: "ui-text ui-text--muted", text: tl("Formula_Slot") })], }), ], }); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts index 3846bca1a..161995cd6 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts @@ -21,6 +21,7 @@ import { explain, loadTable, type TableRow } from "./M01_MasterData_UI_Logic_Not import { jumpToMaster, type MasterJump } from "./M01_MasterData_UI_Side"; import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; +import { createText } from "@ui/ui_template_text"; /** 로직 알약 → 그 로직 열기 — 로직 화면이 자기 열기를 등록 · 열려 있는 모달은 다 닫음 */ let logicOpener: (key: string) => void = () => {}; @@ -138,11 +139,11 @@ function tableView(table: TableRow, target: TableTarget): HTMLElement { children: [ el("strong", { text: `${table.원문번호 ?? ""} ${table.이름 ?? table.키} · ${table.키}` }), el("p", { - className: "m01-logic__muted", + className: "ui-text ui-text--muted", text: [`${tl("Modal_Source")} ${table.출처 ?? ""}`, use].filter(Boolean).join(" · "), }), el("p", { - className: "m01-logic__muted", + className: "ui-text ui-text--muted", text: hit ? tl("Modal_Hit") : target.col ? tl("Modal_ColOnly") : tl("Modal_NoHit"), }), goButton({ kind: "table", key: table.키 }), @@ -150,7 +151,7 @@ function tableView(table: TableRow, target: TableTarget): HTMLElement { className: "m01-logic__scroll", children: [ el("table", { - className: "m01-logic__grid m01lab__table", + className: "ui-table ui-table--data m01-logic__grid m01lab__table", children: [ el("thead", { children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })], @@ -217,7 +218,7 @@ function openDialog(title: string, mount: (body: HTMLElement) => void): void { }); } -const muted = (text: string): HTMLElement => el("p", { className: "m01-logic__muted", text }); +const muted = (text: string): HTMLElement => el("p", { className: "ui-text ui-text--muted", text }); /** 재료 고르기 줄 후보 목록 — 잡힌 품목(`pickedRef`)을 노란 줄로 · 후보가 100건을 넘으면 앞 100건만 옴 */ function candidateView( @@ -249,7 +250,7 @@ function candidateView( className: "m01-logic__scroll", children: [ el("table", { - className: "m01-logic__grid m01lab__table", + className: "ui-table ui-table--data m01-logic__grid m01lab__table", children: [ el("thead", { children: [ @@ -421,12 +422,10 @@ export function openNoteModal(title: string, text: string): void { /** 수량 칸 표 알약을 눌렀을 때 — 그 표만 보임(걸린 줄 강조) */ export function openTableModal(target: TableTarget): void { openDialog(target.title, (body) => { - body.replaceChildren(el("p", { className: "m01-logic__muted", text: "…" })); + body.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: "…" })); void loadTable(target.table).then((table) => { body.replaceChildren( - table - ? tableView(table, target) - : el("p", { className: "m01-logic__bad", text: `${target.table} ?` }), + table ? tableView(table, target) : createText(`${target.table} ?`, "error"), ); }); }); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.css index 657f6bbc9..b919e6967 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.css @@ -1,39 +1,9 @@ -/* M01 로직 개선 시험 — 줄 모달의 알약 단추 */ -.m01lab__pills { - display: flex; - flex-direction: column; - gap: var(--spacing-8); - min-width: 0; -} - -.m01lab__pill-row { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-8); -} - +/* 알약 = 단추 부품(보조 · 24) · 여기는 칸 폭 넘침만 */ .m01lab__pill { - padding: var(--spacing-4) var(--spacing-12); - border: 1px solid var(--color-border); - border-radius: 999px; - background: var(--color-bg); - color: var(--color-text); - font: inherit; - cursor: pointer; max-width: 100%; overflow-wrap: anywhere; } -.m01lab__pill.is-open { - background: var(--color-accent); - border-color: var(--color-accent); - color: var(--color-bg); -} - -.m01lab__pill-stage { - min-width: 0; -} - /* 계산 안 된 만약() 가지 조각 — 흐리게 */ .m01lab__qty-dim { opacity: 0.45; diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.ts index 31904049e..c359d2134 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.ts @@ -6,6 +6,7 @@ * ========================================================================== */ import { el } from "@ui/ui_template_elements"; +import { createButton } from "@ui/ui_template_button"; export interface Pill { /** 알약 글 — 보기 「표 QF000421」 */ @@ -16,11 +17,10 @@ export interface Pill { /** 알약 하나 — 글자 조각 사이에 끼워 쓰는 단추 */ export function pillButton(p: Pill): HTMLButtonElement { - const b = el("button", { - className: "m01lab__pill", - text: p.label, - attrs: { type: "button", "data-pill": p.label }, - }); + // 알약 = 단추 부품(보조 · 24) — data-pill 은 시험 · 촬영이 잡음 + const b = createButton({ label: p.label, variant: "ghost", size: 24 }); + b.classList.add("m01lab__pill"); + b.dataset.pill = p.label; b.addEventListener("click", (ev) => { ev.stopPropagation(); // 줄 전체 누르기(행 모달)와 안 겹치게 p.onClick(); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts index b7ceac66e..399283989 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts @@ -51,7 +51,7 @@ function rawNode(b: Block): HTMLElement { className: "m01-logic__scroll", children: [ el("table", { - className: "m01-logic__grid m01lab-src__table", + className: "ui-table ui-table--data m01-logic__grid m01lab-src__table", children: [ el("tbody", { children: b.table.map((r) => @@ -104,11 +104,11 @@ export function createSourceView(opts: SourceViewOptions = {}): SourceView { download.hidden = !book; download.href = book ? `${API_BASE_URL}${opts.pdf}?${new URLSearchParams({ book })}` : ""; } - body.replaceChildren(el("p", { className: "m01-logic__muted", text: ts("Loading") })); + body.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: ts("Loading") })); const none = (): void => body.replaceChildren( el("p", { className: "m01lab-src__none", text: ts("None") }), - el("p", { className: "m01-logic__muted", text: `${ts("From")} · ${src || "—"}` }), + el("p", { className: "ui-text ui-text--muted", text: `${ts("From")} · ${src || "—"}` }), ); if (!src.trim()) return none(); void fetch( @@ -125,7 +125,7 @@ export function createSourceView(opts: SourceViewOptions = {}): SourceView { body.replaceChildren( ...found.flatMap((f) => [ el("p", { - className: "m01-logic__muted m01lab-src__file", + className: "ui-text ui-text--muted m01lab-src__file", text: [ f.file, f.matched === f.number ? "" : ts("Near", f.number), diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css index ee46c0348..41e4c4ad9 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css @@ -116,15 +116,7 @@ overflow: auto; } -.m01lab__modal-top { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing-8); -} - -.m01lab__modal-body, -.m01lab__tables { +.m01lab__modal-body { display: flex; flex-direction: column; gap: var(--spacing-8); @@ -157,7 +149,7 @@ } /* 복사해서 만들기(3-4) — 별칭 꼴 식 시트 */ -.m01copy__sheet { +.ui-modal.m01copy__sheet { width: min(1040px, 94vw); --modal-max: 90vh; } @@ -177,7 +169,7 @@ } /* 시험 계산 — 재료 · 기계 고르기 표 모달 */ -.m01-logic__pick--table { +.ui-modal.m01-logic__pick--table { width: min(1040px, 94vw); } @@ -193,7 +185,3 @@ .m01lab__pickrow.is-picked { font-weight: 600; } - -.m01lab__warn { - color: var(--color-warning, #b45309); -} diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts index b5b2da676..b97e032be 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts @@ -27,6 +27,7 @@ import { import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; import { formatMoney } from "./M01_MasterData_UI_Logic_Money"; import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text"; +import { createBadge } from "@ui/ui_template_badge"; /** 고르기 값이 마스터 키(자재·기계·직종)면 이름으로 바꿔 보임 — 값 칸은 키 그대로 보냄 */ const KEY_RE = /^[A-Z]{2}\d{6}$/; @@ -197,7 +198,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void { el("h3", { text: tx("Calc_Title") }), ...(fields.length || materials.length ? [el("div", { className: "m01-logic__head", children: [...fields, ...materials] })] // 요소 표가 쓰는 auto-fill 그리드 재사용 — 2열 - : [el("p", { className: "m01-logic__muted", text: tx("Calc_NoInputs") })]), + : [el("p", { className: "ui-text ui-text--muted", text: tx("Calc_NoInputs") })]), runButton, out, ); @@ -205,12 +206,14 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void { /** `money` = 결과가 돈(원) — 그때만 결과 값도 정수 · 수량·비율 결과는 소수 그대로 */ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean): HTMLElement[] { - const note = draft ? [el("p", { className: "m01-logic__muted", text: tx("Calc_Draft") })] : []; + const note = draft + ? [el("p", { className: "ui-text ui-text--muted", text: tx("Calc_Draft") })] + : []; if (!answer.ok) { return [ ...note, el("div", { - className: "m01-logic__reasons", + className: "ui-banner ui-banner--danger m01-logic__reasons", children: [ el("strong", { text: tx("Calc_Stopped") }), el("div", { text: safeReason(answer.reason) }), @@ -231,12 +234,7 @@ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean): : pctView(line).name, }), ...(line.단위경고 - ? [ - el("span", { - className: "m01-logic__tag m01lab__warn", - text: `${tx("Calc_UnitWarn")} · ${line.단위경고}`, - }), - ] + ? [createBadge(`${tx("Calc_UnitWarn")} · ${line.단위경고}`, "warning")] : []), ], }), @@ -247,7 +245,7 @@ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean): ); parts.push( el("table", { - className: "m01-logic__grid", + className: "ui-table ui-table--data m01-logic__grid", children: [ el("thead", { children: [ diff --git a/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts b/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts index ebf5111e6..227dc95df 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts @@ -81,7 +81,7 @@ export function materialRows( .map(headWord) .filter((w, k, all) => w && all.indexOf(w) === k); const current = el("span", { - className: "m01-logic__muted", + className: "ui-text ui-text--muted", text: `${nowName || tx("Mat_NoPrice")} `, attrs: { "data-mat-idx": String(i), "data-ref": brief?.ref ?? "" }, }); @@ -141,7 +141,7 @@ export function machineControl( again: () => void, ): HTMLElement | null { if (!isMachineInput(spec)) return null; - const current = el("span", { className: "m01-logic__muted" }); + const current = el("span", { className: "ui-text ui-text--muted" }); const paint = (): void => { const code = values[spec.이름] ?? ""; const known = machines.get(code); diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts b/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts index f39ddf08e..ff4335c98 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Edit.ts @@ -17,6 +17,9 @@ import type { NamedFormula, } from "./M01_MasterData_UI_Logic_Api"; import { tx } from "./M01_MasterData_UI_Logic_Text"; +import { createBadge } from "@ui/ui_template_badge"; +import { createText } from "@ui/ui_template_text"; +import { createIconButton } from "@ui/ui_template_button"; export type PickMode = "element" | "table"; export type PickDone = (item: ElementBrief, column?: string) => void; @@ -81,12 +84,8 @@ function choice( } function dropButton(onClick: () => void): HTMLButtonElement { - const button = el("button", { - className: "m01-logic__drop", - text: "×", - attrs: { type: "button", title: tx("DeleteRow") }, - }); - button.addEventListener("click", onClick); + const button = createIconButton({ glyph: "×", title: tx("DeleteRow"), size: 20, onClick }); + button.classList.add("m01-logic__drop"); return button; } @@ -100,7 +99,7 @@ function section(title: string, body: HTMLElement, onAdd?: () => void): HTMLElem } function grid(headers: string[], rows: HTMLElement[][], className: string): HTMLElement { - const table = el("table", { className: `m01-logic__grid ${className}` }); + const table = el("table", { className: `ui-table ui-table--data m01-logic__grid ${className}` }); table.append( el("thead", { children: [el("tr", { children: headers.map((h) => el("th", { text: h })) })] }), ); @@ -133,7 +132,7 @@ export function buildEditor(host: HTMLElement, ctx: EditContext): void { if (ctx.reasons.length) { host.append( el("div", { - className: "m01-logic__reasons", + className: "ui-banner ui-banner--danger m01-logic__reasons", children: [ el("strong", { text: tx("Head_Blocked") }), ...ctx.reasons.map((r) => el("div", { text: r })), @@ -191,7 +190,7 @@ function head(ctx: EditContext, set: (p: () => void) => void, rerender: () => vo field(tx("Head_File"), file, true), field( tx("Head_Key"), - el("span", { className: "m01-logic__muted", text: row.키 || tx("New_Key") }), + el("span", { className: "ui-text ui-text--muted", text: row.키 || tx("New_Key") }), ), field( tx("Head_Number"), @@ -276,11 +275,11 @@ function priceCell(ctx: EditContext, item: HoLine, i: number): HTMLElement { const line = ctx.lines?.[i]; if (line) return el("span", { className: "m01-logic__money", text: formatNumber(line.단가) }); if (item.종류 === "로직" || item.요소.includes("{")) { - return el("span", { className: "m01-logic__muted", text: tx("Ho_PriceLater") }); + return el("span", { className: "ui-text ui-text--muted", text: tx("Ho_PriceLater") }); } const brief = ctx.prices[item.요소]; - if (brief === undefined) return el("span", { className: "m01-logic__muted", text: "" }); - if (brief === null) return el("span", { className: "m01-logic__bad", text: tx("Ho_Missing") }); + if (brief === undefined) return el("span", { className: "ui-text ui-text--muted", text: "" }); + if (brief === null) return createText(tx("Ho_Missing"), "error"); return el("span", { className: "m01-logic__money", text: formatNumber(brief.값) }); } @@ -314,7 +313,7 @@ function hoSection( set(() => (item.수량 = v)); tag.textContent = qtyKind(v); }); - const tag = el("span", { className: "m01-logic__tag", text: qtyKind(item.수량) }); + const tag = createBadge(qtyKind(item.수량), "accent"); const table = createButton({ label: tx("Ho_PickTable"), variant: "ghost", @@ -339,7 +338,7 @@ function hoSection( className: "m01-logic__stack", children: [ textBox(item.이름 ?? "", (v) => set(() => (item.이름 = v))), - el("span", { className: "m01-logic__muted", text: spec }), + el("span", { className: "ui-text ui-text--muted", text: spec }), ], }), textBox(item.단위 ?? "", (v) => set(() => (item.단위 = v)), "m01-logic__unit"), diff --git a/M01_MasterData/M01_MasterData_UI_Logic_List.ts b/M01_MasterData/M01_MasterData_UI_Logic_List.ts index cd9a4931d..e90dd77ad 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_List.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_List.ts @@ -67,7 +67,7 @@ export function buildList( const pick = { sub: "", detail: "" }; const searchField = createInputField({ type: "search", placeholder: tx("List_Search") }); const search = searchField.input; - const grid = el("div", { className: "m01-master__grid-wrap" }); + const grid = el("div", { className: "ui-table-wrap m01-master__grid-wrap" }); const draw = (): void => { const q = search.value; @@ -89,7 +89,7 @@ export function buildList( pager.replaceWith((pager = buildPager(shown.length, SIZE, page, go))); grid.replaceChildren( el("table", { - className: "m01-master__grid", + className: "ui-table m01-master__grid", children: [ el("thead", { children: [el("tr", { children: COLS.map((c) => el("th", { text: c })) })], diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Note.ts b/M01_MasterData/M01_MasterData_UI_Logic_Note.ts index 612cf0e0d..40e41ed6f 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Note.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Note.ts @@ -226,10 +226,12 @@ function tableView(find: RelatedFind, table: TableRow, ctx: EvalCtx): HTMLElemen : ""; return [ el("p", { - className: "m01c__muted", + className: "ui-text ui-text--muted", text: `${find.source} · ${table.원문번호 ?? table.키} ${table.이름 ?? ""} (${table.출처 ?? ""})`, }), - ...(use ? [el("p", { className: "m01c__muted", text: `${tx("Use_Of")} — ${use}` })] : []), + ...(use + ? [el("p", { className: "ui-text ui-text--muted", text: `${tx("Use_Of")} — ${use}` })] + : []), el("table", { className: "m01c__table", children: [ @@ -252,17 +254,20 @@ export function buildNote(host: HTMLElement, opts: NoteOptions): void { }); const plain = explain(opts.expr); const head: HTMLElement[] = [ - el("p", { className: "m01c__muted", text: tx("Note_Plain") }), + el("p", { className: "ui-text ui-text--muted", text: tx("Note_Plain") }), el("p", { className: "m01c__plain", text: plain }), ]; if (!related.length) { host.replaceChildren(...head, advanced); return; } - const tables = el("div", { className: "m01c__muted", text: `${tx("Note_Searching")}…` }); + const tables = el("div", { + className: "ui-text ui-text--muted", + text: `${tx("Note_Searching")}…`, + }); host.replaceChildren( ...head, - el("p", { className: "m01c__muted", text: tx("Note_Table") }), + el("p", { className: "ui-text ui-text--muted", text: tx("Note_Table") }), tables, advanced, ); @@ -275,7 +280,7 @@ export function buildNote(host: HTMLElement, opts: NoteOptions): void { ? tableView(r, table, opts.ctx) : [ el("p", { - className: "m01c__muted", + className: "ui-text ui-text--muted", text: tx("Note_TableMissing", { v: r.find.table }), }), ], diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts b/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts index 331f59cc6..71cd94649 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Pick.ts @@ -27,6 +27,7 @@ import { } from "./M01_MasterData_UI_Logic_Api"; import { formatNumber, type PickDone, type PickMode } from "./M01_MasterData_UI_Logic_Edit"; import { tx } from "./M01_MasterData_UI_Logic_Text"; +import { openModal } from "@ui/ui_template_modal"; const GROUPS: Record = { element: ["인력", "재료", "기계", "로직"], @@ -43,30 +44,23 @@ export function openPicker(mode: PickMode, group: string, done: PickDone): void const select = groupField.select; const searchField = createInputField({ type: "search", placeholder: tx("Pick_Search") }); const search = searchField.input; - const count = el("span", { className: "m01-logic__muted" }); + const count = el("span", { className: "ui-text ui-text--muted" }); const list = el("div", { className: "m01-logic__pick-list" }); - const close = (): void => backdrop.remove(); - const dialog = el("div", { - className: "m01-logic__pick ui-popup", - attrs: { role: "dialog", "aria-label": tx("Pick_Title") }, - children: [ - el("div", { - className: "m01-logic__section-head", - children: [ - el("h3", { text: tx("Pick_Title") }), - createButton({ label: tx("Pick_Close"), variant: "ghost", onClick: close }), - ], - }), - el("div", { - className: "m01-logic__pick-bar", - children: [groupField.root, searchField.root, count], - }), - list, - ], + // 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정 + const { close } = openModal({ + title: tx("Pick_Title"), + closeLabel: tx("Pick_Close"), + dialogClass: "m01-logic__pick", + backdropClass: "m01-logic__backdrop", + mount: (body) => + body.append( + el("div", { + className: "m01-logic__pick-bar", + children: [groupField.root, searchField.root, count], + }), + list, + ), }); - const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] }); - backdrop.addEventListener("click", (ev) => ev.target === backdrop && close()); - backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close()); const choose = (item: ElementBrief, column?: string): void => { close(); @@ -94,7 +88,6 @@ export function openPicker(mode: PickMode, group: string, done: PickDone): void timer = window.setTimeout(load, 250); }); select.addEventListener("change", load); - document.body.append(backdrop); search.focus(); void load(); } @@ -110,7 +103,7 @@ function itemRow( children: [ el("strong", { text: `${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() }), el("span", { - className: "m01-logic__muted", + className: "ui-text ui-text--muted", text: [item.ref, note].filter(Boolean).join(" · "), }), ], @@ -128,8 +121,8 @@ function itemRow( const unit = typeof item.단위 === "string" ? item.단위 : ""; const value = typeof item.값 === "object" && item.값 !== null ? "{…}" : formatNumber(item.값); const row = el("button", { - className: "m01-logic__pick-row m01-logic__pick-row--button", - attrs: { type: "button" }, + className: "m01-logic__pick-row ui-list-row ui-sidebar-row", + attrs: { type: "button", "data-size": "32" }, children: [ text, el("span", { className: "m01-logic__money", text: `${value} ${unit}`.trim() }), @@ -276,7 +269,7 @@ function swapRow(one: LogicOne, line: HoLine, i: number, swaps: Swaps, onChange: className: "m01-test__pick", children: [ el("strong", { text: `${line.종류} · ${line.이름 ?? String(line.요소)}${spec}` }), - el("span", { className: "m01c__muted", text: `${line.단위 ?? ""}${cost}` }), + el("span", { className: "ui-text ui-text--muted", text: `${line.단위 ?? ""}${cost}` }), subRow, itemBox.root, ], diff --git a/M01_MasterData/M01_MasterData_UI_Logic_PickTable.ts b/M01_MasterData/M01_MasterData_UI_Logic_PickTable.ts index 9315c9640..f3a892d90 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_PickTable.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_PickTable.ts @@ -4,10 +4,11 @@ * 검색 칸을 채워 열고 · 잡힌 품목 칠함 · 한 줄 누르면 고름 · 저장 안 함(읽기만) * ========================================================================== */ -import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; +import { createInputField, el, showToast } from "@ui/ui_template_elements"; import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch"; import { buildPager, flatten, show } from "./M01_MasterData_UI_Cells"; import { tx } from "./M01_MasterData_UI_Logic_Text"; +import { openModal } from "@ui/ui_template_modal"; export type PickTableKind = "재료" | "기계"; @@ -42,7 +43,6 @@ export interface PickTableOptions { } export function openPickTable(opt: PickTableOptions): void { - const close = (): void => back.remove(); const searchField = createInputField({ type: "search", placeholder: tx("Pick_Search"), @@ -50,24 +50,14 @@ export function openPickTable(opt: PickTableOptions): void { }); const search = searchField.input; const host = el("div", { className: "m01-logic__pick-list" }); - const dialog = el("div", { - className: "m01-logic__pick m01-logic__pick--table ui-popup", - attrs: { role: "dialog", "aria-label": opt.title }, - children: [ - el("div", { - className: "m01-logic__section-head", - children: [ - el("h3", { text: opt.title }), - createButton({ label: tx("Pick_Close"), variant: "ghost", onClick: close }), - ], - }), - searchField.root, - host, - ], + // 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정 · 폭만 표 모달 곁값 + const { close } = openModal({ + title: opt.title, + closeLabel: tx("Pick_Close"), + dialogClass: "m01-logic__pick m01-logic__pick--table", + backdropClass: "m01-logic__backdrop", + mount: (body) => body.append(searchField.root, host), }); - const back = el("div", { className: "m01-logic__backdrop", children: [dialog] }); - back.addEventListener("click", (ev) => ev.target === back && close()); - back.addEventListener("keydown", (ev) => ev.key === "Escape" && close()); let page = 1; const tried = new Set([opt.seed]); @@ -109,10 +99,10 @@ export function openPickTable(opt: PickTableOptions): void { void load(); }), el("div", { - className: "m01-master__grid-wrap", + className: "ui-table-wrap m01-master__grid-wrap", children: [ el("table", { - className: "m01-master__grid", + className: "ui-table m01-master__grid", children: [ el("thead", { children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })], @@ -136,7 +126,5 @@ export function openPickTable(opt: PickTableOptions): void { void load(); }, 300); }); - document.body.append(back); - search.focus(); void load(true); } diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Style.css b/M01_MasterData/M01_MasterData_UI_Logic_Style.css index 3aa086b58..009ac1756 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Style.css +++ b/M01_MasterData/M01_MasterData_UI_Logic_Style.css @@ -54,18 +54,6 @@ background: var(--color-surface); } -.m01-logic__filters { - display: flex; - flex-direction: column; - gap: var(--spacing-4); -} - -.m01-logic__check { - display: flex; - align-items: center; - gap: var(--spacing-4); -} - .m01-logic__bar { display: flex; align-items: center; @@ -79,10 +67,6 @@ color: var(--color-text); } -.m01-logic__empty { - color: var(--color-text-muted); -} - .m01-logic__head { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); @@ -156,23 +140,9 @@ input.m01-logic__input { color: var(--color-text); } -.m01-logic__grid { - width: 100%; - border-collapse: collapse; -} - -.m01-logic__grid th, +/* 겉 = 표 부품 데이터표(ui-table--data · 줄 28) · 여기는 위 맞춤만 */ .m01-logic__grid td { - padding: 2px 4px; - border-bottom: 1px solid var(--color-border); vertical-align: top; - text-align: left; -} - -.m01-logic__grid th { - color: var(--color-text-secondary); - font-size: var(--text-caption); - font-weight: 600; } .m01-logic__grid th, @@ -245,15 +215,6 @@ input.m01-logic__input { width: 64px; } -.m01-logic__tag { - flex: none; - padding: 0 6px; - border-radius: var(--radius-pills); - background: var(--color-mist-violet); - color: var(--color-accent); - font-size: var(--text-caption); -} - .m01-logic__money { display: block; text-align: right; @@ -261,11 +222,6 @@ input.m01-logic__input { white-space: nowrap; } -.m01-logic__muted { - color: var(--color-text-muted); - font-size: var(--text-caption); -} - /* 목록 「별칭」 칸(208-1) — 칸 안 글 상자 · 테두리는 올렸을 때만 */ .m01-logic__alias { width: 160px; @@ -283,29 +239,10 @@ input.m01-logic__input { background: var(--color-canvas); } -.m01-logic__bad { - color: var(--color-danger); - font-size: var(--text-caption); -} - -.m01-logic__drop { - border: 0; - background: none; - color: var(--color-text-muted); - font-size: 16px; - cursor: pointer; -} - -.m01-logic__drop:hover { - color: var(--color-danger); -} - +/* 겉 = 띠 알림 부품(ui-banner--danger) · 여기는 제목 · 까닭 줄 쌓기만 */ .m01-logic__reasons { - padding: var(--spacing-8); - border-left: 3px solid var(--color-danger); - border-radius: var(--radius-sm); - background: var(--color-canvas); - color: var(--color-danger); + flex-direction: column; + gap: 2px; } .m01-logic__sums { @@ -330,31 +267,7 @@ input.m01-logic__input { gap: var(--spacing-8); } -.m01-logic__backdrop { - position: fixed; - inset: 0; - z-index: 1000; - display: flex; - align-items: flex-start; - justify-content: center; - background-color: rgba(38, 17, 74, 0.35); /* 공용 창 틀과 같이 뒤를 가림 */ -} - -.m01-logic__pick { - display: flex; - flex-direction: column; - gap: var(--spacing-8); - width: min(720px, 92vw); - /* 위에서 고정 거리 — 공용 .ui-modal 과 같은 규칙(찾기 칸이 안 움직임) */ - max-height: var(--modal-max, 80vh); - margin-top: calc((100dvh - var(--modal-max, 80vh)) / 2); - padding: var(--spacing-16); - border-radius: var(--radius-xl); - border: 1px solid var(--color-border); - background: var(--color-surface-raised); - box-shadow: var(--shadow-lg); - font-size: var(--text-body-sm); -} +/* 고르기 창 = 모달 부품(ui-modal · 720 · 위 고정) — 여기는 덧붙임 없음 · 표 · 복사 창만 폭 곁값 */ .m01-logic__pick-bar { display: grid; @@ -367,6 +280,7 @@ input.m01-logic__input { overflow: auto; } +/* 고르는 줄 = 목록 줄 부품(button) · 열 고르기 줄(div)만 아래 선 */ .m01-logic__pick-row { display: flex; align-items: center; @@ -374,20 +288,9 @@ input.m01-logic__input { gap: var(--spacing-8); width: 100%; padding: var(--spacing-4) var(--spacing-8); - border: 0; +} +div.m01-logic__pick-row { border-bottom: 1px solid var(--color-border); - background: none; - color: inherit; - font: inherit; - text-align: left; -} - -.m01-logic__pick-row--button { - cursor: pointer; -} - -.m01-logic__pick-row--button:hover { - background: var(--color-paper); } .m01-logic__chips { diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index c43c90a4a..c40020fd3 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -9,6 +9,12 @@ * ========================================================================== */ import "@ui/ui_template_workflow_layout.css"; +// 부품 상자 겉(553-5) — 페이지 CSS 앞에 실어 페이지 곁값이 이김 +import "@ui/ui_template_banner.css"; +import "@ui/ui_template_list_row.css"; +import "@ui/ui_template_table.css"; +import "@ui/ui_template_text.css"; +import "@ui/ui_template_toolbar.css"; import { createButton, createInputField, @@ -71,7 +77,10 @@ function buildPage(): HTMLElement { const summary = el("span", { className: "m01-master__summary" }); const save = createButton({ label: L("M01_Save"), variant: "filled" }); const drop = createButton({ label: L("M01_Discard"), variant: "ghost" }); - const notice = el("div", { className: "m01-master__notice", attrs: { hidden: "" } }); + const notice = el("div", { + className: "ui-banner ui-banner--danger m01-master__notice", + attrs: { hidden: "" }, + }); const body = el("div", { className: "m01-master__body" }); // 제목 줄 · 찾기 줄 = 「단가산출 로직」 화면과 같은 공용 조각 const headBar = buildHeadBar({ title, status: [summary], buttons: [drop, save] }); diff --git a/M01_MasterData/M01_MasterData_UI_Pick.ts b/M01_MasterData/M01_MasterData_UI_Pick.ts index c3934e047..e5bdc8d38 100644 --- a/M01_MasterData/M01_MasterData_UI_Pick.ts +++ b/M01_MasterData/M01_MasterData_UI_Pick.ts @@ -4,6 +4,9 @@ * ========================================================================== */ import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; +import { createEmpty } from "@ui/ui_template_empty"; +import { openModal } from "@ui/ui_template_modal"; +import { createText } from "@ui/ui_template_text"; import { t as L } from "@ui/ui_template_locale"; import { fetchPick, type PickItem, type PickKind } from "./M01_MasterData_Api_Fetch"; import { show } from "./M01_MasterData_UI_Cells"; @@ -34,7 +37,6 @@ const field = (value: string, placeholder: string): ReturnType back.remove(); const searchField = field(opt.seed, L("M01_ProcureSearch")); const search = searchField.input; const list = el("div", { className: "m01-procure__list" }); @@ -44,8 +46,6 @@ export function openPickModal(opt: PickOptions): void { opt.onPick(null); close(); }); - const shut = button(L("M01_ProcureClose")); - shut.addEventListener("click", close); const condRow: HTMLElement[] = []; if (opt.cond) { @@ -61,22 +61,15 @@ export function openPickModal(opt: PickOptions): void { condRow.push(el("div", { className: "m01-procure__cond", children: [name, spec, go] })); } - const box = el("div", { - className: "m01-procure__box", - children: [ - el("h3", { text: opt.title }), - el("p", { className: "m01-master__muted", text: opt.current }), - searchField.root, - list, - ...condRow, - el("div", { className: "m01-procure__foot", children: [cut, shut] }), - ], + // 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정(공용 꼴) · [연결 끊기] 는 머리 단추 자리 + const { close } = openModal({ + title: opt.title, + closeLabel: L("M01_ProcureClose"), + width: "40rem", + dialogClass: "m01-procure__box", + actions: [cut], + mount: (body) => body.append(createText(opt.current), searchField.root, list, ...condRow), }); - const back = el("div", { className: "m01-procure", children: [box] }); - back.addEventListener("click", (e) => { - if (e.target === back) close(); - }); - document.body.append(back); const paint = (items: PickItem[], total: number): void => { list.replaceChildren( @@ -84,12 +77,12 @@ export function openPickModal(opt: PickOptions): void { ? items.map((it) => { const tag = it.관급 ? ` · ${L("M01_Govt")}` : ""; const b = el("button", { - className: "m01-procure__item", - attrs: { type: "button" }, + className: "m01-procure__item ui-list-row ui-sidebar-row", + attrs: { type: "button", "data-size": "36" }, children: [ el("strong", { text: it.이름 }), el("span", { text: `${it.규격} · ${it.단위} · ${show(it.값)}${tag}` }), - el("span", { className: "m01-master__muted", text: it.ref }), + el("span", { className: "ui-text", text: it.ref }), ], }); b.addEventListener("click", () => { @@ -98,10 +91,8 @@ export function openPickModal(opt: PickOptions): void { }); return b; }) - : [el("p", { className: "m01-master__empty", text: L("M01_NoRows") })]), - ...(total > items.length - ? [el("p", { className: "m01-master__muted", text: `${items.length} / ${total}` })] - : []), + : [createEmpty({ text: L("M01_NoRows"), small: true })]), + ...(total > items.length ? [createText(`${items.length} / ${total}`)] : []), ); }; let timer: number | undefined; @@ -123,5 +114,4 @@ export function openPickModal(opt: PickOptions): void { timer = window.setTimeout(() => void run(), 300); }); void run(true); - search.focus(); } diff --git a/M01_MasterData/M01_MasterData_UI_Rows.ts b/M01_MasterData/M01_MasterData_UI_Rows.ts index a3b5d24d0..2239e17df 100644 --- a/M01_MasterData/M01_MasterData_UI_Rows.ts +++ b/M01_MasterData/M01_MasterData_UI_Rows.ts @@ -28,6 +28,7 @@ import { show, withCell, } from "./M01_MasterData_UI_Cells"; +import { createEmpty } from "@ui/ui_template_empty"; const SIZE = 50; const MARKET = "재료_자재품목.json"; @@ -205,19 +206,17 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void addRow(file, d.version, sub && col ? { ...fresh, [col]: sub } : fresh); }); const empty = - d.rows.length || adds.length - ? [] - : [el("p", { className: "m01-master__empty", text: L("M01_NoRows") })]; + d.rows.length || adds.length ? [] : [createEmpty({ text: L("M01_NoRows"), small: true })]; host.replaceChildren( el("div", { - className: "m01-master__bar", + className: "ui-toolbar m01-master__bar", children: [add, ...only, buildPager(d.total, SIZE, d.page, go)], }), el("div", { - className: "m01-master__grid-wrap", + className: "ui-table-wrap m01-master__grid-wrap", children: [ el("table", { - className: "m01-master__grid", + className: "ui-table m01-master__grid", children: [el("thead", { children: [head] }), body], }), ], diff --git a/M01_MasterData/M01_MasterData_UI_Style.css b/M01_MasterData/M01_MasterData_UI_Style.css index f3ef10723..5ee4f67fe 100644 --- a/M01_MasterData/M01_MasterData_UI_Style.css +++ b/M01_MasterData/M01_MasterData_UI_Style.css @@ -17,12 +17,6 @@ color: var(--color-accent); } -.m01-side__muted, -.m01-master__muted { - color: var(--color-text-muted); - font-size: var(--text-caption); -} - /* ── 우측 ── */ .m01-master__panel { display: flex; @@ -58,17 +52,12 @@ html.is-bm-route .m01-master__headbox { font-weight: var(--font-weight-bold); } +/* 겉 = 띠 알림 부품(ui-banner--danger) · 여기는 쌓기 · 높이만 */ .m01-master__notice { - display: flex; flex-direction: column; gap: var(--spacing-4); - padding: var(--spacing-8) var(--spacing-12); max-height: 30vh; overflow: auto; - border-left: 4px solid var(--color-danger); - background: color-mix(in srgb, var(--color-danger) 10%, var(--color-canvas)); - color: var(--color-danger); - font-size: var(--text-body-sm); } .m01-master__notice p { @@ -95,25 +84,20 @@ html.is-bm-route .m01-master__headbox { min-width: 0; } +/* 겉 = 표 부품(ui-table-wrap · ui-table) · 여기는 높이 · 폭 · 머리 고정 · 칸 상태만 */ .m01-master__grid-wrap { max-height: calc(100dvh - 300px); overflow: auto; - border: 1px solid var(--color-border); - border-radius: var(--radius-cards); } -.m01-master__grid { - border-collapse: collapse; - font-size: var(--text-body-sm); +table.m01-master__grid { + width: auto; } .m01-master__grid th, .m01-master__grid td { max-width: 320px; - padding: var(--spacing-8) var(--spacing-12); overflow: hidden; - border-bottom: 1px solid var(--color-border); - text-align: left; text-overflow: ellipsis; white-space: nowrap; } @@ -122,9 +106,7 @@ html.is-bm-route .m01-master__headbox { position: sticky; top: 0; z-index: 1; - background: var(--color-surface); - color: var(--color-text-secondary); - font-weight: var(--font-weight-medium); + background: var(--color-surface-raised); } .m01-master__grid td.is-editable { @@ -161,77 +143,29 @@ html.is-bm-route .m01-master__headbox { cursor: pointer; } -.m01-master__table-row:hover { - background: var(--color-paper); -} - .m01-master__table-row.is-changed { background: color-mix(in srgb, var(--color-warning) 28%, var(--color-canvas)); } -.m01-master__empty { - color: var(--color-text-muted); -} - -.m01-master__logic-key { - padding: 0; - border: 0; - background: transparent; - color: var(--color-accent); - font: inherit; - cursor: pointer; - text-decoration: underline; -} - -/* 조달 찾기 모달 */ -.m01-procure { - position: fixed; - inset: 0; - z-index: 1000; - display: flex; - align-items: flex-start; - justify-content: center; - background: rgba(0, 0, 0, 0.45); -} -.m01-procure__box { - display: flex; - flex-direction: column; - gap: 8px; - width: min(640px, 92vw); - max-height: 80vh; - margin-top: 10vh; /* 위에서 고정 거리 — 찾기 칸이 안 움직임 */ - padding: 16px; - border-radius: 8px; - background: var(--color-surface, #fff); -} +/* 조달 찾기 = 모달 부품(40rem) · 줄 = 목록 줄 부품 — 여기는 줄 안 세 줄 쌓기 · 목록 높이만 */ .m01-procure__list { display: flex; flex-direction: column; - gap: 4px; + gap: var(--spacing-4); overflow-y: auto; min-height: 120px; } .m01-procure__item { - display: flex; flex-direction: column; align-items: flex-start; - padding: 6px 8px; - border: 1px solid var(--color-border, #ccc); - border-radius: 4px; - background: transparent; - cursor: pointer; - text-align: left; -} -.m01-procure__foot { - display: flex; - justify-content: flex-end; - gap: 8px; + gap: 0; + padding-block: var(--spacing-4); } .m01-procure__cond { display: flex; flex-wrap: wrap; - gap: 8px; + gap: var(--spacing-8); } .m01-side__filter { diff --git a/M01_MasterData/M01_MasterData_UI_Tables.ts b/M01_MasterData/M01_MasterData_UI_Tables.ts index bff01e295..75ad992c0 100644 --- a/M01_MasterData/M01_MasterData_UI_Tables.ts +++ b/M01_MasterData/M01_MasterData_UI_Tables.ts @@ -128,10 +128,10 @@ async function showList( (p) => void showList(host, pick, q, usage, p, open, onUsageChange), ), el("div", { - className: "m01-master__grid-wrap m01-master__tables", + className: "ui-table-wrap m01-master__grid-wrap m01-master__tables", children: [ el("table", { - className: "m01-master__grid", + className: "ui-table m01-master__grid", children: [ el("thead", { children: [el("tr", { children: LIST_COLS.map((c) => el("th", { text: c })) })], @@ -184,11 +184,8 @@ function openTable( now.forEach((item, i) => { const tr = el("tr"); tr.classList.toggle("is-new", item.o === null); - const del = el("button", { - className: "m01-master__row-btn", - text: L("M01_RowRemove"), - attrs: { type: "button" }, - }); + const del = createButton({ label: L("M01_RowRemove"), variant: "ghost", size: "sm" }); + del.classList.add("m01-master__row-btn"); del.addEventListener("click", () => commit(now.filter((_, j) => j !== i))); tr.append(el("td", { children: [del] })); for (const c of cols) { @@ -217,32 +214,30 @@ function openTable( back(); }); const notes = ((table.주 ?? []) as string[]).map((n) => - el("p", { className: "m01-master__muted", text: n }), + el("p", { className: "ui-text ui-text--muted", text: n }), ); const usage = table.용도 as TableUsage | undefined; const logicLinks = (usage?.로직키 ?? []).map((k, i) => { - const btn = el("button", { - className: "m01-master__logic-key", - text: k, - attrs: { type: "button", title: L("M01_OpenLogic") }, - }); + const btn = createButton({ label: k, variant: "link" }); + btn.classList.add("m01-master__logic-key"); + btn.title = L("M01_OpenLogic"); btn.addEventListener("click", () => openLogic(k)); return i === 0 ? btn : el("span", { text: ", ", children: [btn] }); }); host.replaceChildren( el("div", { - className: "m01-master__bar", + className: "ui-toolbar m01-master__bar", children: [ backButton, el("strong", { text: String(table.이름 ?? key) }), el("span", { - className: "m01-master__muted", + className: "ui-text ui-text--muted", text: `${key} · ${table.기준 || "—"} · ${table.출처 || ""}`, }), ...(usage ? [ el("span", { - className: "m01-master__muted", + className: "ui-text ui-text--muted", text: `${L("M01_TableUsage")}: ${usage.공종} · ${usage.대상.join(", ")} · `, children: logicLinks, }), @@ -252,10 +247,10 @@ function openTable( ], }), el("div", { - className: "m01-master__grid-wrap", + className: "ui-table-wrap m01-master__grid-wrap", children: [ el("table", { - className: "m01-master__grid", + className: "ui-table m01-master__grid", children: [ el("thead", { children: [ diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts index b5b6bd70c..f74b0f691 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts @@ -13,6 +13,7 @@ import { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head"; import "../M01_MasterData/M01_MasterData_UI_Style.css"; import { fetchTree, type Layer, type TreeItem } from "./M02_MasterTemplete_Api_Fetch"; import { folderOf, leafName, matchText } from "./M02_MasterTemplete_TreePanel_Model"; +import { createEmpty } from "@ui/ui_template_empty"; /** 목록 종류 — 하위 컨테이너 셋 + 항목 통째(복사 생성) */ export type ListKind = "structure" | "area" | "labor" | "item"; @@ -160,10 +161,10 @@ export function buildItemList(): ItemListHandle { let page = 1; const searchField = createInputField({ type: "search", placeholder: TEXT.search }); const search = searchField.input; - const grid = el("div", { className: "m01-master__grid-wrap" }); + const grid = el("div", { className: "ui-table-wrap m01-master__grid-wrap" }); let pager = buildPager(0, SIZE, 1, () => {}); const note = (text: string): void => { - grid.replaceChildren(el("p", { className: "m01-master__empty", text })); + grid.replaceChildren(createEmpty({ text, small: true })); }; const show = ( @@ -193,7 +194,7 @@ export function buildItemList(): ItemListHandle { text, attrs: { title: note ? `${text} ${note}` : text }, children: note - ? [el("small", { className: "m01-master__muted", text: ` ${note}` })] + ? [el("small", { className: "ui-text ui-text--muted", text: ` ${note}` })] : [], }); }), @@ -209,7 +210,7 @@ export function buildItemList(): ItemListHandle { }); grid.replaceChildren( el("table", { - className: "m01-master__grid", + className: "ui-table m01-master__grid", children: [ el("thead", { children: [el("tr", { children: cols.map((c) => el("th", { text: c.head })) })], diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index d319abb5a..ac3fa3209 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -1,13 +1,8 @@ -/* 일위대가 화면 — 표는 칸 폭을 채움 · - 흐린 안내 글은 보조 글색(뮤트 글색은 밝은 테마에서 대비 3.1 로 모자람) */ -.m01-combo .m01-master__grid { +/* 일위대가 화면 — 표는 칸 폭을 채움 */ +.m01-combo table.m01-master__grid { width: 100%; } -.m01-combo .m01-logic__muted { - color: var(--color-text-secondary); -} - /* 일위대가 상세(39-3) — 담은 로직 · 자재목록 표 · 금액 칸 두 줄(위 기준 작게 · 아래 반영) */ .m02-labrow { width: 100%; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index f1fe92e7b..5b71c1bfe 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -75,6 +75,7 @@ import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"; import "@ui/ui_template_general_layout.css"; import "./M02_MasterTemplete_Labor.css"; +import { createText } from "@ui/ui_template_text"; export interface LaborDetailOptions { layer: Layer; @@ -160,10 +161,7 @@ export function mountLaborDetail( }); function showSource(): void { if (side?.current() !== "source") return; - if (!picked) - return void sourceHost.replaceChildren( - el("p", { className: "m01-logic__muted", text: tc("Labor_SourceHint") }), - ); + if (!picked) return void sourceHost.replaceChildren(createText(tc("Labor_SourceHint"))); source.show(logics.get(picked)?.출처 ?? ""); if (sourceHost.firstChild !== source.root) sourceHost.replaceChildren(source.root); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index bccd33756..d03b5c56a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -28,6 +28,7 @@ import { precomboClasses } from "./M02_MasterTemplete_UI_Side_Precombo"; import { fetchStructureDef } from "./StructOptions/M02_SO_Api"; import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀 import "./M02_MasterTemplete_Labor.css"; +import { openModal } from "@ui/ui_template_modal"; export { withHopyo } from "./M02_MasterTemplete_Hopyo_Pick"; @@ -92,7 +93,7 @@ export function openLogicPicker( const sub = createSelectField({ options: opts([]), compact: true, label: tc("Sub") }); const detail = createSelectField({ options: opts([]), compact: true, label: tc("Detail") }); detail.select.disabled = true; - const close = (): void => backdrop.remove(); + let close = (): void => {}; /* 한 목록 — 구분(단가산출 · 일위조합) 칸 · 층 단추는 서고 줄(사전조합)의 층 */ const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } }); @@ -266,24 +267,14 @@ export function openLogicPicker( ); }); - const dialog = el("div", { - className: "m01-logic__pick m01-combo ui-popup", - attrs: { role: "dialog", "aria-label": tc("Pick_Title") }, - children: [ - el("div", { - className: "m01-logic__section-head", - children: [ - el("h3", { text: lib?.title ?? tc("Pick_Title") }), - createButton({ label: tc("Pick_Close"), variant: "ghost", onClick: close }), - ], - }), - finder.root, - ], - }); - const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] }); - backdrop.addEventListener("click", (ev) => ev.target === backdrop && close()); - backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close()); - document.body.append(backdrop); + // 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정 + ({ close } = openModal({ + title: lib?.title ?? tc("Pick_Title"), + closeLabel: tc("Pick_Close"), + dialogClass: "m01-logic__pick m01-combo", + backdropClass: "m01-logic__backdrop", + mount: (body) => body.append(finder.root), + })); finder.search.focus(); void lib?.more?.().then( diff --git a/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts b/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts index 003678691..0a8d76385 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Library_List.ts @@ -255,11 +255,11 @@ export function mountLibraryList( host.replaceChildren( buildHeadFilters([searchField.root, pager]), el("div", { - className: "m01-master__grid-wrap", + className: "ui-table-wrap m01-master__grid-wrap", attrs: { "data-lib-list": opt.kind }, children: [ el("table", { - className: "m01-master__grid", + className: "ui-table m01-master__grid", children: [ el("thead", { children: [ diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 2f8f094bb..7cb89166b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -9,6 +9,12 @@ * ========================================================================== */ import "@ui/ui_template_workflow_layout.css"; +// 부품 상자 겉(553-5) — 페이지 CSS 앞에 실어 페이지 곁값이 이김 +import "@ui/ui_template_banner.css"; +import "@ui/ui_template_list_row.css"; +import "@ui/ui_template_table.css"; +import "@ui/ui_template_text.css"; +import "@ui/ui_template_toolbar.css"; import { ROUTES } from "@config/config_frontend"; import { el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; diff --git a/resources/tester/test_553_5_m01_m02.py b/resources/tester/test_553_5_m01_m02.py new file mode 100644 index 000000000..20e94c87c --- /dev/null +++ b/resources/tester/test_553_5_m01_m02.py @@ -0,0 +1,69 @@ +"""553-5 M01 · M02 페이지 부품 옮김 지킴 시험 — 자체 변종이 부품으로 바뀐 자리 · 날 단추 · 손 모달 · 날 색 없음.""" + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +OLD_ALIAS = re.compile(r"--color-(royal-amethyst|deep-iris|mist-violet|plum-velvet|lavender-wash)") +RAW_COLOR = re.compile(r"#[0-9a-fA-F]{3,8}\b|\brgba?\(") + + +def _src(folder: str, suffix: str) -> dict[str, str]: + return {p.name: p.read_text(encoding="utf-8") for p in (ROOT / folder).glob(f"*{suffix}")} + + +def test_m01_no_raw_buttons_or_hand_modals(): + for name, ts in _src("M01_MasterData", ".ts").items(): + # 날 단추 — 왼쪽 목록 줄(공용 ui-sidebar-row) · 목록 줄 부품 꼴 단추만 남음 + for m in re.finditer(r'el\("button", \{\s*className: "([^"]*)"', ts): + assert "ui-sidebar-row" in m.group(1), f"{name}: 날 단추 {m.group(1)}" + assert "document.body.append(" not in ts, f"{name}: 손 모달 — openModal 부품을 씀" + + +def test_m01_parts_in_place(): + ts = "".join(_src("M01_MasterData", ".ts").values()) + for part in ( + "openModal(", + "createBanner", + "createBadge(", + "createText(", + "createEmpty(", + "createIconButton(", + ): + if part == "createBanner": + assert ( + "ui-banner ui-banner--danger m01-master__notice" in ts + ) # 알림 띠 = 띠 알림 부품 꼴 + assert "ui-banner ui-banner--danger m01-logic__reasons" in ts + continue + assert part in ts, f"M01 이 부품 {part} 를 안 씀" + assert "ui-table m01-master__grid" in ts and "ui-table-wrap m01-master__grid-wrap" in ts + for gone in ( + "m01-logic__muted", + "m01c__muted", + "m01-master__muted", + "m01-logic__tag", + "m01-logic__bad", + ): + assert gone not in ts, f"옛 변종 {gone} 남음" + + +def test_m01_css_common_values_only(): + for name, css in _src("M01_MasterData", ".css").items(): + assert not OLD_ALIAS.search(css), f"{name}: 옛 보라 별칭" + body = re.sub(r"/\*.*?\*/", "", css, flags=re.S) + assert not RAW_COLOR.search(body), f"{name}: 날 색" + for gone in ( + ".m01-procure {", + ".m01-logic__backdrop {", + ".m01-logic__tag", + ".m01lab__pills", + ): + assert gone not in css, f"{name}: 죽은 · 옛 CSS {gone}" + + +def test_m02_borrowed_m01_classes_follow_parts(): + ts = "".join(_src("M02_MasterTemplete", ".ts").values()) + assert "m01-logic__muted" not in ts and "m01-master__muted" not in ts.replace("ui-text", "") + assert 'className: "m01-master__grid"' not in ts # 빌린 표도 표 부품 꼴 + assert "ui-table m01-master__grid" in ts diff --git a/resources/tester/test_modal_top_and_add_word.py b/resources/tester/test_modal_top_and_add_word.py index 89dc80eb1..26c09d446 100644 --- a/resources/tester/test_modal_top_and_add_word.py +++ b/resources/tester/test_modal_top_and_add_word.py @@ -42,17 +42,18 @@ def test_공용_모달_위_고정(): def test_따로_둔_모달도_위_고정(): - logic = _text("M01_MasterData/M01_MasterData_UI_Logic_Style.css") - assert "align-items: flex-start;" in _block(logic, ".m01-logic__backdrop") - assert "margin-top: calc((100dvh - var(--modal-max, 80vh)) / 2);" in _block( - logic, ".m01-logic__pick" - ) + # M01 고르기 · 복사 · 조달 창 = 모달 부품(553-5) — 위 고정은 공용 .ui-modal 이 냄 assert "--modal-max: 90vh;" in _block( - _text("M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"), ".m01copy__sheet" + _text("M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"), ".ui-modal.m01copy__sheet" ) - m01 = _text("M01_MasterData/M01_MasterData_UI_Style.css") - assert "align-items: flex-start;" in _block(m01, ".m01-procure") - assert "margin-top: 10vh;" in _block(m01, ".m01-procure__box") + for rel in ( + "M01_MasterData/M01_MasterData_UI_Pick.ts", + "M01_MasterData/M01_MasterData_UI_Logic_Pick.ts", + "M01_MasterData/M01_MasterData_UI_Logic_PickTable.ts", + "M01_MasterData/M01_MasterData_UI_LogicLab_Copy.ts", + ): + src = _text(rel) + assert "openModal(" in src and "document.body.append(" not in src, rel b01 = _text("B01_Dashboard/B01_Dashboard_UI_Style.css") + _text( "B01_Dashboard/B01_Dashboard_UI_Style_Company.css" )