refactor(M01): 표 · 모달 · 띠 알림 · 배지 · 알약 · 보조 글 · 빈 상태를 부품으로 (PLAN 553-5)

- 표 둘 = 표 부품(m01-master__grid = 일반표 · m01-logic__grid = 데이터표) · 페이지 CSS 는 폭 · 머리 고정 · 칸 상태만
- 손 모달 다섯(조달 찾기 · 요소 고르기 · 표 바꾸기 · 복사 시트 · 표 고르기) = 모달 부품 openModal
- 알림 띠 · 막힘 까닭 = 띠 알림 · 태그 = 배지 · 알약 · × · 삭제 · 로직 키 = 단추 부품 · muted 세 벌 = 보조 글 · 빈 줄 = 빈 상태
- M02 가 빌린 M01 클래스(표 · muted · 고르기 창)도 같은 부품으로 · 죽은 CSS 걷음 · 지킴 시험 test_553_5_m01_m02

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 22:57:33 +09:00
co-authored by Claude Opus 5.5
parent 6d8b38b518
commit 022eff69f5
34 changed files with 367 additions and 559 deletions
+1 -1
View File
@@ -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")}`,
+9 -7
View File
@@ -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)) }),
);
}
};
@@ -53,7 +53,7 @@ export function askBasicInfo(units: string[]): Promise<BasicInfo | null> {
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<BasicInfo | null> {
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({
@@ -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<void> => {
@@ -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<void> {
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<void> {
drawAliases();
refreshCalc();
const close = (): void => backdrop.remove();
let close = (): void => {};
const onSave = async (): Promise<void> => {
showLoadingOverlay();
try {
@@ -337,55 +327,48 @@ export async function openCopyScreen(opt: CopyOptions): Promise<void> {
}
};
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() }),
],
}),
),
}));
}
@@ -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.결과?.식 ?? "" }),
]);
}
@@ -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);
}
@@ -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);
}
@@ -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") })],
}),
],
});
@@ -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"),
);
});
});
@@ -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;
@@ -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();
@@ -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),
@@ -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);
}
+8 -10
View File
@@ -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: [
@@ -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);
+13 -14
View File
@@ -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"),
@@ -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 })) })],
+11 -6
View File
@@ -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 }),
}),
],
+20 -27
View File
@@ -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<PickMode, string[]> = {
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,
],
@@ -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);
}
@@ -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 {
+10 -1
View File
@@ -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] });
+16 -26
View File
@@ -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<typeof createInpu
createInputField({ type: "search", placeholder, value });
export function openPickModal(opt: PickOptions): void {
const close = (): void => 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();
}
+5 -6
View File
@@ -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],
}),
],
+10 -76
View File
@@ -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 {
+13 -18
View File
@@ -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: [
@@ -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 = <R>(
@@ -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 })) })],
@@ -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%;
@@ -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);
}
@@ -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(
@@ -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: [
@@ -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";
+69
View File
@@ -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
@@ -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"
)