refactor(M02): 띠 알림 · 빈 상태 · 보조 글 · 배지 · 탭 · 표 · 목록 줄 · 도구줄 · 체크를 부품으로 (PLAN 553-5)

- DataFlow: 표 넷 = 데이터표 · 함수 판 알림 · 선/알약 경고 · 끊긴 곳 = 띠 알림 · 알약 · 칸 종류 = 배지 · 고르는 줄 = 목록 줄 · 입력 넷 = 입력 · 도구줄 여섯 · 날 색 → 공통 값
- 구조물 옵션 · 구조물 · 도면 · 면적 · 품목 고르기: 판 띠 = 띠 알림 · 한눈/종류별/부품 = 탭 · 표 둘 = 데이터표 · 체크 둘 = 체크 · 카드 · 번호 = 카드 · 배지 · 도면 도구줄
- 일위대가 · 호표 · 실무자료 · 본 화면 · 서고: 알림 = 띠 알림 · 빈 상태 · 보조 글 · 도구줄 여섯 · 옛 보라 별칭 0
- M01 로직 입력칸 = 입력 부품(작은 칸) · 일위대가 상세 줄 수 고정 시험 693 · 지킴 시험 M02 몫 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 23:44:19 +09:00
co-authored by Claude Opus 5.5
parent 022eff69f5
commit 09134bab84
86 changed files with 583 additions and 705 deletions
@@ -199,7 +199,7 @@ function logicAliasControls(
function lineBox(row: LogicRow, line: FormulaLine, onChange: () => void): HTMLElement {
const f = field(row, line.자리);
const box = el("textarea", { className: "m01-logic__input m01lab__qty" });
const box = el("textarea", { className: "ui-input ui-input--sm m01-logic__input m01lab__qty" });
box.value = f.get();
box.rows = Math.max(1, Math.ceil(box.value.length / 60));
box.addEventListener("input", () => {
@@ -144,7 +144,10 @@ function infoBox(ctx: DetailContext): HTMLElement {
const own = isOwnLogic(row);
const field = (label: string, key: "이름" | "원문번호" | "결과단위" | "출처"): HTMLElement => {
if (!own) return cell(label, row[key] ?? "");
const box = el("input", { className: "m01-logic__input", attrs: { "data-own": key } });
const box = el("input", {
className: "ui-input ui-input--sm m01-logic__input",
attrs: { "data-own": key },
});
box.value = row[key] ?? "";
box.addEventListener("input", () => {
row[key] = box.value;
@@ -163,7 +166,9 @@ function infoBox(ctx: DetailContext): HTMLElement {
el("span", { text: value || "—" }),
],
});
const note = el("textarea", { className: "m01-logic__input m01-logic__note" });
const note = el("textarea", {
className: "ui-input ui-input--sm m01-logic__input m01-logic__note",
});
note.value = row.비고 ?? "";
note.addEventListener("input", () => {
if (note.value.trim()) row.비고 = note.value;
@@ -111,7 +111,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
}
} else {
const box = el("input", {
className: "m01-logic__input",
className: "ui-input ui-input--sm m01-logic__input",
attrs: { type: "text", inputmode: "decimal" },
});
if (spec.범위) box.placeholder = `${spec.범위[0]} ∼ ${spec.범위[1]}`;
@@ -61,7 +61,7 @@ export function formatNumber(value: unknown): string {
function textBox(value: unknown, onInput: (v: string) => void, className = ""): HTMLInputElement {
const input = el("input", {
className: `m01-logic__input ${className}`,
className: `ui-input ui-input--sm m01-logic__input ${className}`,
attrs: { type: "text" },
});
input.value = value === null || value === undefined ? "" : String(value);
@@ -176,7 +176,9 @@ function head(ctx: EditContext, set: (p: () => void) => void, rerender: () => vo
: el("span", { className: "m01-logic__file", text: ctx.file });
const unit = textBox(row.결과단위, (v) => set(() => (row.결과단위 = v)));
unit.addEventListener("change", rerender); // 돈 로직 ↔ 돈 아닌 로직 칸이 바뀜
const note = el("textarea", { className: "m01-logic__input m01-logic__note" });
const note = el("textarea", {
className: "ui-input ui-input--sm m01-logic__input m01-logic__note",
});
note.value = row.비고 ?? "";
note.addEventListener("input", () =>
set(() => {
@@ -97,22 +97,11 @@
font-size: var(--text-body-sm);
}
/* 겉 = 입력 부품(ui-input--sm) · 여기는 칸 폭만 */
.m01-logic__input {
box-sizing: border-box;
width: 100%;
min-width: 0;
padding: 3px 6px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-canvas);
color: var(--color-text-body);
font: inherit;
font-size: var(--text-body-sm);
}
/* 한 줄 칸은 작은 고르기칸과 같은 높이 */
input.m01-logic__input {
height: var(--control-height-sm);
}
.m01-logic__note {
@@ -9,6 +9,8 @@ import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, el } from "@ui/ui_template_elements";
import type { CfBox, CfLogic } from "./M02_DF_Model";
import { portText } from "./M02_DF_Detail";
import "@ui/ui_template_badge.css";
import "@ui/ui_template_table.css";
const TEXT = {
Title: ["칸 연결 표", "Cell connections"],
@@ -103,7 +105,8 @@ export function renderCellTable(
el("td", {
children: [
el("span", {
className: "m02-dfcells__kind",
className: `ui-badge ui-badge--pill ui-badge--${r.kind === "in" ? "info" : r.kind === "var" ? "warning" : "success"} m02-dfcells__kind`,
attrs: { "data-size": "20" },
text: tt(r.kind === "in" ? "In" : r.kind === "var" ? "Var" : "Out"),
}),
],
@@ -128,7 +131,7 @@ export function renderCellTable(
host.replaceChildren(
el("strong", { className: "m02-dfcells__title", text: tt("Title") }),
el("table", {
className: "m02-dfcells__table",
className: "ui-table ui-table--data m02-dfcells__table",
children: [el("thead", { children: [head] }), el("tbody", { children: body })],
}),
);
@@ -48,15 +48,6 @@
color: var(--color-text-secondary);
}
.m02-dfdg__note {
color: var(--color-danger);
}
.m02-dfdg__hint {
margin: 0;
color: var(--color-text-muted);
}
/* 판 — 화면 위쪽 큰 영역 */
.m02-dfdg__view {
position: relative;
@@ -38,6 +38,8 @@ import {
type TEXT,
} from "./M02_DF_Diagram_Parts";
import "./M02_DF_Diagram.css";
import "@ui/ui_template_text.css";
import "@ui/ui_template_toolbar.css";
export * from "./M02_DF_Diagram_Parts";
@@ -395,7 +397,7 @@ export function renderDiagram(
: [];
const bar = el("div", {
className: "m02-dfdg__bar",
className: "ui-toolbar m02-dfdg__bar",
children: [
el("strong", { className: "m02-dfdg__title", text: tt("Title") }),
...(ro
@@ -425,12 +427,12 @@ export function renderDiagram(
children: [
bar,
el("div", {
className: "m02-dfdg__tools",
className: "ui-toolbar m02-dfdg__tools",
children: [
...tools,
el("span", { className: "m02-dfdg__note", text: st.note }),
el("span", { className: "ui-text ui-text--error", text: st.note }),
el("span", {
className: "m02-dfdg__hint",
className: "ui-text ui-text--muted",
text: tools.length || st.note ? "" : tt("Hint"),
}),
],
@@ -12,6 +12,7 @@ import type { CfAnswer, CfQuery } from "./M02_DF_Api";
import { fillTemplate } from "./M02_DF_InputPanel_Build";
import type { CfBreak, CfValues } from "./M02_DF_Eval";
import type { CfElementRef, CfLogic, CfPort } from "./M02_DF_Model";
import "@ui/ui_template_text.css";
const TEXT = {
Detail: ["상세 내용", "Details"],
@@ -120,7 +121,7 @@ export function drawWarnings(d: DetailParts, pick: DfPick, all: CfBreak[]): void
d.warn.replaceChildren(
...(mine.length
? [...new Set(mine.map((k) => k.text))].map((w) => el("li", { text: w }))
: [el("li", { className: "m02-df__muted", text: tt("WarnNone") })]),
: [el("li", { className: "ui-text ui-text--muted", text: tt("WarnNone") })]),
);
}
@@ -12,6 +12,7 @@ import { recalcWorkbook } from "../../A00_Common/spreadsheet/spreadsheet_recalc"
import { ifBody, splitArgs } from "../../A00_Common/spreadsheet/spreadsheet_condition_text";
import type { CalcValue } from "../../A00_Common/spreadsheet/spreadsheet_types";
import type { CfBox } from "./M02_DF_Model";
import { createEmpty } from "@ui/ui_template_empty";
export type CellScalar = number | string | boolean | null;
@@ -259,7 +260,7 @@ function fromCalc(v: CalcValue): CellScalar {
/** 식 보기 그리기 — 줄마다 이름 식 · 값 식 · 갈래(지금 갈래는 ▶ 와 「지금」 글) */
export function renderFormulaView(host: HTMLElement, lines: FormulaLine[]): void {
host.classList.add("m02-df-formula");
host.classList.add("ui-card", "m02-df-formula");
host.replaceChildren(
el("strong", { className: "m02-df-formula__title", text: tv("Title") }),
...(lines.length
@@ -285,6 +286,6 @@ export function renderFormulaView(host: HTMLElement, lines: FormulaLine[]): void
],
}),
)
: [el("p", { className: "m02-df-formula__empty", text: tv("Empty") })]),
: [createEmpty({ text: tv("Empty"), small: true })]),
);
}
@@ -34,28 +34,14 @@
text-align: center;
}
.m02-dfpanel-cell {
padding: 2px var(--spacing-12);
border: 1px solid var(--color-border);
border-radius: var(--radius-pills);
background: var(--color-surface-raised);
}
.m02-dfpanel-label {
font-weight: var(--font-weight-bold);
}
.m02-dfpanel-note {
color: var(--color-text-muted);
font-size: var(--text-body-sm);
}
.m02-dfpanel-form {
/* 겉 = 카드 부품 · 여기는 아래 맞춤 · 안 여백만 */
.m02-dfpanel-form.ui-card {
align-items: flex-end;
padding: var(--spacing-8) var(--spacing-12);
border: 1px solid var(--color-border);
border-radius: var(--radius-cards);
background: var(--color-surface-raised);
}
.m02-dfpanel-fieldbox {
@@ -72,26 +58,18 @@
width: 6em;
}
.m02-dfpanel-why,
/* 알림 = 띠 알림 부품 — 숨김은 부품 display 를 이김 */
.m02-dfpanel-msg {
margin: 0;
padding: var(--spacing-8) var(--spacing-12);
border-left: 4px solid var(--color-warning);
background: var(--color-surface-raised);
font-weight: var(--font-weight-medium);
}
.m02-dfpanel-msg[hidden] {
display: none;
}
/* 받음 · 변수 · 출력 알약 = 배지 부품(info · warning · success) — 안 놓인 받음은 점선 */
.m02-dfpanel-chip {
display: inline-flex;
align-items: center;
gap: var(--spacing-4);
padding: var(--spacing-4) var(--spacing-12);
border: 2px solid;
border-radius: var(--radius-pills);
background: var(--color-surface);
color: inherit;
font: inherit;
font-weight: var(--font-weight-medium);
font-family: inherit;
}
button.m02-dfpanel-chip {
@@ -102,22 +80,11 @@ button.m02-dfpanel-chip:focus-visible {
outline: 2px solid var(--color-focus-ring);
}
.m02-dfpanel-chip.is-in {
border-color: #3b82f6;
}
.m02-dfpanel-chip.is-in:not(.is-placed) {
border-color: var(--badge-tone);
border-style: dashed;
}
.m02-dfpanel-chip.is-out {
border-color: #27ae60;
}
.m02-dfpanel-chip.is-var {
border-color: #d99a00;
}
/* 판 높이 크게 — 안쪽 세로 스크롤 없이 보이게 */
.m02-dfpanel-sheet {
position: relative;
@@ -129,15 +96,15 @@ button.m02-dfpanel-chip:focus-visible {
/* 칸 위 표시 — 색 + 글자(받음 · 변수 · 출력) 둘 다 */
.m02-dfpanel .aislo-cell[data-cf-kind="in"] {
box-shadow: inset 0 0 0 2px #3b82f6;
box-shadow: inset 0 0 0 2px var(--color-info);
}
.m02-dfpanel .aislo-cell[data-cf-kind="var"] {
box-shadow: inset 0 0 0 2px #d99a00;
box-shadow: inset 0 0 0 2px var(--color-warning);
}
.m02-dfpanel .aislo-cell[data-cf-kind="out"] {
box-shadow: inset 0 0 0 2px #27ae60;
box-shadow: inset 0 0 0 2px var(--color-success);
}
.m02-dfpanel .aislo-cell[data-cf-kind]::after {
@@ -196,20 +163,6 @@ button.m02-dfpanel-chip:focus-visible {
font-weight: var(--font-weight-bold);
}
.m02-dfcells__table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-body-sm);
}
.m02-dfcells__table th,
.m02-dfcells__table td {
padding: var(--spacing-4);
border-bottom: 1px solid var(--color-border);
text-align: left;
vertical-align: middle;
}
.m02-dfcells__cell {
width: 5em;
}
@@ -219,34 +172,15 @@ button.m02-dfpanel-chip:focus-visible {
border-style: dashed;
}
.m02-dfcells__kind {
padding: 0 var(--spacing-8);
border: 2px solid;
border-radius: var(--radius-pills);
white-space: nowrap;
}
.m02-dfcells__row.is-in .m02-dfcells__kind {
border-color: #3b82f6;
}
.m02-dfcells__row.is-var .m02-dfcells__kind {
border-color: #d99a00;
}
.m02-dfcells__row.is-out .m02-dfcells__kind {
border-color: #27ae60;
}
/* 칸 종류 = 배지 부품(받음 info · 변수 warning · 출력 success) */
/* 식 보기 */
.m02-df-formula {
/* 겉 = 카드 부품 · 여기는 쌓기 · 안 여백만 */
.m02-df-formula.ui-card {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
padding: var(--spacing-8) var(--spacing-12);
border: 1px solid var(--color-border);
border-radius: var(--radius-cards);
background: var(--color-surface-raised);
}
.m02-df-formula__title {
@@ -274,8 +208,3 @@ button.m02-dfpanel-chip:focus-visible {
.m02-df-formula__branches .is-now {
font-weight: var(--font-weight-bold);
}
.m02-df-formula__empty {
margin: 0;
color: var(--color-text-muted);
}
+43 -19
View File
@@ -27,6 +27,9 @@ import { createRoundRow } from "./M02_DF_RoundRow";
import { createUnitPick, suggestPlaces } from "./M02_DF_UnitPick";
import { ERR_KEY, FRAMES, TEXT, tf } from "./M02_DF_FuncText";
import "./M02_DF_FuncPanel.css";
import "@ui/ui_template_text.css";
import "@ui/ui_template_banner.css";
import "@ui/ui_template_badge.css";
type Handlers = { change(lg: CfLogic): void };
@@ -261,10 +264,14 @@ function drawHead(st: State): void {
});
st.parts.head.replaceChildren(
el("strong", { className: "m02-dfpanel-title", text: box?.name ?? "" }),
el("span", { className: "m02-dfpanel-cell", text: `${tf("Cell")} ${st.cell}` }),
el("span", {
className: "ui-badge ui-badge--pill ui-badge--neutral",
attrs: { "data-size": "24" },
text: `${tf("Cell")} ${st.cell}`,
}),
el("label", {
className: "m02-dfpanel-unitbox",
children: [el("span", { className: "m02-dfpanel-note", text: tf("Unit") }), unit],
children: [el("span", { className: "ui-text ui-text--muted", text: tf("Unit") }), unit],
}),
);
}
@@ -275,9 +282,9 @@ function drawGot(st: State): void {
const chips = items.map(({ port, unit }) => {
const at = f?.inputs.find((i) => i.port === port)?.cell;
const b = el("button", {
className: `m02-dfpanel-chip is-in${at ? " is-placed" : ""}`,
className: `ui-badge ui-badge--pill ui-badge--info m02-dfpanel-chip is-in${at ? " is-placed" : ""}`,
text: `${port} (${unit}) · ${at ? `${tf("Placed")} ${at}` : tf("Free")}`,
attrs: { type: "button", draggable: st.readOnly ? "false" : "true" },
attrs: { type: "button", "data-size": "24", draggable: st.readOnly ? "false" : "true" },
});
b.addEventListener("click", () => placePort(st, port, st.cell));
b.addEventListener("dragstart", (e) => e.dataTransfer?.setData("text/plain", port));
@@ -285,9 +292,11 @@ function drawGot(st: State): void {
});
st.parts.got.replaceChildren(
el("span", { className: "m02-dfpanel-label", text: tf("Got") }),
...(chips.length ? chips : [el("span", { className: "m02-dfpanel-note", text: tf("NoGot") })]),
...(chips.length
? chips
: [el("span", { className: "ui-text ui-text--muted", text: tf("NoGot") })]),
...(chips.length && !st.readOnly
? [el("span", { className: "m02-dfpanel-note", text: tf("GotHint") })]
? [el("span", { className: "ui-text ui-text--muted", text: tf("GotHint") })]
: []),
);
}
@@ -303,7 +312,7 @@ function formField(
return el("label", {
className: "m02-dfpanel-fieldbox",
children: [
el("span", { className: "m02-dfpanel-note", text: tf(label) }),
el("span", { className: "ui-text ui-text--muted", text: tf(label) }),
createUnitPick({
value: form.unit || "-",
ariaLabel: tf(label),
@@ -321,7 +330,7 @@ function formField(
input.addEventListener("keydown", (e) => e.key === "Enter" && makeVariable(st));
return el("label", {
className: "m02-dfpanel-fieldbox",
children: [el("span", { className: "m02-dfpanel-note", text: tf(label) }), input],
children: [el("span", { className: "ui-text ui-text--muted", text: tf(label) }), input],
});
}
@@ -330,7 +339,8 @@ function drawVars(st: State): void {
const row: HTMLElement[] = [el("span", { className: "m02-dfpanel-label", text: tf("Vars") })];
for (const v of f?.variables ?? []) {
const chip = el("span", {
className: "m02-dfpanel-chip is-var",
className: "ui-badge ui-badge--pill ui-badge--warning m02-dfpanel-chip is-var",
attrs: { "data-size": "24" },
children: [`${v.name} (${v.unit}) · ${v.cell ?? ""}`],
});
if (!st.readOnly) {
@@ -351,7 +361,7 @@ function drawVars(st: State): void {
row.push(chip);
}
if (!f?.variables.length)
row.push(el("span", { className: "m02-dfpanel-note", text: tf("NoVars") }));
row.push(el("span", { className: "ui-text ui-text--muted", text: tf("NoVars") }));
const parts: HTMLElement[] = [el("div", { className: "m02-dfpanel-row", children: row })];
if (!st.readOnly) {
const form = st.form;
@@ -400,12 +410,15 @@ function drawVars(st: State): void {
});
parts.push(
el("div", {
className: "m02-dfpanel-row m02-dfpanel-form",
className: "ui-card m02-dfpanel-row m02-dfpanel-form",
children: [
formField(st, "VarName", "name"),
el("label", {
className: "m02-dfpanel-fieldbox",
children: [el("span", { className: "m02-dfpanel-note", text: tf("VarType") }), type],
children: [
el("span", { className: "ui-text ui-text--muted", text: tf("VarType") }),
type,
],
}),
formField(st, "VarUnit", "unit"),
...(form.type === "number"
@@ -413,7 +426,11 @@ function drawVars(st: State): void {
: []),
...(form.type === "choice" ? [formField(st, "VarChoices", "choices")] : []),
formField(st, "VarExample", "example"),
el("span", { className: "m02-dfpanel-cell", text: `${tf("VarCell")} ${st.cell}` }),
el("span", {
className: "ui-badge ui-badge--pill ui-badge--neutral",
attrs: { "data-size": "24" },
text: `${tf("VarCell")} ${st.cell}`,
}),
createButton({
label: tf("Make"),
variant: "filled",
@@ -427,7 +444,7 @@ function drawVars(st: State): void {
onClick: () => ((st.form = null), drawVars(st)),
}),
...(form.why
? [el("span", { className: "m02-dfpanel-why", text: `⚠ ${form.why}` })]
? [el("span", { className: "ui-text ui-text--warning", text: `⚠ ${form.why}` })]
: []),
],
}),
@@ -441,7 +458,7 @@ function drawBar(st: State): void {
const f = funcOf(st);
if (st.readOnly) {
st.parts.bar.replaceChildren(
el("span", { className: "m02-dfpanel-note", text: tf("ReadOnly") }),
el("span", { className: "ui-text ui-text--muted", text: tf("ReadOnly") }),
);
return;
}
@@ -456,8 +473,12 @@ function drawBar(st: State): void {
size: "sm",
onClick: () => setOutput(st, st.cell),
}),
el("span", { className: "m02-dfpanel-chip is-out", text: outNow }),
el("span", { className: "m02-dfpanel-note", text: tf("OutOnly") }),
el("span", {
className: "ui-badge ui-badge--pill ui-badge--success m02-dfpanel-chip is-out",
attrs: { "data-size": "24" },
text: outNow,
}),
el("span", { className: "ui-text ui-text--muted", text: tf("OutOnly") }),
],
}),
...(f?.output.cell
@@ -482,7 +503,7 @@ function drawBar(st: State): void {
onClick: () => putFormula(st, formula),
}),
),
el("span", { className: "m02-dfpanel-note", text: tf("FrameHint") }),
el("span", { className: "ui-text ui-text--muted", text: tf("FrameHint") }),
],
}),
);
@@ -647,7 +668,10 @@ export function renderFuncPanel(
got: el("div", { className: "m02-dfpanel-row" }),
vars: el("div", { className: "m02-dfpanel-vars" }),
bar: el("div", { className: "m02-dfpanel-bar" }),
msg: el("p", { className: "m02-dfpanel-msg", attrs: { role: "status" } }),
msg: el("p", {
className: "ui-banner ui-banner--warning m02-dfpanel-msg",
attrs: { role: "status" },
}),
sheet: el("div", { className: "m02-dfpanel-sheet", text: tf("Loading") }),
};
const st: State = {
@@ -42,31 +42,9 @@
flex-wrap: wrap;
gap: var(--spacing-8);
}
.m02-dfip__chip,
.m02-dfip__row {
/* 고르는 줄 = 목록 줄 부품(ui-list-row) — 여기는 안 여백 · 두 칸 벌림만 */
button.m02-dfip__row {
padding: var(--spacing-8) var(--spacing-12);
border: 2px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface);
color: var(--color-text);
font: inherit;
cursor: pointer;
}
.m02-dfip__chip {
border-radius: var(--radius-pills);
padding: var(--spacing-4) var(--spacing-12);
}
.m02-dfip__chip.is-on {
border-color: var(--color-accent);
outline: 2px solid var(--color-accent);
}
.m02-dfip__chip:hover,
.m02-dfip__chip:focus-visible,
.m02-dfip__row:hover,
.m02-dfip__row:focus-visible {
border-color: var(--color-accent);
}
.m02-dfip__row {
display: flex;
flex: 1 1 auto;
gap: var(--spacing-8);
@@ -104,12 +82,9 @@
.m02-dfip__value {
color: var(--color-accent);
}
.m02-dfip__tag {
/* 태그 = 배지 부품 — 이름 뒤 띄움만 */
.m02-dfip__name .ui-badge {
margin-left: var(--spacing-8);
padding: 0 var(--spacing-8);
border: 1px solid var(--color-border);
border-radius: var(--radius-pills);
font-size: var(--text-caption);
}
.m02-dfip__new,
.m02-dfip__form {
@@ -124,19 +99,17 @@
cursor: pointer;
font-weight: 600;
}
/* 미리 보기 = 표 부품 데이터표 — 여기는 줄바꿈 막기 · 가운데 · 찾은 줄만 */
.m02-dfip__preview table {
border-collapse: collapse;
font-size: var(--text-body-sm);
width: auto;
}
.m02-dfip__preview th,
.m02-dfip__preview td {
padding: 2px 8px;
border: 1px solid var(--color-border);
white-space: nowrap;
text-align: center;
}
.m02-dfip__preview tr.is-hit td {
background: rgba(240, 190, 40, 0.35);
background: color-mix(in srgb, var(--color-warning) 35%, transparent);
font-weight: 700;
}
.m02-dfip__preview {
@@ -38,6 +38,10 @@ import { renderMaterial } from "./M02_DF_InputPanel_Material";
import { renderTablePick } from "./M02_DF_InputPanel_Table";
import { renderVariables } from "./M02_DF_InputPanel_Vars";
import "./M02_DF_InputPanel.css";
import "@ui/ui_template_text.css";
import "@ui/ui_template_badge.css";
import "@ui/ui_template_list_row.css";
import "@ui/ui_template_toolbar.css";
/** Api 모양 — 서버 Api 가 채워지면(사용자 요소 = Model 모양 · 값 물음 = ref) 그 모양을 그대로 씀 */
interface Answer {
@@ -125,7 +129,7 @@ export function renderInputPanel(
host.className = "m02-dfip";
const box = lg.boxes.find((b) => b.id === boxId);
if (!box || box.kind !== "input") {
host.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("NotInput") }));
host.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: t("NotInput") }));
return;
}
// 상자 단위가 고른 요소 · 변수 단위와 다르면(견본 등) 맞춰 다시 그림 — 맞으면 바로 지나감
@@ -246,7 +250,15 @@ function drawChosen(
el("span", {
className: "m02-dfip__name",
children: [
...(rule ? [el("span", { className: "m02-dfip__tag", text: t("RuleTag") })] : []),
...(rule
? [
el("span", {
className: "ui-badge ui-badge--pill ui-badge--neutral",
attrs: { "data-size": "20" },
text: t("RuleTag"),
}),
]
: []),
el("strong", { text: ` ${e.name}` }),
...(rule && e.rule
? [
@@ -274,7 +286,7 @@ function drawChosen(
],
});
})
: [el("p", { className: "m02-dfip__muted", text: t("NoneChosen") })]),
: [el("p", { className: "ui-text ui-text--muted", text: t("NoneChosen") })]),
);
const items = els
.filter((e) => e.mode !== "rule")
@@ -331,7 +343,7 @@ async function drawFind(
let seq = 0;
const load = async (): Promise<void> => {
const mine = ++seq;
list.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("Loading") }));
list.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: t("Loading") }));
try {
const word = q.input.value.trim();
const page = await fetchRows(file, 1, 40, word, false, sub, detail);
@@ -341,11 +353,11 @@ async function drawFind(
list.replaceChildren(
...(rows.length
? rows.map((r) => rowOf(r))
: [el("p", { className: "m02-dfip__muted", text: t("NoRow") })]),
: [el("p", { className: "ui-text ui-text--muted", text: t("NoRow") })]),
);
} catch (e) {
if (mine === seq)
list.replaceChildren(el("p", { className: "m02-dfip__muted", text: plain(e) }));
list.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: plain(e) }));
}
};
const rowOf = (r: M01Row): HTMLElement => {
@@ -361,7 +373,7 @@ async function drawFind(
const v = valueOf(group, r, field.select.value || "가격");
const unitText = group === "기계" ? "" : String(r["단위"] ?? "");
const b = el("button", {
className: "m02-dfip__row",
className: "m02-dfip__row ui-list-row ui-sidebar-row",
attrs: { type: "button" },
children: [
el("span", {
@@ -390,7 +402,7 @@ async function drawFind(
subBox.root.hidden = !subs.length;
bar.push(subBox.root, detailBox.root);
} catch (e) {
host.replaceChildren(el("p", { className: "m02-dfip__muted", text: plain(e) }));
host.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: plain(e) }));
return;
}
let timer = 0;
@@ -399,7 +411,7 @@ async function drawFind(
() => (window.clearTimeout(timer), (timer = window.setTimeout(load, 250))),
);
host.replaceChildren(
el("div", { className: "m02-dfip__bar", children: [...bar, q.root] }),
el("div", { className: "ui-toolbar m02-dfip__bar", children: [...bar, q.root] }),
count,
list,
);
@@ -35,6 +35,10 @@ import {
wouldCycle,
type MasterLogicBrief,
} from "./M02_DF_InputPanel_Build";
import "@ui/ui_template_text.css";
import "@ui/ui_template_badge.css";
import "@ui/ui_template_list_row.css";
import "@ui/ui_template_toolbar.css";
const TEXT = {
Help: [
@@ -183,7 +187,7 @@ export function renderLogicInput(
...(shown.length
? shown.map((i) => {
const b = el("button", {
className: "m02-dfip__row",
className: "m02-dfip__row ui-list-row ui-sidebar-row",
attrs: {
type: "button",
...(i.blocked ? { disabled: "true", title: (i.reasons ?? []).join(" · ") } : {}),
@@ -200,7 +204,8 @@ export function renderLogicInput(
text: ` ${aliasNote(matchLogic(q.input.value, i).alias)}`,
}),
el("span", {
className: "m02-dfip__tag",
className: "ui-badge ui-badge--pill ui-badge--neutral",
attrs: { "data-size": "20" },
text: i.blocked ? t("Blocked") : i.상세구분,
}),
],
@@ -211,7 +216,7 @@ export function renderLogicInput(
b.addEventListener("click", () => void pick(i));
return b;
})
: [el("p", { className: "m02-dfip__muted", text: t("None") })]),
: [el("p", { className: "ui-text ui-text--muted", text: t("None") })]),
);
};
@@ -253,10 +258,13 @@ export function renderLogicInput(
window.clearTimeout(timer);
timer = window.setTimeout(() => ((page = 1), draw()), 200);
});
list.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("Loading") }));
list.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: t("Loading") }));
host.replaceChildren(
el("p", { className: "m02-dfip__muted", text: t("Help") }),
el("div", { className: "m02-dfip__bar", children: [bookBox.root, chapterBox.root, q.root] }),
el("p", { className: "ui-text ui-text--muted", text: t("Help") }),
el("div", {
className: "ui-toolbar m02-dfip__bar",
children: [bookBox.root, chapterBox.root, q.root],
}),
info,
list,
el("div", { className: "m02-dfip__chips", children: [prev, next] }),
@@ -268,7 +276,7 @@ export function renderLogicInput(
fillChapters();
draw();
},
(e) => list.replaceChildren(el("p", { className: "m02-dfip__muted", text: plain(e) })),
(e) => list.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: plain(e) })),
);
}
@@ -282,7 +290,7 @@ function showChosen(
): void {
const read = readNotes.get(ref.key);
const values = new Map<string, HTMLElement>();
const errors = el("p", { className: "m02-dfip__muted is-bad" });
const errors = el("p", { className: "ui-text ui-text--error" });
const line = (name: string, unit: string, valueNode?: HTMLElement, extra = ""): HTMLElement =>
el("div", {
className: "m02-dfip__item",
@@ -300,7 +308,7 @@ function showChosen(
el("h5", { text: `${t("Chosen")} — ${ref.name ?? ref.key}` }),
...(read?.converted
? [
el("p", { className: "m02-dfip__muted", text: t("Converted") }),
el("p", { className: "ui-text ui-text--muted", text: t("Converted") }),
...(read.notes.length
? [
el("details", {
@@ -320,14 +328,14 @@ function showChosen(
values.set(o.name, v);
return line(o.name, o.unit, v);
})
: [el("p", { className: "m02-dfip__muted", text: t("NoOut") })]),
: [el("p", { className: "ui-text ui-text--muted", text: t("NoOut") })]),
errors,
el("h5", { text: `${t("Designs")} · ${ref.designs.length}` }),
...(ref.designs.length
? ref.designs.map((d) =>
line(d.name, d.unit, undefined, String(ref.examples?.[d.name] ?? "")),
)
: [el("p", { className: "m02-dfip__muted", text: t("NoDesign") })]),
: [el("p", { className: "ui-text ui-text--muted", text: t("NoDesign") })]),
el("div", {
className: "m02-dfip__chips",
children: [
@@ -30,6 +30,10 @@ import {
variablePills,
type MatItem,
} from "./M02_DF_InputPanel_Build";
import "@ui/ui_template_text.css";
import "@ui/ui_template_badge.css";
import "@ui/ui_template_list_row.css";
import "@ui/ui_template_toolbar.css";
const TEXT = {
Chosen: ["넣은 재료 — 알약마다 이름 · 값 · 단위", "Chosen materials"],
@@ -159,7 +163,10 @@ function scopeBoxes(
() => undefined,
);
return {
root: el("div", { className: "m02-dfip__bar", children: [subBox.root, detailBox.root] }),
root: el("div", {
className: "ui-toolbar m02-dfip__bar",
children: [subBox.root, detailBox.root],
}),
get: () => ({ ...state }),
};
}
@@ -190,7 +197,7 @@ function itemList(
});
const load = async (): Promise<void> => {
const mine = ++seq;
list.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("Loading") }));
list.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: t("Loading") }));
try {
const r = await searchMaterials({ q: q.input.value.trim(), ...scope(), page, size: 12 });
if (mine !== seq) return;
@@ -208,7 +215,7 @@ function itemList(
...(r.items.length
? r.items.map((it) => {
const b = el("button", {
className: "m02-dfip__row",
className: "m02-dfip__row ui-list-row ui-sidebar-row",
attrs: { type: "button" },
children: [
el("span", {
@@ -227,11 +234,11 @@ function itemList(
b.addEventListener("click", () => onPick(it));
return b;
})
: [el("p", { className: "m02-dfip__muted", text: t("NoRow") })]),
: [el("p", { className: "ui-text ui-text--muted", text: t("NoRow") })]),
);
} catch {
if (mine === seq)
list.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("Failed") }));
list.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: t("Failed") }));
}
};
let timer = 0;
@@ -398,9 +405,13 @@ export function renderMaterial(
el("span", {
className: "m02-dfip__name",
children: [
el("span", { className: "m02-dfip__tag", text: kind }),
el("span", {
className: "ui-badge ui-badge--pill ui-badge--neutral",
attrs: { "data-size": "20" },
text: kind,
}),
el("strong", { text: ` ${e.name}` }),
el("div", { className: "m02-dfip__muted", text: summaryOf(e, lg) }),
el("div", { className: "ui-text ui-text--muted", text: summaryOf(e, lg) }),
note,
],
}),
@@ -435,7 +446,7 @@ export function renderMaterial(
);
return row;
})
: [el("p", { className: "m02-dfip__muted", text: t("None") })]),
: [el("p", { className: "ui-text ui-text--muted", text: t("None") })]),
);
};
@@ -455,7 +466,7 @@ export function renderMaterial(
const drawBody = (): void => {
if (mode === "fixed") {
body.replaceChildren(
el("p", { className: "m02-dfip__muted", text: t("FixedHelp") }),
el("p", { className: "ui-text ui-text--muted", text: t("FixedHelp") }),
itemList((it) =>
on.change(
addEl(lg, {
@@ -520,7 +531,7 @@ export function renderMaterial(
onChange: (v) => ((how = v as "plain" | "template"), drawBody()),
});
const tpl = templateBox(lg, () => void refresh());
const defNote = el("p", { className: "m02-dfip__muted" });
const defNote = el("p", { className: "ui-text ui-text--muted" });
const build = (): CfElementRef => {
const sc = scope.get();
if (mode === "rule" && howBox.select.value === "template") {
@@ -603,7 +614,7 @@ export function renderMaterial(
? [tpl.root]
: pills.length
? [fieldBox.root, opBox.root, pillBox.root]
: [el("p", { className: "m02-dfip__muted is-bad", text: t("NoPill") })]),
: [el("p", { className: "ui-text ui-text--error", text: t("NoPill") })]),
]
: [
el("h5", { text: t("Default") }),
@@ -21,6 +21,10 @@ import {
variablePills,
type GroupRuleField,
} from "./M02_DF_InputPanel_Build";
import "@ui/ui_template_text.css";
import "@ui/ui_template_badge.css";
import "@ui/ui_template_list_row.css";
import "@ui/ui_template_toolbar.css";
const TEXT = {
Help: [
@@ -128,11 +132,11 @@ export function renderGroupRule(
},
});
host.replaceChildren(
el("p", { className: "m02-dfip__muted", text: t("Help") }),
el("p", { className: "ui-text ui-text--muted", text: t("Help") }),
name.root,
...(pills.length
? [field.root, pill.root, note, add]
: [el("p", { className: "m02-dfip__muted is-bad", text: t("NoPill") })]),
: [el("p", { className: "ui-text ui-text--error", text: t("NoPill") })]),
);
if (pills.length) void refresh();
}
@@ -15,6 +15,11 @@ import {
} from "@ui/ui_template_elements";
import type { CfGroup, CfLogic } from "./M02_DF_Model";
import { setTable } from "./M02_DF_InputPanel_Build";
import "@ui/ui_template_text.css";
import "@ui/ui_template_badge.css";
import "@ui/ui_template_list_row.css";
import "@ui/ui_template_toolbar.css";
import "@ui/ui_template_table.css";
const TEXT = {
Book: ["적용 품셈", "Standard"],
@@ -120,6 +125,7 @@ function previewOf(tb: RawTable, row?: number): HTMLElement {
className: "m02-dfip__preview",
children: [
el("table", {
className: "ui-table ui-table--data",
children: [
el("tr", { children: cols.map((c) => el("th", { text: c })) }),
...body.map((r, i) =>
@@ -131,7 +137,7 @@ function previewOf(tb: RawTable, row?: number): HTMLElement {
],
}),
...(tb.줄.length > body.length
? [el("p", { className: "m02-dfip__muted", text: `… ${tb.줄.length} 줄` })]
? [el("p", { className: "ui-text ui-text--muted", text: `… ${tb.줄.length} 줄` })]
: []),
...(tb.주?.length
? [
@@ -172,7 +178,7 @@ export function renderTablePick(
let seq = 0;
const load = async (): Promise<void> => {
const mine = ++seq;
list.replaceChildren(el("p", { className: "m02-dfip__muted", text: t("Loading") }));
list.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: t("Loading") }));
try {
const r = await get<{ sections: SectionItem[] }>("/sections", {
book,
@@ -184,7 +190,7 @@ export function renderTablePick(
...(r.sections.length
? r.sections.map((s) => {
const b = el("button", {
className: "m02-dfip__row",
className: "m02-dfip__row ui-list-row ui-sidebar-row",
attrs: { type: "button" },
children: [
el("strong", { text: `${s.section} ${s.title}` }),
@@ -197,7 +203,7 @@ export function renderTablePick(
b.addEventListener("click", () => void pickSection(s));
return b;
})
: [el("p", { className: "m02-dfip__muted", text: t("NoSection") })]),
: [el("p", { className: "ui-text ui-text--muted", text: t("NoSection") })]),
);
} catch (e) {
if (mine === seq) showToast(e instanceof Error ? e.message : t("Failed"), "error");
@@ -218,7 +224,7 @@ export function renderTablePick(
...(heads.length
? heads.map((h) => {
const b = el("button", {
className: "m02-dfip__row",
className: "m02-dfip__row ui-list-row ui-sidebar-row",
attrs: { type: "button" },
children: [
el("strong", { text: h.이름 }),
@@ -242,7 +248,7 @@ export function renderTablePick(
});
return b;
})
: [el("p", { className: "m02-dfip__muted", text: t("NoTable") })]),
: [el("p", { className: "ui-text ui-text--muted", text: t("NoTable") })]),
);
} catch (e) {
showToast(e instanceof Error ? e.message : t("Failed"), "error");
@@ -8,6 +8,10 @@ import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createInputField, createSelectField, el } from "@ui/ui_template_elements";
import type { CfLogic, CfPort } from "./M02_DF_Model";
import { templateSlots, variablePills } from "./M02_DF_InputPanel_Build";
import "@ui/ui_template_text.css";
import "@ui/ui_template_badge.css";
import "@ui/ui_template_list_row.css";
import "@ui/ui_template_toolbar.css";
const TEXT = {
Template: ["검색어 틀 — 자리는 {이름} 으로", "Search template — slots as {name}"],
@@ -68,8 +72,8 @@ export function templateBox(lg: CfLogic, onChange: () => void): TemplateBox {
picks.set(n, sel.select);
return sel.root;
})
: [el("p", { className: "m02-dfip__muted is-bad", text: t("NoPill") })]
: [el("p", { className: "m02-dfip__muted", text: t("NoSlot") })]),
: [el("p", { className: "ui-text ui-text--error", text: t("NoPill") })]
: [el("p", { className: "ui-text ui-text--muted", text: t("NoSlot") })]),
);
};
tpl.input.addEventListener("input", () => (draw(), onChange()));
@@ -77,7 +81,11 @@ export function templateBox(lg: CfLogic, onChange: () => void): TemplateBox {
return {
root: el("div", {
className: "m02-dfip__stack",
children: [el("p", { className: "m02-dfip__muted", text: t("Help") }), tpl.root, slotBox],
children: [
el("p", { className: "ui-text ui-text--muted", text: t("Help") }),
tpl.root,
slotBox,
],
}),
get: () => ({
template: tpl.input.value.trim(),
@@ -9,6 +9,10 @@ import { createButton, createInputField, createSelectField, el } from "@ui/ui_te
import type { CfLogic, CfVariable } from "./M02_DF_Model";
import { NO_UNIT, createUnitPick } from "./M02_DF_UnitPick";
import { checkVariable, putVariable, removeVariable } from "./M02_DF_InputPanel_Build";
import "@ui/ui_template_text.css";
import "@ui/ui_template_badge.css";
import "@ui/ui_template_list_row.css";
import "@ui/ui_template_toolbar.css";
const TEXT = {
List: ["변수 — 알약마다 하나", "Variables — one pill each"],
@@ -201,7 +205,7 @@ export function renderVariables(
],
}),
)
: [el("p", { className: "m02-dfip__muted", text: t("None") })];
: [el("p", { className: "ui-text ui-text--muted", text: t("None") })];
host.replaceChildren(el("h5", { text: t("List") }), ...list, form());
};
draw();
@@ -24,24 +24,13 @@
font-size: var(--text-body-sm);
}
.m02-dflink-value {
padding: 2px var(--spacing-12);
border: 1px solid var(--color-border);
border-radius: var(--radius-pills);
background: var(--color-surface-raised);
font-weight: var(--font-weight-medium);
}
.m02-dflink-num {
width: 7em;
}
/* 겉 = 띠 알림 부품 */
.m02-dflink-warn {
margin: 0;
padding: var(--spacing-8) var(--spacing-12);
border-left: 4px solid var(--color-warning);
background: var(--color-surface-raised);
font-weight: var(--font-weight-medium);
}
/* 알약 판 */
@@ -72,11 +61,3 @@
gap: var(--spacing-8);
align-items: center;
}
.m02-dfpill-rule {
padding: 0 var(--spacing-8);
border: 1px dashed var(--color-border);
border-radius: var(--radius-pills);
color: var(--color-text-secondary);
font-size: var(--text-body-sm);
}
@@ -36,6 +36,9 @@ import {
} from "./M02_DF_Detail";
import { suggestPlaces } from "./M02_DF_UnitPick";
import "./M02_DF_LinkPanel.css";
import "@ui/ui_template_text.css";
import "@ui/ui_template_banner.css";
import "@ui/ui_template_badge.css";
const TEXT = {
Title: ["선 규칙", "Line rule"],
@@ -248,7 +251,8 @@ function lookupEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "lookup" }>): HTML
select(ctx, colOpts, rule.col, (v) => put({ col: v }), tk("Take")),
),
);
if (!cols.length) out.push(el("p", { className: "m02-dflink-note", text: tk("NoHeader") }));
if (!cols.length)
out.push(el("p", { className: "ui-text ui-text--muted", text: tk("NoHeader") }));
return out;
}
@@ -289,7 +293,7 @@ function gateValue(ctx: Ctx, c: CfGateCond, onPick: (v: number | string) => void
if (range && Number.isFinite(n) && (n < range[0] || n > range[1])) {
out.push(
el("span", {
className: "m02-dflink-warn",
className: "ui-text ui-text--warning",
text: `⚠ ${tk("OutRange", `${range[0]} ~ ${range[1]}`)}`,
}),
);
@@ -302,7 +306,9 @@ function gateEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "gate" }>): HTMLElem
const put = (conds: CfGateCond[], join = g.join): void => setRule(ctx, makeGate(conds, join));
const edit = (i: number, patch: Partial<CfGateCond>): void =>
put(g.conds.map((c, j) => (j === i ? { ...c, ...patch } : c)));
const out: HTMLElement[] = [el("p", { className: "m02-dflink-note", text: tk("GateHint") })];
const out: HTMLElement[] = [
el("p", { className: "ui-text ui-text--muted", text: tk("GateHint") }),
];
g.conds.forEach((c, i) => {
out.push(
row(
@@ -380,7 +386,7 @@ function gateEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "gate" }>): HTMLElem
function scaleEditor(ctx: Ctx, rule: Extract<CfRule, { kind: "scale" }>): HTMLElement[] {
return [
el("p", { className: "m02-dflink-note", text: tk("ScaleHint") }),
el("p", { className: "ui-text ui-text--muted", text: tk("ScaleHint") }),
row(
select(
ctx,
@@ -502,10 +508,18 @@ export function renderLinkPanel(
parts.push(
row(
el("span", { className: "m02-dflink-label", text: tk("Before") }),
el("span", { className: "m02-dflink-value", text: pv.before }),
el("span", {
className: "ui-badge ui-badge--pill ui-badge--neutral",
attrs: { "data-size": "20" },
text: pv.before,
}),
"→",
el("span", { className: "m02-dflink-label", text: tk("After") }),
el("span", { className: "m02-dflink-value", text: pv.after }),
el("span", {
className: "ui-badge ui-badge--pill ui-badge--neutral",
attrs: { "data-size": "20" },
text: pv.after,
}),
),
row(
el("span", { className: "m02-dflink-label", text: tk("Label") }),
@@ -518,6 +532,10 @@ export function renderLinkPanel(
if (!rule.conds.length) warns.push(tk("WarnLookup"));
if (!rule.col) warns.push(tk("WarnTake"));
}
parts.push(...warns.map((w) => el("p", { className: "m02-dflink-warn", text: `⚠ ${w}` })));
parts.push(
...warns.map((w) =>
el("p", { className: "ui-banner ui-banner--warning m02-dflink-warn", text: `⚠ ${w}` }),
),
);
host.replaceChildren(...parts);
}
@@ -53,23 +53,8 @@
}
.m02-dfout__input {
box-sizing: border-box;
width: 160px;
max-width: 100%;
height: var(--control-height-sm);
padding: 3px 6px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-canvas);
color: var(--color-text-body);
font: inherit;
font-size: var(--text-body-sm);
}
.m02-dfout__warn {
margin: var(--spacing-4) 0 0;
color: var(--color-danger);
font-size: var(--text-body-sm);
}
.m02-dfout__muted {
@@ -78,12 +63,6 @@
font-size: var(--text-caption);
}
.m02-dfout__msg {
margin: var(--spacing-4) 0 0;
color: var(--color-danger);
font-size: var(--text-caption);
}
.m02-dfout__round {
margin: var(--spacing-8) 0;
color: var(--color-text-body);
@@ -13,6 +13,7 @@ import { portsOut, type CfBox, type CfCost, type CfLogic, type CfRound } from ".
import { defaultItemRound, roundText } from "./M02_DF_Round";
import { createUnitPick, suggestPlaces } from "./M02_DF_UnitPick";
import "./M02_DF_OutputPanel.css";
import "@ui/ui_template_text.css";
const TEXT = {
Title: ["출력", "Output"],
@@ -83,7 +84,7 @@ export function renderOutputPanel(
host.replaceChildren();
return;
}
const msg = el("p", { className: "m02-dfout__msg" });
const msg = el("p", { className: "ui-text ui-text--error" });
const emit = (make: () => CfLogic): void => {
msg.textContent = "";
try {
@@ -133,14 +134,14 @@ export function renderOutputPanel(
} else {
got.replaceChildren(
el("strong", { text: to("Got") }),
el("p", { className: "m02-dfout__warn", text: to("NoLink") }),
el("p", { className: "ui-text ui-text--error", text: to("NoLink") }),
);
}
/* 이름 · 비용 종류 · 단위 */
const nameBox = el("input", {
className: "m02-dfout__input",
attrs: { type: "text", "aria-label": to("Name") },
className: "ui-input m02-dfout__input",
attrs: { "data-size": "32", type: "text", "aria-label": to("Name") },
});
nameBox.value = box.name;
nameBox.addEventListener("change", () => {
@@ -283,7 +284,9 @@ export function renderOutputPanel(
],
}),
roundBlock,
...(cost === "값" ? [el("p", { className: "m02-dfout__muted", text: to("PlainHint") })] : []),
...(cost === "값"
? [el("p", { className: "ui-text ui-text--muted", text: to("PlainHint") })]
: []),
msg,
);
}
+2 -1
View File
@@ -16,6 +16,7 @@ import type { CfLogic } from "./M02_DF_Model";
import { createPaper } from "./M02_DF_Paper";
import { renderStandards } from "./M02_DF_Standards";
import "./M02_DF_Panel.css";
import "@ui/ui_template_toolbar.css";
const TEXT = {
Big: ["크게 보기", "Full screen"],
@@ -156,7 +157,7 @@ export function mountFlowPanel(host: HTMLElement, opts: FlowPanelOpts): FlowPane
attrs: { open: "" },
children: [el("summary", { text: tf("Cost") })],
});
const tools = el("div", { className: "m02-dfp__tools", children: [undo] });
const tools = el("div", { className: "ui-toolbar m02-dfp__tools", children: [undo] });
const open = el("div", {
className: "m02-dfp__open",
children: [head, tools, note, diagram, detail, costBox],
+3 -1
View File
@@ -32,6 +32,7 @@ import {
type DfPick,
} from "./M02_DF_Entry_Detail";
import "./M02_DF_Paper.css";
import "@ui/ui_template_text.css";
const TEXT = {
Undo: ["실행 취소", "Undo"],
@@ -133,7 +134,8 @@ export function createPaper(opts: PaperOpts): Paper {
});
lock.disabled = readOnly;
detail.body.append(lock);
if (readOnly) detail.root.prepend(el("p", { className: "m02-df__muted", text: tp("ReadOnly") }));
if (readOnly)
detail.root.prepend(el("p", { className: "ui-text ui-text--muted", text: tp("ReadOnly") }));
const evalNow = (): CfEvalResult => {
try {
@@ -22,6 +22,9 @@ import {
type DetailOptions,
} from "./M02_DF_Detail";
import "./M02_DF_LinkPanel.css";
import "@ui/ui_template_text.css";
import "@ui/ui_template_banner.css";
import "@ui/ui_template_badge.css";
const TEXT = {
Name: ["이름", "Name"],
@@ -55,7 +58,7 @@ function steps(
empty: string,
onBox?: (id: string) => void,
): HTMLElement {
if (!list.length) return el("p", { className: "m02-dflink-note", text: empty });
if (!list.length) return el("p", { className: "ui-text ui-text--muted", text: empty });
const pill = (box: string, name: string): HTMLElement | string =>
onBox
? createButton({
@@ -75,7 +78,14 @@ function steps(
pill(link.from.box, link.from.name),
"→",
...(s.rule
? [el("span", { className: "m02-dfpill-rule", text: `(${labelOf(s.rule)})` }), "→"]
? [
el("span", {
className: "ui-badge ui-badge--pill ui-badge--neutral",
attrs: { "data-size": "20" },
text: `(${labelOf(s.rule)})`,
}),
"→",
]
: []),
pill(link.to.box, link.to.name),
]
@@ -121,7 +131,14 @@ export function renderPillPanel(
],
}),
line(tp("Name"), port),
line(tp("Value"), el("span", { className: "m02-dflink-value", text: showValue(value) })),
line(
tp("Value"),
el("span", {
className: "ui-badge ui-badge--pill ui-badge--neutral",
attrs: { "data-size": "20" },
text: showValue(value),
}),
),
line(tp("Unit"), unit || "-"),
el("div", {
children: [
@@ -139,8 +156,13 @@ export function renderPillPanel(
children: [
el("div", { className: "m02-dflink-label", text: tp("Breaks") }),
...(breaks.length
? breaks.map((b) => el("p", { className: "m02-dflink-warn", text: `⚠ ${b}` }))
: [el("p", { className: "m02-dflink-note", text: tp("NoBreak") })]),
? breaks.map((b) =>
el("p", {
className: "ui-banner ui-banner--warning m02-dflink-warn",
text: `⚠ ${b}`,
}),
)
: [el("p", { className: "ui-text ui-text--muted", text: tp("NoBreak") })]),
],
}),
);
@@ -7,6 +7,7 @@
import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, el } from "@ui/ui_template_elements";
import type { CfRound } from "./M02_DF_Model";
import "@ui/ui_template_text.css";
const TEXT = {
Label: ["끝수", "Rounding"],
@@ -65,7 +66,7 @@ export function createRoundRow(opts: RoundRowOptions): HTMLElement {
});
if (opts.suggest !== null && !now && !opts.readOnly) {
row.append(
el("span", { className: "m02-dfpanel-note", text: tr("Suggest", opts.suggest) }),
el("span", { className: "ui-text ui-text--muted", text: tr("Suggest", opts.suggest) }),
createButton({
label: tr("Apply"),
variant: "ghost",
@@ -16,30 +16,13 @@
margin: 0 auto 0 0;
}
.m02-df-std__state,
.m02-df-std__hint,
.m02-df-std__line {
color: var(--color-text-muted, #6b7280);
}
.m02-df-std__hint {
margin: 0;
}
.m02-df-std__scroll {
overflow-x: auto;
}
.m02-df-std__table {
border-collapse: collapse;
width: 100%;
color: var(--color-text-muted);
}
/* 겉 = 표 부품 데이터표(ui-table-wrap) · 여기는 줄바꿈 막기 · 누르는 칸만 */
.m02-df-std__table th,
.m02-df-std__table td {
border: 1px solid var(--color-border, #e5e7eb);
padding: 2px 6px;
text-align: left;
white-space: nowrap;
}
@@ -47,10 +30,6 @@
cursor: pointer;
}
.m02-df-std__table th {
background: var(--color-surface-muted, rgba(127, 127, 127, 0.08));
}
.m02-df-std__notes {
margin: 0;
padding-left: 0;
@@ -15,6 +15,8 @@ import { cfStandards, cfStandardsReset, cfStandardsSave, type CfStandards } from
import unitDefault from "./_standard/단위표준.json";
import moneyDefault from "./_standard/금액단위표준.json";
import "./M02_DF_Standards.css";
import "@ui/ui_template_text.css";
import "@ui/ui_template_table.css";
const TEXT = {
Title: ["표준 표", "Standards"],
@@ -198,7 +200,7 @@ export function renderStandards(
): void {
let work = clone(current);
let dirty = false;
const state = el("span", { className: "m02-df-std__state" });
const state = el("span", { className: "ui-text ui-text--muted m02-df-std__state" });
const save = createButton({ label: tx("Save"), size: "sm", onClick: () => void doSave() });
const reset = createButton({
label: tx("Reset"),
@@ -250,10 +252,10 @@ export function renderStandards(
const table = (heads: string[], rows: HTMLElement[][]): HTMLElement =>
el("div", {
className: "m02-df-std__scroll",
className: "ui-table-wrap m02-df-std__scroll",
children: [
el("table", {
className: "m02-df-std__table",
className: "ui-table ui-table--data m02-df-std__table",
children: [
el("thead", {
children: [el("tr", { children: heads.map((h) => el("th", { text: h })) })],
@@ -350,7 +352,7 @@ export function renderStandards(
save,
],
}),
el("p", { className: "m02-df-std__hint", text: `${tx("Source")} · ${tx("Hint")}` }),
el("p", { className: "ui-text ui-text--muted", text: `${tx("Source")} · ${tx("Hint")}` }),
body,
],
}),
+3 -60
View File
@@ -41,30 +41,12 @@
.m02-dfsum__scroll {
max-width: 100%;
overflow-x: auto;
}
.m02-dfsum__table {
width: 100%;
min-width: 360px;
margin-top: var(--spacing-4);
border-collapse: collapse;
font-size: var(--text-body-sm);
}
.m02-dfsum__table th,
.m02-dfsum__table td {
padding: var(--spacing-4) var(--spacing-8);
border: 1px solid var(--color-border);
text-align: left;
vertical-align: middle;
}
.m02-dfsum__table th {
background: var(--color-surface);
color: var(--color-text-secondary);
font-weight: 600;
white-space: nowrap;
/* 겉 = 표 부품 데이터표(ui-table-wrap) · 여기는 최소 폭 · 수 칸 오른쪽만 */
.m02-dfsum__table {
min-width: 360px;
}
.m02-dfsum__table td.m02-dfsum__num {
@@ -91,42 +73,3 @@
color: var(--color-text-secondary);
font-size: var(--text-body-sm);
}
.m02-dfsum__input {
box-sizing: border-box;
width: 140px;
height: var(--control-height-sm);
padding: 3px 6px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-canvas);
color: var(--color-text-body);
font: inherit;
font-size: var(--text-body-sm);
}
.m02-dfsum__muted {
margin: 0;
color: var(--color-text-muted);
font-size: var(--text-caption);
}
.m02-dfsum__msg {
margin: var(--spacing-4) 0 0;
color: var(--color-danger);
font-size: var(--text-caption);
}
.m02-dfsum__problems {
margin: 0 0 var(--spacing-12);
padding: var(--spacing-8) var(--spacing-12);
border-left: 4px solid var(--color-danger);
background: var(--color-surface);
color: var(--color-text-body);
font-size: var(--text-body-sm);
}
.m02-dfsum__problems ul {
margin: var(--spacing-4) 0 0;
padding-left: var(--spacing-16);
}
@@ -22,6 +22,8 @@ import {
} from "./M02_DF_Round";
import { renderVerify, type CfSel } from "./M02_DF_Verify";
import "./M02_DF_Summary.css";
import "@ui/ui_template_text.css";
import "@ui/ui_template_table.css";
const TEXT = {
Per: ["{per} 당", "per {per}"],
@@ -115,10 +117,10 @@ export function renderSummary(
children: [
el("strong", { text: title }),
el("div", {
className: "m02-dfsum__scroll",
className: "ui-table-wrap m02-dfsum__scroll",
children: [
el("table", {
className: "m02-dfsum__table",
className: "ui-table ui-table--data m02-dfsum__table",
children: [
el("thead", {
children: [
@@ -162,14 +164,14 @@ export function renderSummary(
children: [
el("strong", { text: ts("Plain") }),
...plainValues.map(([k, v]) =>
el("p", { className: "m02-dfsum__muted", text: `${k} · ${showValue(v)}` }),
el("p", { className: "ui-text ui-text--muted", text: `${k} · ${showValue(v)}` }),
),
],
}),
]
: []),
el("p", {
className: "m02-dfsum__muted",
className: "ui-text ui-text--muted",
text: `${ts("RoundTotal")}: ${roundText(sumRound)}`,
}),
verifyHost,
@@ -27,30 +27,15 @@
overflow-y: auto;
}
/* 단위 줄 = 목록 줄 부품 · 화살표 키로 고른 줄(is-on)만 마우스 꼴 */
.m02-df-unitpick__item {
display: block;
width: 100%;
padding: var(--spacing-4) var(--spacing-8);
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.m02-df-unitpick__item.is-on,
.m02-df-unitpick__item:hover {
background: var(--color-surface-raised);
.m02-df-unitpick__item.is-on {
background: var(--color-hover);
}
.m02-df-unitpick__item.is-now {
font-weight: var(--font-weight-bold);
}
.m02-df-unitpick__empty {
padding: var(--spacing-8);
color: var(--color-text-muted);
font-size: var(--text-body-sm);
}
@@ -10,6 +10,8 @@ import { currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, el } from "@ui/ui_template_elements";
import { cfStandards } from "./M02_DF_Api";
import "./M02_DF_UnitPick.css";
import { createEmpty } from "@ui/ui_template_empty";
import "@ui/ui_template_list_row.css";
const TEXT = {
None: ["없음", "none"],
@@ -139,14 +141,14 @@ export function createUnitPick(opts: UnitPickOptions): HTMLElement {
...(shown.length
? shown.map((u, i) => {
const b = el("button", {
className: `m02-df-unitpick__item${i === at ? " is-on" : ""}${u === (opts.value || NO_UNIT) ? " is-now" : ""}`,
className: `ui-list-row ui-sidebar-row m02-df-unitpick__item${i === at ? " is-on" : ""}${u === (opts.value || NO_UNIT) ? " is-now" : ""}`,
text: label(u),
attrs: { type: "button", role: "option" },
});
b.addEventListener("click", () => pick(u));
return el("li", { children: [b] });
})
: [el("li", { className: "m02-df-unitpick__empty", text: tu("Empty") })]),
: [el("li", { children: [createEmpty({ text: tu("Empty"), small: true })] })]),
);
};
const close = (): void => {
+2 -18
View File
@@ -3,10 +3,9 @@
color: var(--color-text-body);
}
/* 끊긴 곳 = 띠 알림 부품(위험) · 여기는 제목 · 목록 쌓기만 */
.m02-dfver__breaks {
padding: var(--spacing-8) var(--spacing-12);
border-left: 4px solid var(--color-danger);
background: var(--color-surface);
flex-direction: column;
}
.m02-dfver__title {
@@ -33,17 +32,8 @@
}
.m02-dfver__input {
box-sizing: border-box;
width: 160px;
max-width: 100%;
height: var(--control-height-sm);
padding: 3px 6px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-canvas);
color: var(--color-text-body);
font: inherit;
font-size: var(--text-body-sm);
}
.m02-dfver__item {
@@ -111,12 +101,6 @@
font-size: var(--text-caption);
}
.m02-dfver__msg {
margin: var(--spacing-4) 0 0;
color: var(--color-danger);
font-size: var(--text-caption);
}
/* 쉬는 갈래 — 문제가 아니라 회색으로 */
.m02-dfver__rest {
padding: var(--spacing-8) var(--spacing-12);
+27 -13
View File
@@ -20,6 +20,9 @@ import {
type CfVariable,
} from "./M02_DF_Model";
import "./M02_DF_Verify.css";
import "@ui/ui_template_text.css";
import "@ui/ui_template_banner.css";
import "@ui/ui_template_toolbar.css";
const TEXT = {
Breaks: ["끊긴 곳", "Broken spots"],
@@ -133,8 +136,9 @@ function sampleField(
}).root;
} else {
const box = el("input", {
className: "m02-dfver__input",
className: "ui-input m02-dfver__input",
attrs: {
"data-size": "32",
type: "text",
inputmode: v.type === "number" ? "decimal" : "text",
"aria-label": v.name,
@@ -168,15 +172,20 @@ function slotRow(box: CfBox, e: CfElementRef, on: CfVerifyOn, lg: CfLogic): HTML
const panel = el("div", { className: "m02-dfver__panel" });
panel.hidden = true;
const q = el("input", {
className: "m02-dfver__input",
attrs: { type: "text", placeholder: tv("Search"), "aria-label": tv("Search") },
className: "ui-input m02-dfver__input",
attrs: {
"data-size": "32",
type: "text",
placeholder: tv("Search"),
"aria-label": tv("Search"),
},
});
q.value = e.scope?.찾는말 ?? "";
const list = el("ul", { className: "m02-dfver__list" });
const pager = el("div", { className: "m02-dfver__pager" });
const pager = el("div", { className: "ui-toolbar m02-dfver__pager" });
let page = 1;
const load = async (): Promise<void> => {
list.replaceChildren(el("li", { className: "m02-dfver__muted", text: tv("Loading") }));
list.replaceChildren(el("li", { className: "ui-text ui-text--muted", text: tv("Loading") }));
const got = await findMaterials({
q: q.value.trim(),
sub: e.scope?.구분,
@@ -199,7 +208,7 @@ function slotRow(box: CfBox, e: CfElementRef, on: CfVerifyOn, lg: CfLogic): HTML
],
}),
)
: [el("li", { className: "m02-dfver__muted", text: tv("NoHit") })]),
: [el("li", { className: "ui-text ui-text--muted", text: tv("NoHit") })]),
);
pager.replaceChildren(
createButton({
@@ -247,7 +256,7 @@ function slotRow(box: CfBox, e: CfElementRef, on: CfVerifyOn, lg: CfLogic): HTML
});
panel.append(
el("div", {
className: "m02-dfver__search",
className: "ui-toolbar m02-dfver__search",
children: [
q,
createButton({
@@ -326,7 +335,7 @@ export function renderVerify(
on: CfVerifyOn,
opts: { values?: CfValues; logics?: CfEvalOptions["logics"] } = {},
): void {
const msg = el("p", { className: "m02-dfver__msg" });
const msg = el("p", { className: "ui-text ui-text--error" });
const safe = (make: () => CfLogic): void => {
msg.textContent = "";
try {
@@ -355,7 +364,7 @@ export function renderVerify(
}),
),
}),
el("p", { className: "m02-dfver__muted", text: tv(hint) }),
el("p", { className: "ui-text ui-text--muted", text: tv(hint) }),
];
const problems = spots.filter((s) => s.level !== "쉼");
const resting = spots.filter((s) => s.level === "쉼");
@@ -363,7 +372,7 @@ export function renderVerify(
...(problems.length
? [
el("div", {
className: "m02-dfver__block m02-dfver__breaks",
className: "ui-banner ui-banner--danger m02-dfver__block m02-dfver__breaks",
children: [el("strong", { text: tv("Breaks") }), ...spotList(problems, "BreaksHint")],
}),
]
@@ -427,8 +436,13 @@ export function renderVerify(
const logics = designs.flatMap((d) => {
if (d.kind !== "다른 로직") return [];
const input = el("input", {
className: "m02-dfver__input",
attrs: { type: "text", placeholder: tv("OwnSample"), "aria-label": d.port },
className: "ui-input m02-dfver__input",
attrs: {
"data-size": "32",
type: "text",
placeholder: tv("OwnSample"),
"aria-label": d.port,
},
});
input.value = d.example === null || d.example === undefined ? "" : String(d.example);
input.addEventListener("change", () => safe(() => putLogicExample(d.box, d.port, input.value)));
@@ -479,7 +493,7 @@ export function renderVerify(
children: [
el("strong", { className: "m02-dfver__title", text: tv("Design") }),
...design,
el("p", { className: "m02-dfver__muted", text: tv("DesignHint") }),
el("p", { className: "ui-text ui-text--muted", text: tv("DesignHint") }),
msg,
],
}),
+1 -8
View File
@@ -19,12 +19,6 @@
min-width: 0;
}
.m02-inputval__missing {
margin: 0;
color: var(--color-text-secondary);
font-size: var(--text-caption);
}
.m02-inputval__logic {
display: flex;
flex-direction: column;
@@ -87,10 +81,9 @@
font-weight: 600;
}
/* 겉 = 보조 글 부품(ui-text--error) · 여기는 한 줄 차지만 */
.m02-inputval__error {
flex: 1 0 100%;
color: var(--color-danger);
font-size: var(--text-caption);
}
.m02-inputval__control[aria-invalid="true"] {
+2 -2
View File
@@ -81,7 +81,7 @@ export function createValueControl(
: ""
: String(value);
control.classList.toggle("is-auto", empty);
const error = el("span", { className: "m02-inputval__error" });
const error = el("span", { className: "ui-text ui-text--error m02-inputval__error" });
const out: ValueControl = { control, error, wasBad: false };
control.addEventListener(control instanceof HTMLSelectElement ? "change" : "input", () => {
const raw = control.value;
@@ -266,7 +266,7 @@ export function createInputsValueCard(opts: InputsValueOptions): InputsValueHand
const missing = data.list.빠진로직.length
? [
el("p", {
className: "m02-inputval__missing",
className: "ui-text ui-text--muted",
text: fill(VALUE_TEXT.missing, { value: data.list.빠진로직.join(", ") }),
}),
]
@@ -52,7 +52,9 @@ export function mountAreaSheet(host: HTMLElement, opts: AreaSheetOptions): AreaS
let picked = "A1";
let moved = false; // 누른 사이 고름이 바뀜 — 머리 · 굴림 막대를 눌렀다 뗀 것은 안 고름
let book: Workbook | null = null;
host.replaceChildren(el("p", { className: "m02-cellpick__now", text: TEXT.loading }));
host.replaceChildren(
el("p", { className: "ui-text ui-text--muted m02-cellpick__now", text: TEXT.loading }),
);
const choose = async (): Promise<void> => {
const at = picked;
// 엔진 조각은 고를 때만(정적 import 금지 — 계약 `spreadsheet_types.ts` 머리)
@@ -101,7 +103,7 @@ export function mountAreaSheet(host: HTMLElement, opts: AreaSheetOptions): AreaS
if (done) return;
host.replaceChildren(
el("p", {
className: "m02-cellpick__now",
className: "ui-text ui-text--muted m02-cellpick__now",
text: fill(TEXT.failed, error instanceof Error ? error.message : ""),
}),
);
@@ -89,7 +89,7 @@ export function createAreaDrawing(
/** 연결별로 마지막에 도면에 비춘 칸 글(없으면 저장된 `값`) */
const pushed = new Map<string, Record<string, string>>();
const note = (text: string): void =>
stage.replaceChildren(el("p", { className: "m02-apanel__note", text }));
stage.replaceChildren(el("p", { className: "ui-text ui-text--muted m02-apanel__note", text }));
const send = createButton({ label: TEXT.send, size: "sm", variant: "ghost" });
send.title = TEXT.sendTip;
@@ -9,6 +9,7 @@
import { createButton, createInputField, el } from "@ui/ui_template_elements";
import { openModal } from "@ui/ui_template_modal";
import type { CalcValue, LinkInfo, SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
import { createText } from "@ui/ui_template_text";
const TEXT = {
title: "칸 지정 — {value}",
@@ -62,7 +63,7 @@ export function openAreaLink(opts: AreaLinkOptions): void {
closeLabel: TEXT.close,
dialogClass: "m02-modal m02-area-link",
mount: (body) => {
const now = el("p", { className: "m02-area-link__now" });
const now = el("p", { className: "ui-text ui-text--muted m02-area-link__now" });
const paint = (): void => {
const text = linkedText(opts);
now.textContent = `${TEXT.now} · ${text ?? TEXT.none}`;
@@ -118,9 +119,7 @@ export function openAreaLink(opts: AreaLinkOptions): void {
paint();
body.append(
now,
...(opts.readOnly
? [el("p", { className: "m02-struct__empty", text: TEXT.readOnly })]
: []),
...(opts.readOnly ? [createText(TEXT.readOnly)] : []),
el("div", { className: "m02-area-link__row", children: [pick, unlink] }),
el("div", { className: "m02-area-link__row", children: [address.root, link] }),
);
@@ -19,13 +19,11 @@
.m02-apanel__note {
margin: var(--spacing-12);
color: var(--color-text-secondary);
}
/* 칸 지정 모달 — 지금 연결 한 줄 · [지금 고른 칸으로] [풀기] · 주소 칸 [잇기] */
.m02-area-link__now {
margin: 0 0 var(--spacing-12);
color: var(--color-text-secondary);
}
.m02-area-link__row {
@@ -54,7 +52,6 @@
.m02-cellpick__now {
margin: 0 0 var(--spacing-8);
color: var(--color-text-secondary);
}
.m02-cellpick__sheet {
@@ -29,11 +29,9 @@
flex: 1 1 0;
}
/* 겉 = 도구줄 부품(ui-toolbar) · 여기는 줄 사이 간격만 */
.m02-drawing__toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-8) var(--spacing-16, 16px);
gap: var(--spacing-8) var(--spacing-16);
}
.m02-drawing__title {
@@ -77,7 +75,7 @@
margin-top: 0;
width: 23rem;
max-width: calc(100vw - 16px);
box-shadow: 0 4px 16px rgb(0 0 0 / 25%);
box-shadow: var(--shadow-lg);
}
/* 탭 줄(82-1) — 그림 칸에 붙임 */
@@ -155,7 +155,8 @@ export function mountDrawingTemplate(
const root = document.createElement("div");
root.className = "m02-drawing";
const toolbar = document.createElement("div");
toolbar.className = "m02-drawing__toolbar";
toolbar.className = "ui-toolbar m02-drawing__toolbar"; // 도구줄 부품 겉
toolbar.setAttribute("role", "toolbar");
// 작도 영역 — 도면 내용이 이 칸 한가운데에 놓인다. 제목 줄에는 값이 다 보이는 단추 하나,
// 누르면 칸 넷이 작은 창으로 펼쳐진다(칸은 창을 닫아도 그대로 살아 있음).
@@ -19,16 +19,8 @@
gap: var(--spacing-8);
}
.m02-hopyo__note {
margin: 0;
color: var(--color-text-secondary);
font-size: var(--text-body-sm);
}
/* 겉 = 보조 글 부품(ui-text--warning) · 여기는 줄바꿈 살림만 */
.m02-hopyo__warn {
margin: 0;
color: var(--color-warning);
font-size: var(--text-body-sm);
white-space: pre-wrap;
}
@@ -41,7 +33,7 @@
}
.m02-hopyo__row:hover {
background: var(--color-mist-violet);
background: var(--color-accent-soft);
}
.m02-hopyo__num {
@@ -300,7 +292,7 @@
.m02-hopyo-sheet__item.is-current {
border-left-color: var(--color-accent);
background: var(--color-mist-violet);
background: var(--color-accent-soft);
}
.m02-hopyo-sheet__no {
@@ -318,7 +310,7 @@
}
/* 목록 숫자 — 다 연결(359-2)이면 녹색 · [수정] 은 번호 바로 오른쪽 · 줄 높이 안 커짐 */
.m02-hopyo-sheet__item .m02-hopyo__note.is-linked {
.m02-hopyo-sheet__item .ui-text.is-linked {
color: var(--color-success);
}
@@ -194,7 +194,8 @@ const why = (error: unknown): string => (error instanceof Error ? error.message
const won = (v: number | null | undefined): string =>
v === null || v === undefined ? "" : v.toLocaleString("ko-KR");
const fail = (error: unknown): void => showToast(`실패 — ${why(error)}`, "error");
const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text });
const note = (text: string): HTMLElement =>
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
const td = (text: string, cls = ""): HTMLElement =>
el("td", { text, className: cls, attrs: { title: text } });
const openSheet = (o: HopyoSheetOptions): void =>
@@ -503,7 +504,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
tools.push(all, go);
}
const head = el("div", {
className: "m02-hopyo__bar",
className: "ui-toolbar m02-hopyo__bar",
children: [
el("strong", { text: `제${h.번}호표 ${h.이름}` }),
note(h.규격),
@@ -545,7 +546,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
});
return [
el("label", {
className: "m02-hopyo__bar m02-hopyo__whole",
className: "ui-toolbar m02-hopyo__bar m02-hopyo__whole",
children: [
box,
note(
@@ -149,7 +149,7 @@ function nameNotes(line: HopyoLine, parts: HopyoPart[], price: LinePrice | null)
].filter(Boolean) as string[];
return [
...opts.map((t) => small(t, "m02-labrow__note m02-hopyo__opts")),
...why.map((t) => small(`셈 못 함: ${t}`, "m02-labrow__empty")),
...why.map((t) => small(`셈 못 함: ${t}`, "ui-text ui-text--error")),
...(parts.some((p) => p.호표 !== undefined) ? [small("하위 호표 = 만든 뒤 셈")] : []),
];
}
@@ -216,7 +216,7 @@ function logicRow(line: HopyoLine, ctx: RowsContext): HTMLElement {
el("td", {
className: "m02-labrow__name m02-hopyo__parts",
children: [
...(parts.length ? partLines(line, parts, ctx) : [small("—", "m02-labrow__muted")]),
...(parts.length ? partLines(line, parts, ctx) : [small("—", "ui-text ui-text--muted")]),
...nameNotes(line, parts, price),
],
}),
@@ -30,7 +30,8 @@ const FOLD_TEXT: Record<string, string> = {
"지역 변형": "하나 고르면 그 지역 · 「전국」으로 되돌림은 위",
숫자: "이름 · 출처 안 닮음 · 숫자 지문만",
};
const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text });
const note = (text: string): HTMLElement =>
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
const num = (v: number): string => String(Math.round(v * 10000) / 10000);
/** 대상 글 — 「이름 [단위]」 · 자재는 규격도 · 호표 = 「제N호표」 */
@@ -277,7 +278,7 @@ export function openHopyoChoose(opt: ChooseOptions): void {
const excel = qtyText(line.맞춤.품?.엑셀);
const basis = line.근거;
const head = el("div", {
className: "m02-hopyo__bar m02-hopyo__source",
className: "ui-toolbar m02-hopyo__bar m02-hopyo__source",
children: [
...(excel ? [note(`엑셀 품 — ${excel}`)] : []),
...(basis ? [note(`엑셀 근거 — ${sourceText(basis)}`)] : []),
@@ -323,7 +324,7 @@ export function openHopyoChoose(opt: ChooseOptions): void {
: "가까운 것 없음 — 아래 검색",
),
...sections,
el("div", { className: "m02-hopyo__bar", children: [together, ...more] }),
el("div", { className: "ui-toolbar m02-hopyo__bar", children: [together, ...more] }),
);
},
});
@@ -122,7 +122,8 @@ export const filterRows = (rows: LedgerView[], q: string, kind: string): LedgerV
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
const td = (text: string, cls = ""): HTMLElement =>
el("td", { text, className: cls, attrs: { title: text } });
const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text });
const note = (text: string): HTMLElement =>
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
export interface LedgerOptions {
scope: HopyoScope;
@@ -323,7 +324,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
}
root.replaceChildren(
el("div", {
className: "m02-hopyo__bar",
className: "ui-toolbar m02-hopyo__bar",
children: [
el("strong", { text: LEDGER_TITLE }),
note(`${LEDGER_HINT} · 사전조합 생성 때 남음 · 아래 층이 이김`),
@@ -80,7 +80,8 @@ const DANSAN = "단가산출";
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
const won = (v: number | null | undefined): string =>
v === null || v === undefined ? "" : v.toLocaleString("ko-KR");
const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text });
const note = (text: string): HTMLElement =>
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
/** 목록 숫자 「연결된 줄 / 연결할 줄」(358-3 두 칸 · 없으면 옛 맞춤 [제안, 줄]) — 다 연결(같고 0 넘음)이면 참 ·
* 0/0 은 색 없음 */
@@ -144,8 +145,14 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
const items = new Map<number, HTMLElement>();
const target = opt.target ?? opt.scope;
const head = el("div", { className: "m02-hopyo__bar", children: [note("엑셀을 읽는 중…")] });
const warn = el("p", { className: "m02-hopyo__warn" });
const head = el("div", {
className: "ui-toolbar m02-hopyo__bar",
children: [note("엑셀을 읽는 중…")],
});
const warn = el("p", {
className: "ui-text ui-text--warning m02-hopyo__warn",
attrs: { "data-text": "14" },
});
warn.hidden = true;
const left = el("div", {
className: "m02-hopyo-sheet__grid",
@@ -165,7 +172,10 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
className: "m02-hopyo-sheet__side",
children: [
search,
el("div", { className: "m02-hopyo__bar", children: [count, ...(opt.canWrite ? [go] : [])] }),
el("div", {
className: "ui-toolbar m02-hopyo__bar",
children: [count, ...(opt.canWrite ? [go] : [])],
}),
list,
result,
],
@@ -10,6 +10,7 @@
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { listHopyo, pageScope, uploadHopyo, type HopyoFile } from "./M02_MasterTemplete_Hopyo_Api";
import { buildHopyoLedger, LEDGER_HINT, LEDGER_TITLE } from "./M02_MasterTemplete_Hopyo_Ledger";
import { createEmpty } from "@ui/ui_template_empty";
export const HOPYO_TITLE = "일위대가 실무자료";
@@ -94,7 +95,7 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
row.addEventListener("click", () => void showSheet(f));
return row;
})
: [el("p", { className: "m02-side__empty", text: "올린 실무자료 없음" })]),
: [createEmpty({ text: "올린 실무자료 없음", small: true })]),
);
}
@@ -37,6 +37,7 @@ import {
import type { ItemPart, ItemSource } from "./M02_MasterTemplete_ItemSource";
import type { LaborDetailHandle } from "./M02_MasterTemplete_Labor_Detail";
import "./M02_MasterTemplete_Structure.css";
import { createBadge } from "@ui/ui_template_badge";
/** 집계표 열 하나에 딸린 문서 셋 — 번호 칸 · 이름 */
export type EditKind = ItemPart;
@@ -189,7 +190,8 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
body: HTMLElement,
save: () => Promise<void>,
) => {
const no = el("span", { className: "m02-struct__no" });
const no = createBadge("", "accent"); // 번호 = 배지 부품
no.classList.add("m02-struct__no");
const name = el("span", { className: "m02-struct__title" });
const shell = cardShell({ title, body, sub: [no, name] });
const saver = saveControl(
@@ -24,8 +24,6 @@
.m02-itempick__note {
margin: 0;
color: var(--color-text-secondary);
font-size: var(--text-body-sm);
}
.m02-itempick__buttons {
@@ -220,7 +220,7 @@ export function buildTwoWay(opt: TwoWayOptions): HTMLElement {
return el("div", {
className: "m02-itempick__two",
children: [
el("p", { className: "m02-itempick__note", text: opt.note }),
el("p", { className: "ui-text ui-text--muted m02-itempick__note", text: opt.note }),
el("div", { className: "m02-itempick__buttons", children: [fresh, copy] }),
below,
],
@@ -256,7 +256,10 @@ export function pickFrom(
close();
})(),
});
if (opt.note) body.append(el("p", { className: "m02-itempick__note", text: opt.note }));
if (opt.note)
body.append(
el("p", { className: "ui-text ui-text--muted m02-itempick__note", text: opt.note }),
);
body.append(picker.root);
},
});
@@ -13,7 +13,7 @@
}
.m02-labrow__row.is-open:hover {
background: var(--color-mist-violet);
background: var(--color-accent-soft);
}
/* 이름 칸(45-7) — 칸은 표 칸 그대로(flex 면 칸 선이 끊김) · 코드 · 이름 · 설명은 글마다 한 줄 말줄임(전체 = title) */
@@ -36,18 +36,12 @@
}
.m02-labrow__key,
.m02-labrow__note,
.m02-labrow__muted {
.m02-labrow__note {
color: var(--color-text-secondary);
font-size: var(--text-caption);
overflow-wrap: anywhere;
}
.m02-labrow__empty {
color: var(--color-danger);
font-size: var(--text-caption);
}
/* 로직 자재 줄(103-1) — 금액은 로직 단가 안(합에 안 더함 · 흐리게) · 바꾼 줄 = 소속 글 강조 */
.m02-labrow__logic-mat .m02-labrow__money {
color: var(--color-text-secondary);
@@ -138,6 +132,7 @@
/* 단추 칸(51-14 · 102-1) — [▲ · ▼ 위아래][옵션|수정][삭제] · 옵션 · 삭제는 두 단추 높이(2행 병합 꼴) */
.m02-labrow__tools {
flex-wrap: nowrap; /* 도구줄 부품은 감김 — 여기는 한 줄 그대로 */
display: inline-flex;
align-items: stretch;
gap: var(--btn-group-gap);
@@ -180,13 +175,13 @@
.m02-labrow__optrow.is-open:hover,
.m02-labrow__row.is-open:hover + .m02-labrow__optrow,
.m02-labrow__row.is-open:has(+ .m02-labrow__optrow:hover) {
background: var(--color-mist-violet);
background: var(--color-accent-soft);
}
/* 고른 줄(47-5) — 옵션 값 줄까지 한 덩이 · 왼쪽 띠(차례 칸 = 두 줄 병합 · 113-1) */
.m02-labrow .m02-labrow__row.is-selected,
.m02-labrow .m02-labrow__optrow.is-selected {
background: var(--color-mist-violet);
background: var(--color-accent-soft);
}
.m02-labrow__row.is-selected > td:first-child {
@@ -255,7 +250,7 @@
/* 총합계 줄 전체 합계 칸(44-6) — 테두리 · 색 · 테두리는 안쪽 그림자(칸 선 굵기 안 바꿈 · 45-7) */
.m02-labrow__sum .m02-labrow__total {
box-shadow: inset 0 0 0 2px var(--color-primary);
background: var(--color-mist-violet);
background: var(--color-accent-soft);
color: var(--color-primary);
}
@@ -290,12 +285,6 @@
font-size: var(--text-caption);
}
.m02-labin__lost {
margin: 0;
color: var(--color-danger);
font-size: var(--text-caption);
}
/* 보기 갈래(면적산출 오른쪽 패널 · 일위대가 카드) */
.m02-labview {
display: flex;
@@ -472,7 +461,7 @@
.m02-labrow__qtybtn:hover {
border-color: var(--color-primary);
background: var(--color-mist-violet);
background: var(--color-accent-soft);
}
/* 수량산출 모달(43-1) — 위 지금 수량 · 손 입력 · [연결 풀기] / 아래 면적산출 시트(읽기만) */
@@ -28,10 +28,6 @@
font-weight: var(--font-weight-bold);
}
.m02-labordg__hint {
color: var(--color-text-muted);
}
.m02-labordg__viewport {
position: relative;
height: clamp(320px, 62vh, 640px);
@@ -293,11 +289,6 @@
opacity: 1;
}
.m02-labordg__none {
margin: var(--spacing-16);
color: var(--color-text-muted);
}
.m02-labor__dg > summary {
cursor: pointer;
font-weight: var(--font-weight-bold);
@@ -11,6 +11,7 @@ import { wheelZoomsIn } from "../A00_Common/touch_bridge";
import { createButton, el } from "@ui/ui_template_elements";
import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money";
import "./M02_MasterTemplete_Labor_Diagram.css";
import { createEmpty } from "@ui/ui_template_empty";
const TEXT = {
Inputs: ["넣을 값", "Inputs"],
@@ -270,13 +271,13 @@ function mountShell(host: HTMLElement): State {
};
host.replaceChildren(
el("div", {
className: "m02-labordg__bar",
className: "ui-toolbar m02-labordg__bar",
children: [
icon("−", "Zoom_Out", () => zoomAt(st, 1 / 1.2)),
icon("+", "Zoom_In", () => zoomAt(st, 1.2)),
icon("⤢", "Fit", () => fit(st, true)),
label,
el("span", { className: "m02-labordg__hint", text: td("Hint") }),
el("span", { className: "ui-text ui-text--muted", text: td("Hint") }),
],
}),
viewport,
@@ -369,7 +370,7 @@ export function renderComboDiagram(
}
const { world } = st;
if (!data.logics.length) {
world.replaceChildren(el("p", { className: "m02-labordg__none", text: td("None") }));
world.replaceChildren(createEmpty({ text: td("None"), small: true }));
return;
}
const hasValue = data.logics.some((l) => isValue(l.out));
@@ -123,7 +123,7 @@ export function openLaborInput(opt: LaborInputOptions): void {
...(opt.notice
? [
el("p", {
className: "m02-labin__muted",
className: "ui-text ui-text--muted",
text: opt.notice,
attrs: { "data-labor-notice": "" },
}),
@@ -132,14 +132,14 @@ export function openLaborInput(opt: LaborInputOptions): void {
...(lost.length
? [
el("p", {
className: "m02-labin__lost",
className: "ui-text ui-text--error",
text: fill(tc("Labor_Lost"), lost.join(", ")),
}),
]
: []),
...(rows.length
? rows
: [el("p", { className: "m02-labin__muted", text: tc("In_None") })]),
: [el("p", { className: "ui-text ui-text--muted", text: tc("In_None") })]),
...(rate
? [
el("div", {
@@ -126,7 +126,9 @@ function matRow(
text: mat.고름 ? `${owner} · ${tc("LogicMat_Picked")}` : owner,
attrs: { title: owner },
}),
...(mat.까닭 ? [el("small", { className: "m02-labrow__empty", text: mat.까닭 })] : []),
...(mat.까닭
? [el("small", { className: "ui-text ui-text--error", text: mat.까닭 })]
: []),
],
}),
el("td", {
@@ -56,7 +56,7 @@ export function nationalRows(origin: LaborDoc["본뜬"]): HTMLElement[] {
}),
);
return [
el("p", { className: "m02-labrow__muted", text: say("Title") }),
el("p", { className: "ui-text ui-text--muted", text: say("Title") }),
el("div", {
className: "ui-general-block__table-wrap",
attrs: { "data-labor-national-table": "" },
@@ -107,7 +107,7 @@ export function openQtyModal(opt: QtyModalOptions): void {
});
} else
host.replaceChildren(
el("p", { className: "m02-labin__muted", text: tc("Labor_PickWait") }),
el("p", { className: "ui-text ui-text--muted", text: tc("Labor_PickWait") }),
);
const ok = (): void => {
const raw = hand.value.trim();
@@ -83,7 +83,7 @@ export async function openRefEdit(opt: RefEditOptions): Promise<void> {
});
body.append(
el("p", {
className: "m02-labin__muted",
className: "ui-text ui-text--muted",
text: notice,
attrs: { "data-labor-notice": "" },
}),
@@ -411,7 +411,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
...(values.empty
? [
el("small", {
className: "m02-labrow__empty",
className: "ui-text ui-text--error",
text: tc("Labor_Empty").replace("{n}", String(values.empty)),
}),
]
@@ -420,7 +420,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
...(why
? [
el("small", {
className: "m02-labrow__empty",
className: "ui-text ui-text--error",
text: `${tc("Labor_Calc_Fail")}: ${why}`,
}),
]
@@ -438,7 +438,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
className: "m02-labrow__move",
children: [
el("div", {
className: "m02-labrow__tools",
className: "ui-toolbar m02-labrow__tools",
children: [
...(move
? [
@@ -529,7 +529,8 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
...COSTS.map((c) => tc(HEAD[c])),
...(tools ? [tc("Col_Move")] : []),
];
if (!data.rows.length) return el("p", { className: "m02-labrow__muted", text: tc("NoLogic") });
if (!data.rows.length)
return el("p", { className: "ui-text ui-text--muted", text: tc("NoLogic") });
const out = table(heads, trs, foot);
if (move) attachRowDrag(out, move);
return out;
@@ -582,7 +583,7 @@ export function matTable(data: MatTableData, on?: Omit<RowHandlers, "move">): HT
children: [
el("strong", { text: mat.이름 ?? "" }),
...(line?.까닭
? [el("small", { className: "m02-labrow__empty", text: line.까닭 })]
? [el("small", { className: "ui-text ui-text--error", text: line.까닭 })]
: []),
],
}),
@@ -637,5 +638,5 @@ export function matTable(data: MatTableData, on?: Omit<RowHandlers, "move">): HT
const lead = data.lead ?? [];
return data.mats.length || lead.length
? table(heads, [...lead, ...trs], [])
: el("p", { className: "m02-labrow__muted", text: tc("Labor_NoMat") });
: el("p", { className: "ui-text ui-text--muted", text: tc("Labor_NoMat") });
}
@@ -111,7 +111,7 @@ export function createLaborView(onRow?: (row: LaborViewRow) => void): LaborViewH
calc = null;
fail = null;
if (!column) return void root.replaceChildren();
root.replaceChildren(el("p", { className: "m02-labrow__muted", text: tc("Loading") }));
root.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: tc("Loading") }));
try {
const got = await readTemplate(layer, "labor", column, null);
if (mine !== seq) return;
@@ -134,7 +134,7 @@ export function createLaborView(onRow?: (row: LaborViewRow) => void): LaborViewH
if (!doc) {
root.replaceChildren(
el("p", {
className: "m02-labrow__empty",
className: "ui-text ui-text--error",
text: error instanceof Error ? error.message : tc("Load_Failed"),
}),
);
@@ -274,7 +274,7 @@ export function mountLibraryList(
);
if (opt.canEdit)
tools.replaceChildren(
el("span", { className: "m02-main__tools", children: [btnNew, btnCopy, btnDel] }),
el("span", { className: "ui-toolbar m02-main__tools", children: [btnNew, btnCopy, btnDel] }),
);
void load();
return { getDoc: () => null, destroy: () => undefined };
@@ -16,6 +16,7 @@ import {
type ComboList,
type ComboOne,
} from "./M02_MasterTemplete_Labor_Api";
import { createEmpty } from "@ui/ui_template_empty";
export interface PracticeSectionOptions {
/** 저장 안 한 고침을 버릴지 확인 — 취소면 false */
@@ -125,10 +126,11 @@ export function buildPracticeSection(opt: PracticeSectionOptions): PracticeSecti
});
view.replaceChildren(
el("div", {
className: "m02-practice__bar",
className: "ui-toolbar m02-practice__bar",
children: [
el("span", {
className: "m02-practice__note",
className: "ui-text ui-text--muted",
attrs: { "data-text": "14" },
text: L("M02_PracticeCount").replace("{n}", String(list.줄.length)),
}),
drop,
@@ -145,7 +147,7 @@ export function buildPracticeSection(opt: PracticeSectionOptions): PracticeSecti
],
rows,
)
: el("p", { className: "m02-practice__note", text: L("M02_PracticeEmpty") }),
: createEmpty({ text: L("M02_PracticeEmpty"), small: true }),
);
}
@@ -171,7 +173,13 @@ export function buildPracticeSection(opt: PracticeSectionOptions): PracticeSecti
[L("M02_PracticeKey"), c.키],
[L("M02_PracticeGroup"), c.구분 ?? ""],
[L("M02_PracticeUnit"), c.단위 ?? ""],
].map(([k, v]) => el("span", { className: "m02-practice__note", text: `${k} ${v}` }));
].map(([k, v]) =>
el("span", {
className: "ui-text ui-text--muted",
attrs: { "data-text": "14" },
text: `${k} ${v}`,
}),
);
const lines = one.줄.map((x) =>
el("tr", {
children: cells([
@@ -185,7 +193,7 @@ export function buildPracticeSection(opt: PracticeSectionOptions): PracticeSecti
);
view.replaceChildren(
el("div", {
className: "m02-practice__bar",
className: "ui-toolbar m02-practice__bar",
children: [back, el("strong", { text: c.이름 }), ...info],
}),
...(c.비고 ? [el("p", { className: "m02-practice__memo", text: c.비고 })] : []),
@@ -200,7 +208,7 @@ export function buildPracticeSection(opt: PracticeSectionOptions): PracticeSecti
],
lines,
)
: el("p", { className: "m02-practice__note", text: L("M02_PracticeNoLogic") }),
: createEmpty({ text: L("M02_PracticeNoLogic"), small: true }),
);
}
@@ -133,14 +133,18 @@
font-size: var(--text-body);
}
/* 카드 번호 = 배지 부품(ui-badge) · 배지 아닌 옛 자리(일위대가 값 요약)만 옛 꼴 */
.m02-struct__no {
flex: none;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.m02-struct__no:not(.ui-badge) {
padding: 0 var(--spacing-8);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
color: var(--color-text);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* 미리보기 — 웹캐드는 높이를 부모에서 받음(`.m02-drawing` 이 flex: 1 1 0).
@@ -241,22 +245,10 @@
margin: var(--spacing-8) 0;
}
/* 줄 = 목록 줄 부품(ui-list-row) · 여기는 왼쪽부터 늘어놓기만 */
.m02-pick__item {
display: flex;
justify-content: flex-start;
gap: var(--spacing-12);
padding: var(--spacing-8);
border: none;
border-radius: var(--radius-sm, 4px);
background: none;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.m02-pick__item:hover,
.m02-pick__item:focus-visible {
background: var(--color-surface-sunken);
}
.m02-pick__key {
@@ -266,16 +258,7 @@
font-variant-numeric: tabular-nums;
}
/* 개인 자재 표(148-3) · 신규 자재 입력 */
.m02-pick__tag {
flex: none;
padding: 0 var(--spacing-4);
border-radius: var(--radius-sm, 4px);
background: var(--color-surface-sunken);
color: var(--color-text-secondary);
font-size: 0.85em;
}
/* 신규 자재 입력 */
.m02-pick__new {
display: flex;
flex-direction: column;
@@ -39,6 +39,7 @@
/* 층 단추 줄 오른쪽 끝 — 「N개 고름」 · 「+」(PLAN 30-2) */
.m02-tree__tools {
flex-wrap: nowrap; /* 도구줄 부품은 감김 — 여기는 한 줄 그대로 */
display: inline-flex;
align-items: center;
gap: var(--spacing-8);
@@ -64,7 +65,7 @@
/* 여러 개 고른 줄 배경 */
.m02-tree__body .ui-tree__row.is-picked {
background: var(--color-mist-violet);
background: var(--color-accent-soft);
}
/* 남의 회사 「개인」 사람 나무 — 고른 사람 줄 아래 그 사람 라이브러리(PLAN 29-6) */
@@ -72,11 +73,9 @@
padding-left: var(--spacing-16);
}
/* 겉 = 보조 글 부품 · 여기는 여백만 */
.m02-tree__hint {
margin: 0;
padding: var(--spacing-8);
color: var(--color-text-muted);
font-size: var(--text-body-sm);
}
.m02-treedlg {
@@ -118,7 +118,12 @@ export interface TreePanelHandle {
}
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
const hint = (text: string): HTMLElement => el("p", { className: "m02-tree__hint", text });
const hint = (text: string): HTMLElement =>
el("p", {
className: "ui-text ui-text--muted m02-tree__hint",
text,
attrs: { "data-text": "14" },
});
export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
const owner = opt.owner ?? null;
@@ -162,7 +167,10 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
multiBtn.classList.add("ui-touch-only");
multiBtn.title = "여럿 고르기 (Ctrl 누르고 누르기와 같음)";
multiBtn.setAttribute("aria-pressed", "false");
const tools = el("span", { className: "m02-tree__tools", children: [status, multiBtn, add] });
const tools = el("span", {
className: "ui-toolbar m02-tree__tools",
children: [status, multiBtn, add],
});
/* --- 그리기 --- */
const canCopy = !!api.paste;
@@ -38,6 +38,7 @@ import { helpParts } from "./M02_MasterTemplete_CardShell";
import { isPrecombo } from "./M02_MasterTemplete_UI_Side_Precombo";
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
import { createEmpty } from "@ui/ui_template_empty";
export interface Selection {
layer: Layer;
projectId: string | null;
@@ -149,7 +150,10 @@ export function createMain(
void open({ ...at, kind });
});
const slot = el("div", { className: "m02-main__slot" }); // 도면 양식의 작도 영역 칸이 들어옴
const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } });
const notice = el("div", {
className: "ui-banner ui-banner--danger m02-main__notice",
attrs: { hidden: "" },
});
const host = el("div", { className: "m02-main__host" });
const extra = el("div", { className: "m02-main__extra" }); // 표 밖 형제 자리 — 구조물 설계 컨테이너
const column = el("div", {
@@ -244,7 +248,7 @@ export function createMain(
notice.hidden = nodes.length === 0;
};
const empty = (text: string): void => {
host.replaceChildren(el("p", { className: "m02-main__empty", text }));
host.replaceChildren(createEmpty({ text }));
};
/** 고를 것마다 고칠 권한 — 사전조합 층은 고름에 실림(134-3) · 그 밖 페이지 층 */
const writable = (at: Selection): boolean => at.canWrite ?? canEdit(at.layer, isAdmin);
@@ -617,7 +621,9 @@ export function createMain(
title.textContent = label;
host.replaceChildren(node);
if (tools)
slot.replaceChildren(el("span", { className: "m02-main__tools", children: [tools] }));
slot.replaceChildren(
el("span", { className: "ui-toolbar m02-main__tools", children: [tools] }),
);
return (d) => (dirty = d);
}
@@ -40,6 +40,7 @@ import {
PRECOMBO_TITLE,
type PrecomboOptions,
} from "./M02_MasterTemplete_UI_Side_Precombo";
import { createEmpty } from "@ui/ui_template_empty";
const KINDS: { kind: Kind; label: () => string }[] = [
{ kind: "table", label: () => L("M02_KindTable") },
@@ -225,7 +226,7 @@ export function buildSide(opt: SideOptions): SideHandle {
b.addEventListener("click", () => void select(info));
return b;
})
: [el("p", { className: "m02-side__empty", text: L("M02_NoTemplates") })]),
: [createEmpty({ text: L("M02_NoTemplates"), small: true })]),
);
}
}
@@ -38,6 +38,7 @@ import { attachTreeDrag } from "./M02_MasterTemplete_TreePanel_Drag";
import { folderIdOf, samePath } from "./M02_MasterTemplete_TreePanel_Model";
import type { Target } from "./M02_MasterTemplete_TreePanel_Pick";
import type { Selection } from "./M02_MasterTemplete_UI_Main";
import { createEmpty } from "@ui/ui_template_empty";
export type PrecomboKind = Extract<Kind, "precombo">;
@@ -278,9 +279,7 @@ export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle {
targets.clear();
const tree = buildPrecomboFolders(rows, view ?? { 폴더: [], 자리: {} });
if (!tree.docs.length && !tree.folders.length)
return void treeHost.replaceChildren(
el("p", { className: "m02-side__empty", text: tc("Lib_None") }),
);
return void treeHost.replaceChildren(createEmpty({ text: tc("Lib_None"), small: true }));
const canWrite = opt.scope().canWrite;
treeHost.replaceChildren(
...renderTree(kids(tree), (node, caret, depth, onClick) => {
@@ -320,7 +319,7 @@ export function buildPrecomboSection(opt: PrecomboOptions): PrecomboHandle {
const mine = ++seq;
const at = opt.scope();
barHost.replaceChildren(opt.bar());
treeHost.replaceChildren(el("p", { className: "m02-side__empty", text: tc("Loading") }));
treeHost.replaceChildren(createEmpty({ text: tc("Loading"), small: true }));
const [got, tree] = await Promise.all([
// 못 받으면 빈 나무(목록표에서 까닭) · 폴더를 못 받으면 문서는 모두 뿌리
listTemplates(at.layer, null, at.owner).then(
@@ -29,12 +29,6 @@
margin-left: auto;
}
.m02-practice__note {
margin: 0;
color: var(--color-text-secondary);
font-size: var(--text-body-sm);
}
.m02-practice__memo {
margin: 0;
color: var(--color-text-secondary);
@@ -47,7 +41,7 @@
}
.m02-practice__row:hover {
background: var(--color-mist-violet);
background: var(--color-accent-soft);
}
.m02-side__row {
@@ -119,20 +113,15 @@ html.is-bm-route .m02-main__head {
/* 제목 줄 오른쪽 끝 도구(일위대가 「새 조합」 · PLAN 30-5) */
.m02-main__tools {
flex-wrap: nowrap; /* 도구줄 부품은 감김 — 여기는 한 줄 그대로 */
display: inline-flex;
gap: var(--spacing-8); /* M01 머리 줄(`m01-head`)과 같은 틈 */
margin-left: auto;
}
/* 겉 = 띠 알림 부품(ui-banner--danger) · 여기는 가운데 맞춤만 */
.m02-main__notice {
display: flex;
align-items: center;
gap: var(--spacing-8);
padding: var(--spacing-8) var(--spacing-12);
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);
}
.m02-main__host {
@@ -181,13 +170,6 @@ html.is-bm-route .m02-main__head {
overflow: auto;
}
.m02-main__empty {
margin: 0;
padding: var(--spacing-24);
color: var(--color-text-muted);
text-align: center;
}
.m02-modal__field {
display: flex;
flex-direction: column;
+10 -8
View File
@@ -12,6 +12,9 @@ import { openModal } from "@ui/ui_template_modal";
import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch";
import { call } from "./M02_MasterTemplete_Api_Fetch";
import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";
import { createBadge } from "@ui/ui_template_badge";
import { createText } from "@ui/ui_template_text";
import { createEmpty } from "@ui/ui_template_empty";
/** 고른 자재 한 줄 — M01 자재(키 `MT`) · 개인 자재(키 `PM`) */
export interface MaterialItem {
@@ -93,13 +96,14 @@ export function pickMaterial(done: (item: MaterialItem) => void, own?: OwnLayer)
const search = createInputField({ placeholder: TEXT.search, type: "search" });
const list = el("div", { className: "m02-pick__list" });
const item = (m: MaterialItem, tags: string[] = []): HTMLElement => {
// 목록 줄 부품 겉 · 꼬리표 = 배지 부품
const row = el("button", {
className: "m02-pick__item",
attrs: { type: "button", "data-material": m.코드 },
className: "m02-pick__item ui-list-row ui-sidebar-row",
attrs: { type: "button", "data-material": m.코드, "data-size": "36" },
children: [
el("span", { className: "m02-pick__key", text: m.코드 }),
`${m.이름} ${m.규격} ${m.단위}`.replace(/\s+/g, " "),
...tags.map((t) => el("span", { className: "m02-pick__tag", text: t })),
...tags.map((t) => createBadge(t, "neutral")),
],
});
row.addEventListener("click", () => {
@@ -111,7 +115,7 @@ export function pickMaterial(done: (item: MaterialItem) => void, own?: OwnLayer)
let seq = 0;
const paint = async (): Promise<void> => {
const ticket = ++seq;
list.replaceChildren(el("p", { className: "m02-struct__empty", text: TEXT.searching }));
list.replaceChildren(createText(TEXT.searching));
try {
const q = search.input.value.trim();
const [hits] = await Promise.all([searchMaterials(q), loaded]);
@@ -122,9 +126,7 @@ export function pickMaterial(done: (item: MaterialItem) => void, own?: OwnLayer)
.map((r) => item(toItem(r), r.값 === null ? [TEXT.own, TEXT.noPrice] : [TEXT.own]));
const all = [...ownHits, ...hits.map((m) => item(m))];
list.replaceChildren(
...(all.length
? all
: [el("p", { className: "m02-struct__empty", text: TEXT.noMatch })]),
...(all.length ? all : [createEmpty({ text: TEXT.noMatch, small: true })]),
);
} catch {
if (ticket === seq) showToast(TEXT.searchFailed, "error");
@@ -219,7 +221,7 @@ function newForm(
children: [el("label", { className: "ui-field__label", text: TEXT.unit }), unitHost],
}),
price.root,
el("p", { className: "m02-struct__empty", text: TEXT.key }),
createText(TEXT.key),
el("div", {
className: "ui-btn-group",
children: [createButton({ label: TEXT.back, variant: "ghost", onClick: back }), save],
+28 -16
View File
@@ -38,6 +38,9 @@ import { checkBox, pickBox, textBox } from "./M02_SO_Def_Ui";
import { addFromTemplate, templates } from "./M02_SO_Def_Add";
import { buildGroupBlock } from "./M02_SO_Groups";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
import { createEmpty } from "@ui/ui_template_empty";
import { createTabs } from "@ui/ui_template_tabs";
/** 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) — 모든 종류에서 숨김(52-12 · 정의 화면 = 이름만) */
const OLD_EDITOR = false;
@@ -93,7 +96,11 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
const canEdit = (): boolean => opt.isAdmin && !past;
const kindList = el("div", { className: "m02-opts__kinds", attrs: { "data-sod-kinds": "" } });
const detail = el("div", { className: "m02-opts__list", attrs: { "data-sod-detail": "" } });
const banner = el("div", { className: "m02-sod__banner", attrs: { "data-sod-banner": "" } });
// 판 띠 = 띠 알림 부품 꼴(정보 · 지난 판 = 경고)
const banner = el("div", {
className: "ui-banner ui-banner--info m02-sod__banner",
attrs: { "data-sod-banner": "" },
});
const issues = el("div", { className: "m02-sod__issues", attrs: { "data-sod-issues": "" } });
const historyBox = el("div", {
className: "m02-sod__history",
@@ -161,8 +168,8 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
const off = t.enabled === false;
const groups = d.그룹[t.type_id]?.length ?? 0;
const b = el("button", {
className: `m02-opts__kind${t.type_id === current ? " is-active" : ""}${off ? " is-off" : ""}`,
attrs: { type: "button", "data-sod-kind": t.type_id },
className: `m02-opts__kind ui-list-row ui-sidebar-row${t.type_id === current ? " is-active" : ""}${off ? " is-off" : ""}`,
attrs: { type: "button", "data-sod-kind": t.type_id, "data-size": "32" },
children: [
el("span", { text: t.name }),
...(groups ? [createTag(to("GroupTag", String(groups)), "accent")] : []),
@@ -204,7 +211,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
const d = shown();
const type = d.types.find((t) => t.type_id === current);
if (!type) {
detail.replaceChildren(el("p", { className: "m02-side__empty", text: to("Pick") }));
detail.replaceChildren(createEmpty({ text: to("Pick"), small: true }));
return;
}
const edit = canEdit();
@@ -220,7 +227,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
attr: "data-sod-kind-name",
cls: "m02-sod__name",
}),
el("small", { className: "m02-opts__key", text: type.type_id }),
el("small", { className: "ui-text ui-text--muted m02-opts__key", text: type.type_id }),
// 안 씀 = 목록 B06 만(enabled 는 목록 짝 · 322-2) · 숨김(hidden)은 한눈 표 목록 칸에서
checkBox(
to("Unused"),
@@ -269,14 +276,17 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
["kinds", to("TabKinds")],
["parts", to("TabParts")],
];
tabBar.replaceChildren(
...pairs.map(([key, label]) => {
const b = createButton({ label, variant: key === tab ? "filled" : "ghost", size: "sm" });
b.setAttribute("data-sod-tab", key);
b.addEventListener("click", () => ((tab = key), paint()));
return b;
}),
);
// 탭 부품(보조 탭) — 칸마다 data-sod-tab 그대로
const tabs = createTabs({
kind: "pill",
tabs: pairs.map(([key, label]) => ({ key, label })),
value: tab,
onChange: (key) => ((tab = key as Tab), paint()),
});
tabs.root.querySelectorAll<HTMLElement>("[data-tab]").forEach((b) => {
b.setAttribute("data-sod-tab", b.dataset.tab ?? "");
});
tabBar.replaceChildren(tabs.root);
const edit = canEdit();
if (tab === "table")
body.replaceChildren(buildTable({ def: shown(), edit, changed, issues: allProblems() }));
@@ -297,8 +307,10 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
]),
);
banner.classList.toggle("is-past", !!past);
banner.classList.toggle("ui-banner--warning", !!past);
banner.classList.toggle("ui-banner--info", !past);
const list = allProblems();
issues.replaceChildren(...list.map((t) => el("p", { className: "m02-sod__bad", text: t })));
issues.replaceChildren(...list.map((t) => createText(t, "error")));
save.disabled = !opt.isAdmin || !!past || !dirty || list.length > 0;
};
@@ -366,7 +378,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
});
return b;
})
: [el("p", { className: "m02-side__empty", text: to("PastNone") })]),
: [createEmpty({ text: to("PastNone"), small: true })]),
);
};
const history = createButton({ label: to("PastList"), variant: "ghost" });
@@ -387,7 +399,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
const node = el("div", {
className: "m02-opts m02-sod",
children: [
el("p", { className: "m02-side__empty", text: to("DefNote") }),
createText(to("DefNote")),
banner,
tabBar,
body,
@@ -20,6 +20,8 @@ import {
import { checkBox, head, moveButtons, pickBox, textBox } from "./M02_SO_Def_Ui";
import { buildPlaceSelect } from "./M02_SO_Groups";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
import { createEmpty } from "@ui/ui_template_empty";
export interface OptionsBlockOptions {
def: StructDef;
@@ -113,8 +115,8 @@ function choicesBlock(opt: OptionsBlockOptions, o: OptionDef): HTMLElement {
return el("div", {
className: "m02-sod__choices",
children: [
el("p", { className: "m02-side__empty", text: to("ChoiceNote") }),
...(rows.length ? rows : [el("p", { className: "m02-sod__bad", text: to("ChoiceNone") })]),
createText(to("ChoiceNote")),
...(rows.length ? rows : [createText(to("ChoiceNone"), "error")]),
...(edit ? [add] : []),
],
});
@@ -171,7 +173,7 @@ function optionRow(opt: OptionsBlockOptions, o: OptionDef): HTMLElement {
attr: "data-sod-name",
cls: "m02-sod__name",
}),
el("small", { className: "m02-opts__key", text: o.key }),
el("small", { className: "ui-text ui-text--muted m02-opts__key", text: o.key }),
...(o.phase === "detail" ? [createTag(to("Detail"), "accent")] : []),
form,
textBox(o.unit ?? "", (v) => (o.unit = v.trim() || null), changed, {
@@ -224,7 +226,7 @@ export function buildOptionsBlock(opt: OptionsBlockOptions): HTMLElement {
...head(to("OptionHead"), to("OptionNote")),
...(type.options.length
? type.options.map((o) => optionRow(opt, o))
: [el("p", { className: "m02-side__empty", text: to("NoOptionKind") })]),
: [createEmpty({ text: to("NoOptionKind"), small: true })]),
...(edit ? [el("div", { className: "m02-side__row", children: [add] })] : []),
],
});
@@ -18,6 +18,7 @@ import {
} from "./M02_SO_Def_Reflect";
import { checkBox, head } from "./M02_SO_Def_Ui";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
type Branch = keyof typeof PART_BOX;
@@ -40,7 +41,7 @@ export function buildParts(opt: {
const blocks = (Object.keys(PART_BOX) as Branch[]).flatMap((branch) => [
...head(branch),
el("table", {
className: "m02-sod__table m02-sod__table--wrap",
className: "ui-table ui-table--data m02-sod__table m02-sod__table--wrap",
attrs: { "data-sod-parts": branch },
children: [
el("thead", {
@@ -99,6 +100,6 @@ export function buildParts(opt: {
]);
return el("div", {
className: "m02-sod__tablewrap",
children: [el("p", { className: "m02-side__empty", text: to("PartsNote") }), ...blocks],
children: [createText(to("PartsNote")), ...blocks],
});
}
@@ -16,6 +16,8 @@ import {
} from "./M02_SO_Def_Model";
import { head, pickBox } from "./M02_SO_Def_Ui";
import { to } from "./M02_SO_Text";
import { createEmpty } from "@ui/ui_template_empty";
import { createCheckbox } from "@ui/ui_template_checkbox";
export interface RulesBlockOptions {
def: StructDef;
@@ -42,15 +44,18 @@ function multiPick(
className: "m02-sod__rulechoices",
attrs: { [attr]: "" },
children: all.map((c) => {
const box = el("input", { attrs: { type: "checkbox", value: c } }) as HTMLInputElement;
box.checked = on.has(c);
box.disabled = !edit;
box.addEventListener("change", () => {
if (box.checked) on.add(c);
else on.delete(c);
set(all.filter((x) => on.has(x)));
const { root } = createCheckbox(c, {
checked: on.has(c),
disabled: !edit,
value: c,
onChange: (checked) => {
if (checked) on.add(c);
else on.delete(c);
set(all.filter((x) => on.has(x)));
},
});
return el("label", { className: "m02-sod__check", children: [box, el("span", { text: c })] });
root.classList.add("m02-sod__check");
return root;
}),
});
}
@@ -227,7 +232,7 @@ export function buildRulesBlock(opt: RulesBlockOptions): HTMLElement {
...head(to("RuleHead"), to("RuleNote")),
...(mine.length
? mine.map((r) => ruleRow(opt, r, def.규칙.indexOf(r)))
: [el("p", { className: "m02-side__empty", text: to("RuleNone") })]),
: [createEmpty({ text: to("RuleNone"), small: true })]),
...(edit ? [el("div", { className: "m02-side__row", children: [add] })] : []),
],
});
@@ -28,6 +28,7 @@ import {
import { hiddenKeys, readKeys, setShown } from "./M02_SO_Def_Reflect";
import { checkBox, head, textBox } from "./M02_SO_Def_Ui";
import { to, type OptionsTextKey } from "./M02_SO_Text";
import { createEmpty } from "@ui/ui_template_empty";
const TAG: Record<NonNullable<SpecNameRow["tag"]>, OptionsTextKey> = {
required: "SpecRequired",
@@ -132,7 +133,7 @@ export function buildSpecBlock(opt: {
cls: "m02-sod__name",
})
: el("span", { text: to("SpecLoose") }),
el("small", { className: "m02-opts__key", text: row.key }),
el("small", { className: "ui-text ui-text--muted m02-opts__key", text: row.key }),
...(row.tag ? [createTag(to(TAG[row.tag]), "neutral")] : []),
...shownBox(
opt,
@@ -164,7 +165,7 @@ export function buildSpecBlock(opt: {
attrs: { "data-sod-spec-block": "" },
children: [
...head(to("SpecHead"), to("SpecNote")),
...(rows.length ? rows : [el("p", { className: "m02-side__empty", text: to("SpecNone") })]),
...(rows.length ? rows : [createEmpty({ text: to("SpecNone"), small: true })]),
...head(to("SpecPreview")),
el("div", {
className: "m02-sod__specpreview",
@@ -25,6 +25,7 @@ import {
} from "./M02_SO_Def_Reflect";
import { checkBox, pickBox, textBox } from "./M02_SO_Def_Ui";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
type Pairs = ReadonlyArray<readonly [string, string]>;
const LIST_PAIRS = (): Pairs => [
@@ -89,7 +90,7 @@ function mark(changed: boolean, ...children: HTMLElement[]): HTMLElement {
/** 형상칸 — 벽(형상 wall)만: 기본 형태(그림 형태 고르기) · 물빼기. */
function shapeOptsCell(ctx: CellCtx): HTMLElement {
const { def, type, row, edit, changed } = ctx;
if (row.shape !== "wall") return el("span", { className: "m02-side__empty", text: "—" });
if (row.shape !== "wall") return el("span", { className: "ui-text ui-text--muted", text: "—" });
const form = String(row.shapeOpts.form ?? "");
const forms = !form || REVET_FORMS.includes(form) ? REVET_FORMS : [form, ...REVET_FORMS];
const box = pickBox(
@@ -278,9 +279,7 @@ export function buildTable(opt: {
el("td", {
attrs: { "data-sod-table-check": "" },
children: [
bad.length
? el("span", { className: "m02-sod__bad", text: bad.join(" · ") })
: createTag(to("Ok"), "accent"),
bad.length ? createText(bad.join(" · "), "error") : createTag(to("Ok"), "accent"),
reset,
],
}),
@@ -292,9 +291,9 @@ export function buildTable(opt: {
return el("div", {
className: "m02-sod__tablewrap",
children: [
el("p", { className: "m02-side__empty", text: to("TableNote") }),
createText(to("TableNote")),
el("table", {
className: "m02-sod__table",
className: "ui-table ui-table--data m02-sod__table",
attrs: { "data-sod-table": "" },
children: [
el("thead", {
@@ -7,6 +7,8 @@
import { createButton, el } from "@ui/ui_template_elements";
import { step } from "./M02_SO_Def_Model";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
import { createCheckbox } from "@ui/ui_template_checkbox";
/** 글 칸 — 글자마다 `set` · 떠날 때 `done` */
export function textBox(
@@ -69,13 +71,15 @@ export function checkBox(
set: (v: boolean) => void,
opt: { edit: boolean; attr?: string },
): HTMLLabelElement {
const box = el("input", {
attrs: { type: "checkbox", ...(opt.attr ? { [opt.attr]: "" } : {}) },
}) as HTMLInputElement;
box.checked = on;
box.disabled = !opt.edit;
box.addEventListener("change", () => set(box.checked));
return el("label", { className: "m02-sod__check", children: [box, el("span", { text: label })] });
// 체크 부품 — 속성(data-sod-*)은 칸에 그대로
const { root, input } = createCheckbox(label, {
checked: on,
disabled: !opt.edit,
onChange: set,
});
if (opt.attr) input.setAttribute(opt.attr, "");
root.classList.add("m02-sod__check");
return root;
}
/** 고르기 — [값, 글] 짝 */
@@ -117,5 +121,5 @@ export function moveButtons<T>(
/** 절 제목 + 안내 한 줄 */
export const head = (title: string, note?: string): HTMLElement[] => [
el("h4", { className: "m02-opts__group", text: title }),
...(note ? [el("p", { className: "m02-side__empty", text: note })] : []),
...(note ? [createText(note)] : []),
];
@@ -11,6 +11,8 @@ import { createButton, el, showConfirmDialog } from "@ui/ui_template_elements";
import { SHAPE_KEYS, type GroupDef } from "./M02_SO_Def_Model";
import { checkBox, textBox } from "./M02_SO_Def_Ui";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
import { createEmpty } from "@ui/ui_template_empty";
const NAME_MAX = 40;
@@ -109,7 +111,7 @@ export function buildGroupBlock(opt: GroupBlockOptions): HTMLElement {
children: [
name,
el("small", {
className: "m02-opts__key",
className: "ui-text ui-text--muted m02-opts__key",
text: to("GroupCount", String(group.옵션.length)),
}),
...(opt.edit ? [up, down, del] : []),
@@ -129,11 +131,12 @@ export function buildGroupBlock(opt: GroupBlockOptions): HTMLElement {
opt.changed();
});
return el("div", {
className: "m02-sog",
className: "ui-card m02-sog",
attrs: { "data-pad": "12" },
children: [
el("h4", { className: "m02-opts__group", text: to("GroupHead") }),
el("p", { className: "m02-side__empty", text: to("GroupNote") }),
...(rows.length ? rows : [el("p", { className: "m02-side__empty", text: to("GroupNone") })]),
createText(to("GroupNote")),
...(rows.length ? rows : [createEmpty({ text: to("GroupNone"), small: true })]),
...(opt.edit ? [el("div", { className: "m02-side__row", children: [add] })] : []),
],
});
@@ -7,6 +7,7 @@
import { el } from "@ui/ui_template_elements";
import { openDefScreen } from "./M02_SO_Def";
import "@ui/ui_template_card.css";
import "./M02_SO_Style.css";
import { to } from "./M02_SO_Text";
@@ -37,28 +37,11 @@
font-size: var(--text-body-sm);
}
/* 종류 줄 = 목록 줄 부품(ui-list-row) · 여기는 글 · 꼬리표 늘어놓기만 */
.m02-opts__kind {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: var(--spacing-4);
padding: var(--spacing-4) var(--spacing-8);
border: 1px solid transparent;
border-radius: var(--radius-sm);
background: none;
color: var(--color-text);
font: inherit;
text-align: left;
cursor: pointer;
}
.m02-opts__kind:hover {
background: var(--color-mist-violet);
}
.m02-opts__kind.is-active {
border-color: var(--color-accent);
background: var(--color-mist-violet);
}
/* 옵션 줄 = 왼쪽 보이기 체크 · 라벨 · 키 · 표시 / 오른쪽 들어갈 자리(본체 · 그룹) */
@@ -81,14 +64,11 @@
}
/* 옵션 그룹(PLAN 46-2) — 그룹 줄 = 이름 · 옵션 수 · 위로 · 아래로 · 지우기 */
/* 겉 = 카드 부품(ui-card · 여백 12) */
.m02-sog {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
padding: var(--spacing-8) var(--spacing-12);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface-raised);
}
.m02-sog__row {
@@ -108,9 +88,9 @@
width: 11rem;
}
/* 키 글 = 보조 글 부품(ui-text) · 글줄 안에 놓임 */
.m02-opts__key {
color: var(--color-text-muted);
font-size: 11px;
display: inline-flex;
}
/* 구조물 정의 편집 화면(PLAN 48-2) — 판 띠 · 종류 머리 · 옵션 · 선택지 · 배치 격자 · 규칙 · 지난 판 */
@@ -118,19 +98,9 @@
max-width: 80rem;
}
/* 판 띠 = 띠 알림 부품(정보 · 지난 판 = 경고) · 여기는 한 줄 늘어놓기만 */
.m02-sod__banner {
display: flex;
align-items: center;
gap: var(--spacing-8);
color: var(--color-text-muted);
}
.m02-sod__banner.is-past {
padding: var(--spacing-4) var(--spacing-8);
border: 1px solid var(--color-accent);
border-radius: var(--radius-sm);
background: var(--color-mist-violet);
color: var(--color-text);
}
.m02-sod__family,
@@ -267,7 +237,7 @@
}
.m02-sod__cell.is-base {
background: var(--color-mist-violet);
background: var(--color-accent-soft);
}
.m02-sod__cell.is-bad,
@@ -328,11 +298,6 @@
gap: var(--spacing-4);
}
.m02-sod__bad {
margin: 0;
color: var(--color-danger);
}
/* 구조물 명세 이름 고치기(PLAN 52-11) — 컨테이너 › 항목 › 값 들여쓰기 */
.m02-sod__specrow {
display: flex;
@@ -370,25 +335,20 @@
}
/* ── 322-2 탭 셋 · 한눈 표 · 부품 ── */
/* 탭 = 탭 부품(보조 탭) · 여기는 위아래 간격만 */
.m02-sod__tabs {
display: flex;
gap: var(--spacing-4);
margin: var(--spacing-8) 0;
}
.m02-sod__tablewrap {
overflow-x: auto;
}
.m02-sod__table {
border-collapse: collapse;
font-size: var(--text-body-sm);
/* 표 = 표 부품 데이터표(ui-table--data) · 여기는 폭 · 줄바꿈만 */
table.m02-sod__table {
width: auto;
min-width: 100%;
}
.m02-sod__table th,
.m02-sod__table td {
border-bottom: 1px solid var(--color-border);
padding: var(--spacing-4);
text-align: left;
vertical-align: middle;
white-space: nowrap;
}
.m02-sod__familyrow td {
@@ -434,7 +394,7 @@
}
/* 부품 탭 — 긴 글은 줄바꿈해 표가 화면 안에(322-2 다듬기) */
.m02-sod__table--wrap {
table.m02-sod__table--wrap {
table-layout: fixed;
width: 100%;
}
+28
View File
@@ -67,3 +67,31 @@ def test_m02_borrowed_m01_classes_follow_parts():
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
def _m02_src(suffix: str) -> dict[str, str]:
out = {}
for sub in ("", "DataFlow", "StructOptions"):
out.update(_src(f"M02_MasterTemplete/{sub}".rstrip("/"), suffix))
return out
def test_m02_parts_in_place():
ts = "".join(_m02_src(".ts").values())
for part in ("createEmpty(", "createText(", "createBadge(", "createTabs(", "createCheckbox("):
assert part in ts, f"M02 이 부품 {part} 를 안 씀"
for cls in (
"ui-banner ui-banner--",
"ui-toolbar m02-",
"ui-table ui-table--data",
"ui-list-row",
):
assert cls in ts, f"M02 부품 꼴 {cls} 없음"
# 시험 · 가이드가 잡는 이름은 그대로
for keep in ("m02-layers__btn", "m02-side__lists", "m02-main__new", "data-sod-tab"):
assert keep in ts, f"잡히는 이름 {keep} 사라짐"
def test_m02_css_no_old_alias():
for name, css in _m02_src(".css").items():
assert not OLD_ALIAS.search(css), f"{name}: 옛 보라 별칭"
+1 -1
View File
@@ -360,7 +360,7 @@ def test_좌측_판_자리와_권한():
def test_로직_추가_한_곳과_파일_규칙():
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
assert len(detail.splitlines()) == 695 # 줄 수 그대로(361-2 전국 자재 부르는 줄 둘)
assert len(detail.splitlines()) == 693 # 줄 수 그대로(361-2 전국 자재 부르는 줄 둘 · 553-5 보조 글 부품 -2)
assert "withHopyo(library)," in detail
pick = _text("M02_MasterTemplete_Labor_Pick.ts")
assert "more?: () => Promise<PickExtra[]>;" in pick and "...later" in pick