Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1

This commit is contained in:
2026-10-27 10:54:33 +09:00
39 changed files with 604 additions and 339 deletions
+1 -27
View File
@@ -1,4 +1,4 @@
/* ☰ [설정] 창(PLAN 332-2) — 탭 줄 = 부품 보조 탭 · 휠 방향 = 부품 라디오 · CAD 별칭 표 = 부품 표 */
/* ☰ [설정] 창(PLAN 332-2) — 탭 줄 = 부품 보조 탭 · 휠 방향 = 부품 라디오 · CAD 별칭 표 = 부품 표 · 규칙 줄 = 부품 값 목록 규칙 꼴 */
.app-settings .ui-text[hidden] {
display: none; /* 부품 보조 글은 display:flex 라 hidden 을 이김 */
}
@@ -28,29 +28,3 @@
top: 0;
background: var(--color-surface-raised);
}
.app-settings__rules {
margin: 0 0 var(--spacing-8);
padding: 0;
list-style: none;
}
.app-settings__rules > li {
display: grid;
grid-template-columns: minmax(0, 15em) 1fr;
align-items: center;
gap: var(--spacing-12);
padding: var(--spacing-4) 0;
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
.app-settings__keys {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-4);
}
.app-settings__keys:only-child {
grid-column: 1 / -1;
}
+7 -12
View File
@@ -9,6 +9,7 @@
import { createBadge } from "@ui/ui_template_badge";
import { createCheckbox } from "@ui/ui_template_checkbox";
import { createKv } from "@ui/ui_template_kv";
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { askLeave } from "@ui/ui_template_leave_dialog";
import { t as L } from "@ui/ui_template_locale";
@@ -58,24 +59,18 @@ const normalize = (p: InputPrefs): InputPrefs => ({
});
const snapshot = (p: InputPrefs): string => JSON.stringify(normalize(p));
/** 규칙 목록 — 한 줄 = 「입력 = 동작」 · 입력은 키 칩(` · ` 로 여럿) · 동작 없는 줄은 칩만 */
/** 규칙 목록 = 값 목록 부품 규칙 꼴 — 한 줄 = 「입력 = 동작」 · 입력은 키 칩(` · ` 로 여럿) · 동작 없는 줄은 칩만 */
function ruleList(keys: LocaleKey[]): HTMLElement {
const items = keys
const rows = keys
.flatMap((key) => L(key).split("\n"))
.map((line) => {
const at = line.indexOf(" = ");
const input = at < 0 ? line : line.slice(0, at);
return el("li", {
children: [
el("span", {
className: "app-settings__keys",
children: input.split(" · ").map((text) => createBadge(text, "neutral")),
}),
...(at < 0 ? [] : [el("span", { text: line.slice(at + 3) })]),
],
});
const chips = document.createDocumentFragment();
chips.append(...input.split(" · ").map((text) => createBadge(text, "neutral")));
return { label: chips, value: at < 0 ? undefined : line.slice(at + 3) };
});
return el("ul", { className: "app-settings__rules", children: items });
return createKv(rows, { rule: true });
}
function mouseTab(draft: InputPrefs): HTMLElement {
+7 -7
View File
@@ -1,10 +1,10 @@
[
{"name":"D01_00_overview","route":"#/d01-plan-register","project":"none","before":[{"wait":".b02-proj__grid"},{"wait_ms":600}],"clip":null,"callouts":[{"n":1,"sel":".b02-proj__grid > .ui-field:nth-child(1)"},{"n":2,"sel":".b02-proj__grid > .ui-field:nth-child(3)"},{"n":3,"sel":".b02-proj__grid > .ui-field:nth-child(5)"},{"n":4,"sel":".b02-proj__grid > .ui-field:nth-child(7)"},{"n":5,"sel":".b02-proj__grid > .ui-field:nth-child(8)"},{"n":6,"sel":".b02-proj__actions","pos":"br"}]},
{"name":"D01_00_overview","route":"#/d01-plan-register","project":"none","before":[{"wait":".ui-form-grid"},{"wait_ms":600}],"clip":null,"callouts":[{"n":1,"sel":".ui-form-grid > .ui-field:nth-child(1)"},{"n":2,"sel":".ui-form-grid > .ui-field:nth-child(3)"},{"n":3,"sel":".ui-form-grid > .ui-field:nth-child(5)"},{"n":4,"sel":".ui-form-grid > .ui-field:nth-child(7)"},{"n":5,"sel":".ui-form-grid > .ui-field:nth-child(8)"},{"n":6,"sel":".ui-general-block__actions--end","pos":"br"}]},
{"name":"D01_01_dashboard_new","route":"#/b01-account","project":"none","before":[{"wait_ms":2500},{"eval":"[...document.querySelectorAll('button')].find(x=>x.textContent.trim()==='+ 새 프로젝트'&&x.offsetParent).click()"},{"wait":".b01-new-project__grid"},{"wait_ms":600}],"clip":".b01-new-project","callouts":[{"n":1,"sel":"[data-new-project=\"plan\"]"}]},
{"name":"D01_02_year_region","route":"#/d01-plan-register","project":"none","before":[{"wait":".b02-proj__grid"},{"fill":".b02-proj__grid > .ui-field:nth-child(1) input","value":"2027"},{"fill":".b02-proj__grid > .ui-field:nth-child(2) input","value":"2029"},{"fill":".b02-proj__grid > .ui-field:nth-child(3) input","value":"울진군 금강송면"},{"wait_ms":400}],"clip":".b02-proj__grid","callouts":[{"n":1,"sel":".b02-proj__grid > .ui-field:nth-child(1)"},{"n":2,"sel":".b02-proj__grid > .ui-field:nth-child(2)"},{"n":3,"sel":".b02-proj__grid > .ui-field:nth-child(3)"},{"n":4,"sel":".b02-proj__grid > .ui-field:nth-child(4)"}]},
{"name":"D01_03_type_speed","route":"#/d01-plan-register","project":"none","before":[{"wait":".b02-proj__grid"},{"select":".b02-proj__grid > .ui-field:nth-child(6) select","value":"40"},{"wait_ms":400}],"clip":".b02-proj__grid","callouts":[{"n":1,"sel":".b02-proj__grid > .ui-field:nth-child(5)"},{"n":2,"sel":".b02-proj__grid > .ui-field:nth-child(6)"}]},
{"name":"D01_04_name","route":"#/d01-plan-register","project":"none","before":[{"wait":".b02-proj__grid"},{"fill":".b02-proj__grid > .ui-field:nth-child(3) input","value":"울진군 금강송면"},{"wait_ms":400}],"clip":".b02-proj__grid","callouts":[{"n":1,"sel":".b02-proj__grid > .ui-field:nth-child(7)"}]},
{"name":"D01_05_chainage","route":"#/d01-plan-register","project":"none","before":[{"wait":".b02-proj__grid"},{"fill":".b02-proj__grid > .ui-field:nth-child(8) input","value":"0"},{"fill":".b02-proj__grid > .ui-field:nth-child(9) input","value":"1100"},{"wait_ms":400}],"clip":".b02-proj__grid","callouts":[{"n":1,"sel":".b02-proj__grid > .ui-field:nth-child(8)"},{"n":2,"sel":".b02-proj__grid > .ui-field:nth-child(9)"}]},
{"name":"D01_06_errors","route":"#/d01-plan-register","project":"none","before":[{"wait":".b02-proj__grid"},{"fill":".b02-proj__grid > .ui-field:nth-child(1) input","value":"2029"},{"fill":".b02-proj__grid > .ui-field:nth-child(2) input","value":"2027"},{"fill":".b02-proj__grid > .ui-field:nth-child(8) input","value":"500"},{"fill":".b02-proj__grid > .ui-field:nth-child(9) input","value":"100"},{"click":".b02-proj__actions button:last-child"},{"wait":".ui-field__error.is-visible"},{"wait_ms":400}],"clip":".b02-proj__grid","callouts":[{"n":1,"sel":".b02-proj__grid > .ui-field:nth-child(2)"},{"n":2,"sel":".b02-proj__grid > .ui-field:nth-child(3)"},{"n":3,"sel":".b02-proj__grid > .ui-field:nth-child(9)"}]},
{"name":"D01_07_buttons","route":"#/d01-plan-register","project":"none","before":[{"wait":".b02-proj__actions"}],"clip":".b02-proj__actions","callouts":[{"n":1,"sel":".b02-proj__actions button:nth-child(1)"},{"n":2,"sel":".b02-proj__actions button:nth-child(2)"},{"n":3,"sel":".b02-proj__actions button:nth-child(3)"}]}
{"name":"D01_02_year_region","route":"#/d01-plan-register","project":"none","before":[{"wait":".ui-form-grid"},{"fill":".ui-form-grid > .ui-field:nth-child(1) input","value":"2027"},{"fill":".ui-form-grid > .ui-field:nth-child(2) input","value":"2029"},{"fill":".ui-form-grid > .ui-field:nth-child(3) input","value":"울진군 금강송면"},{"wait_ms":400}],"clip":".ui-form-grid","callouts":[{"n":1,"sel":".ui-form-grid > .ui-field:nth-child(1)"},{"n":2,"sel":".ui-form-grid > .ui-field:nth-child(2)"},{"n":3,"sel":".ui-form-grid > .ui-field:nth-child(3)"},{"n":4,"sel":".ui-form-grid > .ui-field:nth-child(4)"}]},
{"name":"D01_03_type_speed","route":"#/d01-plan-register","project":"none","before":[{"wait":".ui-form-grid"},{"select":".ui-form-grid > .ui-field:nth-child(6) select","value":"40"},{"wait_ms":400}],"clip":".ui-form-grid","callouts":[{"n":1,"sel":".ui-form-grid > .ui-field:nth-child(5)"},{"n":2,"sel":".ui-form-grid > .ui-field:nth-child(6)"}]},
{"name":"D01_04_name","route":"#/d01-plan-register","project":"none","before":[{"wait":".ui-form-grid"},{"fill":".ui-form-grid > .ui-field:nth-child(3) input","value":"울진군 금강송면"},{"wait_ms":400}],"clip":".ui-form-grid","callouts":[{"n":1,"sel":".ui-form-grid > .ui-field:nth-child(7)"}]},
{"name":"D01_05_chainage","route":"#/d01-plan-register","project":"none","before":[{"wait":".ui-form-grid"},{"fill":".ui-form-grid > .ui-field:nth-child(8) input","value":"0"},{"fill":".ui-form-grid > .ui-field:nth-child(9) input","value":"1100"},{"wait_ms":400}],"clip":".ui-form-grid","callouts":[{"n":1,"sel":".ui-form-grid > .ui-field:nth-child(8)"},{"n":2,"sel":".ui-form-grid > .ui-field:nth-child(9)"}]},
{"name":"D01_06_errors","route":"#/d01-plan-register","project":"none","before":[{"wait":".ui-form-grid"},{"fill":".ui-form-grid > .ui-field:nth-child(1) input","value":"2029"},{"fill":".ui-form-grid > .ui-field:nth-child(2) input","value":"2027"},{"fill":".ui-form-grid > .ui-field:nth-child(8) input","value":"500"},{"fill":".ui-form-grid > .ui-field:nth-child(9) input","value":"100"},{"click":".ui-general-block__actions--end button:last-child"},{"wait":".ui-field__error.is-visible"},{"wait_ms":400}],"clip":".ui-form-grid","callouts":[{"n":1,"sel":".ui-form-grid > .ui-field:nth-child(2)"},{"n":2,"sel":".ui-form-grid > .ui-field:nth-child(3)"},{"n":3,"sel":".ui-form-grid > .ui-field:nth-child(9)"}]},
{"name":"D01_07_buttons","route":"#/d01-plan-register","project":"none","before":[{"wait":".ui-general-block__actions--end"}],"clip":".ui-general-block__actions--end","callouts":[{"n":1,"sel":".ui-general-block__actions--end button:nth-child(1)"},{"n":2,"sel":".ui-general-block__actions--end button:nth-child(2)"},{"n":3,"sel":".ui-general-block__actions--end button:nth-child(3)"}]}
]
+5 -5
View File
@@ -1,7 +1,7 @@
[
{"name": "E01_00_overview", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".b02-proj__grid select"}, {"wait_ms": 800}], "clip": null, "callouts": [{"n": 1, "sel": ".ui-general-layout__title"}, {"n": 2, "sel": ".b02-proj__grid > :nth-child(1)"}, {"n": 3, "sel": ".b02-proj__grid > :nth-child(2)"}, {"n": 4, "sel": ".b02-proj__actions"}]},
{"name": "E01_01_source", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".b02-proj__grid select"}, {"wait_ms": 800}], "clip": ".b02-proj__grid > :nth-child(1)", "callouts": []},
{"name": "E01_02_name_typed", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".b02-proj__grid select"}, {"wait_ms": 800}, {"fill": ".b02-proj__grid input", "value": "용화 안정성평가 1차"}], "clip": ".b02-proj__grid", "callouts": [{"n": 1, "sel": ".b02-proj__grid input"}]},
{"name": "E01_03_buttons", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".b02-proj__grid select"}, {"wait_ms": 800}], "clip": ".b02-proj__actions", "callouts": [{"n": 1, "sel": ".b02-proj__actions .ui-help-btn"}, {"n": 2, "sel": ".b02-proj__actions .ui-btn--ghost:not(.ui-help-btn)"}, {"n": 3, "sel": ".b02-proj__actions .ui-btn--filled"}]},
{"name": "E01_04_help_modal", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".b02-proj__grid select"}, {"click": ".b02-proj__actions .ui-help-btn"}, {"wait": ".ui-help__grid"}, {"wait_ms": 600}], "clip": null, "callouts": []}
{"name": "E01_00_overview", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".ui-form-grid select"}, {"wait_ms": 800}], "clip": null, "callouts": [{"n": 1, "sel": ".ui-general-layout__title"}, {"n": 2, "sel": ".ui-form-grid > :nth-child(1)"}, {"n": 3, "sel": ".ui-form-grid > :nth-child(2)"}, {"n": 4, "sel": ".ui-general-block__actions--end"}]},
{"name": "E01_01_source", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".ui-form-grid select"}, {"wait_ms": 800}], "clip": ".ui-form-grid > :nth-child(1)", "callouts": []},
{"name": "E01_02_name_typed", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".ui-form-grid select"}, {"wait_ms": 800}, {"fill": ".ui-form-grid input", "value": "용화 안정성평가 1차"}], "clip": ".ui-form-grid", "callouts": [{"n": 1, "sel": ".ui-form-grid input"}]},
{"name": "E01_03_buttons", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".ui-form-grid select"}, {"wait_ms": 800}], "clip": ".ui-general-block__actions--end", "callouts": [{"n": 1, "sel": ".ui-general-block__actions--end .ui-help-btn"}, {"n": 2, "sel": ".ui-general-block__actions--end .ui-btn--ghost:not(.ui-help-btn)"}, {"n": 3, "sel": ".ui-general-block__actions--end .ui-btn--filled"}]},
{"name": "E01_04_help_modal", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".ui-form-grid select"}, {"click": ".ui-general-block__actions--end .ui-help-btn"}, {"wait": ".ui-help__grid"}, {"wait_ms": 600}], "clip": null, "callouts": []}
]
+10 -4
View File
@@ -78,10 +78,10 @@ async function fetchPipeSummary(projectId: string): Promise<PipeSummary> {
}
const PIPE_TOP = "관 공";
/** 미연결 램프 칸 class(`B08_Quantity_UI_Page.css` · 198-1) · 램프 자리 폭(12px 글 기준) */
/** 미연결 램프 칸 표지(198-1 · 풍선 · 가이드가 잡음) · 꼴 = 표 부품 칸 램프 · 램프 자리 폭(12px 글 기준) */
export const LAMP_CLASS = "b08-summary__lamp";
/** 모두 연결 = 같은 램프 초록(247-1) */
const LINKED_CLASS = `${LAMP_CLASS} is-linked`;
/** 모두 연결 = 같은 램프 초록 ✓(247-1) */
const LINKED_CLASS = `ui-data-table__lamp ${LAMP_CLASS} is-linked`;
const LAMP_W = 14;
const stationKey = (station: number): number => Math.round(station * 100);
/** 열 소수 자리 — 칸 · 계 값이 쓰는 만큼(최대 2). */
@@ -236,7 +236,13 @@ export function summaryTable(view: SummaryView): {
})),
total,
cellClass: (i, key) =>
key !== "station" ? null : missing[i] ? LAMP_CLASS : linked[i] ? LINKED_CLASS : null,
key !== "station"
? null
: missing[i]
? `ui-data-table__lamp ${LAMP_CLASS}`
: linked[i]
? LINKED_CLASS
: null,
frozen: 1,
empty: "",
caption: L("B08_Quantity_Tab_StructureSummary"),
-15
View File
@@ -17,21 +17,6 @@
border-top: 1px solid var(--color-border);
}
.b08-sheets__title {
display: flex;
align-items: baseline;
gap: var(--spacing-8);
margin: 0;
font-size: var(--text-body);
color: var(--color-text);
}
.b08-sheets__from {
font-size: var(--text-caption);
font-weight: var(--font-weight-regular);
color: var(--color-text-secondary);
}
/* M02 에 없는 조합 안내 — 빨간 글 */
.b08-sheets__combo > .ui-text {
color: var(--color-danger);
+2 -78
View File
@@ -1,4 +1,4 @@
/* B08 4탭 홍수량 계산서(PLAN 62-2 · 69-4) — 공용 카드 · 입력 · 드롭다운 위에 줄 배치만.
/* B08 4탭 홍수량 계산서(PLAN 62-2 · 69-4) — 공용 카드 · 계산 줄(557-2) · 입력 · 드롭다운 위에 카드 배치만.
색은 테마 토큰만(밝음 · 어두움 같이) · 테두리는 입력 · 선택 칸(공용 ui-input · ui-select)만
· 굵게 + 붉은 계열은 중요 결과(.is-key)만. */
@@ -61,83 +61,7 @@
color: var(--color-text);
}
.b08-hydro__formula {
margin: 0 0 0 var(--spacing-8);
font-family: var(--font-body);
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
}
.b08-hydro__row {
display: grid;
grid-template-columns: minmax(0, 1fr) 112px minmax(0, 0.8fr);
align-items: center;
gap: var(--spacing-8);
min-height: var(--control-height-sm);
}
.b08-hydro__row--f {
grid-template-columns: max-content minmax(0, 1fr) 112px;
}
.b08-hydro__label {
margin: 0;
color: var(--color-text-secondary);
}
.b08-hydro__label--select {
display: flex;
align-items: center;
gap: var(--spacing-8);
}
.b08-hydro__row .b08-hydro__formula {
margin: 0;
font-size: var(--text-caption);
}
.b08-hydro__hint {
color: var(--color-text-secondary);
font-size: var(--text-caption);
}
/* 계산 칸 — 테두리 없이 글자만 · 숫자 자리 맞춤 */
.b08-hydro__out,
.b08-hydro__value {
padding: 0 var(--spacing-8);
color: var(--color-text-body);
font-variant-numeric: tabular-nums;
text-align: right;
}
.b08-hydro__value {
text-align: left;
font-weight: var(--font-weight-medium);
}
.b08-hydro__in {
width: 100%;
min-width: 0;
box-sizing: border-box;
text-align: right;
font-variant-numeric: tabular-nums;
}
/* 줄에 바로 놓인 드롭다운은 값 칸 + 덧칸 — 선택지 글이 안 잘리게 */
.b08-hydro__row > .b08-hydro__select {
grid-column: 2 / 4;
justify-self: start;
width: auto;
}
/* 식 칸에 놓인 입력(하상경사 1 / ☐)은 값 칸 폭 */
.b08-hydro__row--f > .b08-hydro__in:nth-child(2) {
justify-self: start;
width: 112px;
}
/* 중요 결과 — Qd · 유출량 · 판정 · 유출율 · 글자색은 판정 따라(O K 초록 · N G 붉음 · 없으면 기본) */
.is-key.b08-hydro__out,
/* 중요 결과 — Qd · 유출량 · 판정 · 유출율 · 글자색은 판정 따라(O K 초록 · N G 붉음 · 없으면 기본) · 계산 칸 굵기 = 부품 계산 줄 */
.is-key.b08-hydro__verdict {
color: var(--color-text-body);
font-weight: var(--font-weight-bold);
+16 -35
View File
@@ -15,6 +15,7 @@
* ========================================================================== */
import { createCard, createInputField, createSelectField } from "@ui/ui_template_elements";
import { createCalcFormula, createCalcFormulaRow, createCalcRow } from "@ui/ui_template_calc_row";
export interface HydroInputs {
factor: number;
@@ -117,7 +118,7 @@ function sheetLabel(left: HTMLElement, right: HTMLElement, key: string): HTMLEle
const input = [left, right]
.map((card) => card.querySelector(`input[data-b08-hydro="${key}"]`))
.find(Boolean);
return input?.closest(".b08-hydro__row")?.querySelector(".b08-hydro__label") ?? null;
return input?.closest(".ui-calc-row")?.querySelector(".ui-calc-row__label") ?? null;
}
function node<K extends keyof HTMLElementTagNameMap>(
@@ -131,17 +132,8 @@ function node<K extends keyof HTMLElementTagNameMap>(
return el;
}
/** 식 글 — `^…^` 사이는 위첨자 · `_…_` 사이는 아래첨자. */
function formula(text: string, className = "b08-hydro__formula"): HTMLElement {
const box = node("div", className);
for (const part of text.split(/(\^[^^]+\^|_[^_]+_)/)) {
if (part.startsWith("^") && part.endsWith("^")) box.append(node("sup", "", part.slice(1, -1)));
else if (part.startsWith("_") && part.endsWith("_"))
box.append(node("sub", "", part.slice(1, -1)));
else if (part) box.append(document.createTextNode(part));
}
return box;
}
/** 식 글 — 부품 계산 줄의 식(`^…^` 위첨자 · `_…_` 아래첨자) */
const formula = createCalcFormula;
/** 관 단면 그림 — 단면 가정대로 물 찬 자리를 칠함(색은 테마 토큰 · CSS). */
function pipeFigure(section: HydroInputs["section"]): SVGSVGElement {
@@ -246,7 +238,7 @@ export function buildHydroSheet(
};
const out = new Map<string, HTMLElement>();
const outCell = (id: string, key = false): HTMLElement => {
const cell = node("span", `b08-hydro__out${key ? " is-key" : ""}`, DASH);
const cell = node("span", `ui-calc-row__out${key ? " is-key" : ""}`, DASH);
out.set(id, cell);
return cell;
};
@@ -258,7 +250,7 @@ export function buildHydroSheet(
};
const numberInput = (key: Key, placeholder = ""): HTMLInputElement => {
const { input } = createInputField({ type: "number", compact: true, placeholder });
input.classList.add("b08-hydro__in");
input.classList.add("ui-calc-row__in");
input.step = "any";
input.dataset.b08Hydro = key;
const value = inputs[key];
@@ -269,7 +261,7 @@ export function buildHydroSheet(
const [source, title] =
key === "slope_denominator" ? ["B06", "B06 횡단 경사"] : ["B05", "B05 분석"];
input.title = `${title} ${linked}`;
const mark = node("span", "b08-hydro__hint", source);
const mark = node("span", "ui-calc-row__hint", source);
mark.title = title;
mark.dataset.b08HydroLinked = key;
marks.set(key, mark);
@@ -298,27 +290,16 @@ export function buildHydroSheet(
options: choices.map(([value, text]) => ({ value, text })),
value: String(inputs[key]),
});
el.classList.add("b08-hydro__select");
el.classList.add("ui-calc-row__select");
el.dataset.b08Hydro = key;
el.addEventListener("change", () => commit(key, toValue(el.value)));
return el;
};
/** 표 줄 — 이름 · 값 칸(입력 또는 계산) · 덧칸 */
const row = (label: string | HTMLElement, value: HTMLElement, extra?: HTMLElement) => {
const line = node("div", "b08-hydro__row");
const name = typeof label === "string" ? node("span", "b08-hydro__label", label) : label;
line.append(name, value);
if (extra) line.append(extra);
return line;
};
/** 식 줄 — 이름 · 식 · 값 칸 */
const rowF = (label: string, middle: HTMLElement | null, value: HTMLElement) => {
const line = node("div", "b08-hydro__row b08-hydro__row--f");
line.append(node("span", "b08-hydro__label", label), middle ?? node("span"), value);
return line;
};
/** 표 줄 = 부품 계산 줄(이름 · 값 칸 · 덧칸) · 식 줄(이름 · 식 · 값 칸) */
const row = createCalcRow;
const rowF = createCalcFormulaRow;
const sub = (text: string): HTMLElement => node("h4", "b08-hydro__h2", text);
const hint = (text: string): HTMLElement => node("span", "b08-hydro__hint", text);
const hint = (text: string): HTMLElement => node("span", "ui-calc-row__hint", text);
/** B05 분석 값 참고(비교만) — 값이 없으면 칸 없음 */
const b05Hint = (
key: "design_flow_m3s" | "tc_minutes",
@@ -351,10 +332,10 @@ export function buildHydroSheet(
if (hasRain) out.get("r24")!.textContent = fmt(r24Site(current.return_period), 1);
});
const region: HTMLElement = site.region.name
? node("span", "b08-hydro__value", site.region.name)
? node("span", "ui-calc-row__value", site.region.name)
: (() => {
const { input } = createInputField({ compact: true, placeholder: "지역 입력" });
input.classList.add("b08-hydro__in");
input.classList.add("ui-calc-row__in");
input.dataset.b08Hydro = "region";
input.value = inputs.region;
input.addEventListener("change", () => commit("region", input.value.trim()));
@@ -375,7 +356,7 @@ export function buildHydroSheet(
formula("Q = 1/3.6 × f × rt × A"),
row(
(() => {
const label = node("span", "b08-hydro__label b08-hydro__label--select");
const label = node("span", "ui-calc-row__label ui-calc-row__label--select");
label.append("Qd : 설계 홍수량 (㎥/s) = Q ", factor);
return label;
})(),
@@ -406,7 +387,7 @@ export function buildHydroSheet(
formula("rt = (R_24_ / 24) × (24 / T)^0.557^"),
row("지역", region, hint(regionNote)),
row("재현기간", periodSelect),
row(formula("R_24_ : 확률강우량 (mm/day)", "b08-hydro__label"), r24Value, hint(rainNote)),
row(formula("R_24_ : 확률강우량 (mm/day)", "ui-calc-row__label"), r24Value, hint(rainNote)),
],
});
left.classList.add("b08-hydro__card");
+7 -11
View File
@@ -14,7 +14,7 @@
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import { createButton, showConfirmDialog, showToast } from "@ui/ui_template_elements";
import { attachCollapsible } from "@ui/ui_template_collapsible";
import { attachCollapsible, createCollapsibleHead } from "@ui/ui_template_collapsible";
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
import {
projectItems,
@@ -166,16 +166,12 @@ export function linkSection(
// 공용 접기(163-1) — 모두 연결이면 접힘 · 미연결 있으면 펼침
section.className = `b08-sheets__combo b08-link ui-collapsible${missing ? "" : " is-collapsed"}`;
section.dataset.b08Link = "section";
const title = document.createElement("h4");
title.className = "b08-sheets__title ui-collapsible__title";
const count = document.createElement("span");
count.className = "b08-sheets__from";
count.textContent = `${L("B08_Quantity_Link_Linked")} ${slots.length - missing} / ${slots.length}`;
const label = document.createElement("span");
label.textContent = L("B08_Quantity_Link_Title");
label.append(" ", count);
title.append(label);
section.append(title);
const head = createCollapsibleHead({
title: L("B08_Quantity_Link_Title"),
sub: `${L("B08_Quantity_Link_Linked")} ${slots.length - missing} / ${slots.length}`,
level: 4,
});
section.append(head.root);
attachCollapsible(section);
for (const facility of facilities) {
const line = document.createElement("div");
@@ -243,25 +243,3 @@
.b08-milestone__route-addr {
color: var(--color-text-secondary);
}
/* 단가연결 램프(251-4) — 항목 칸 앞 · 노랑 = 없음 · 초록 = 연결 */
[data-b08-milestone="table"] td.b08-milestone__lamp::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
margin-right: 6px;
border-radius: 50%;
background: var(--color-warning);
vertical-align: middle;
}
[data-b08-milestone="table"] td.b08-milestone__lamp.is-linked::before {
content: "✓"; /* 성공 램프는 ✓ 같이(색만으로 안 가름) */
background: none;
color: var(--color-success);
font-size: 9px;
font-weight: var(--font-weight-bold);
line-height: 8px;
text-align: center;
}
+7 -2
View File
@@ -42,7 +42,7 @@ import {
type MilestoneView,
} from "./B08_Quantity_UI_Milestone_Route";
/** 단가연결 램프 칸 class(`B08_Quantity_UI_Milestone.css`) */
/** 단가연결 램프 칸 표지(풍선이 잡음) · 꼴 = 표 부품 칸 램프 */
const LAMP = "b08-milestone__lamp";
/** 램프 호버 글 — 연결된 것 */
@@ -86,7 +86,12 @@ function tableView(projectId: string): MilestoneView {
caption: "거리이정표",
empty: "",
editOnDouble: true,
cellClass: (i, key) => (key !== "item" ? null : routeAt(i)?.ref ? `${LAMP} is-linked` : LAMP),
cellClass: (i, key) =>
key !== "item"
? null
: routeAt(i)?.ref
? `ui-data-table__lamp ${LAMP} is-linked`
: `ui-data-table__lamp ${LAMP}`,
editable: (_i, key) => key === "item" || key === "company" || key === "note",
onEdit: (i, key, text) => {
const at = keys[i];
-23
View File
@@ -58,29 +58,6 @@ html.is-bm-route .b08-quantity > .b08-quantity__tabs:first-child {
overflow-wrap: anywhere;
}
/* 구조물 집계표 미연결 램프(198-1) — 측점 칸 왼쪽 경고 노랑 점 하나(177-1 미연결 노랑 토큰) · 연결 줄은 없음 */
[data-b08-summary="table"] td.b08-summary__lamp::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
margin-right: 6px;
border-radius: 50%;
background: var(--color-warning);
vertical-align: middle;
}
/* 247-1 — 모두 연결 = 같은 램프 초록 */
[data-b08-summary="table"] td.b08-summary__lamp.is-linked::before {
content: "✓"; /* 성공 램프는 ✓ 같이(색만으로 안 가름) */
background: none;
color: var(--color-success);
font-size: 9px;
font-weight: var(--font-weight-bold);
line-height: 8px;
text-align: center;
}
/* 좌측 패널 옵션(122) — 연암 · 경암 두 칸 나란히 · 설명 한 줄 */
.b08-options__row {
display: grid;
@@ -122,14 +122,11 @@ export function openHaulModal(o: HaulModalOpts): void {
openModal({
title: `운반 단가 — ${o.name}`,
closeLabel: "닫기",
// 크기(276) — 부품 넓은 중간 창(1100 × 680) · 가운데 · 몸 안 두 칸이 따로 스크롤
wide: true,
width: "1100px",
dialogClass: "b09-haul-modal",
mount: (body, close, head) => {
// 크기(276) — 공용 큰 창(96vw × 94vh)보다 작게 · 가운데 · 몸 안 두 칸이 따로 스크롤
body.parentElement?.setAttribute(
"style",
"--modal-max:min(680px,86vh);width:min(1100px,94vw);height:var(--modal-max)",
);
const title = el("strong");
const linkBox = el("div", {
attrs: { style: "display:flex;gap:8px;align-items:center;margin-bottom:8px" },
-26
View File
@@ -138,32 +138,6 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
font-weight: var(--font-weight-bold);
}
/* 257-1 설계내역서 단가연결 칸 = 램프(B08 구조물집계표 램프 꼴 · 글 숨김 · 풍선 = 글) — 노랑 = 없음 · 초록 = 연결 */
.b09-estimation .ui-data-table__table tbody td.b09-bill__lamp {
font-size: 0;
text-align: center;
}
.b09-estimation .ui-data-table__table tbody td.b09-bill__lamp::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-warning);
vertical-align: middle;
}
.b09-estimation .ui-data-table__table tbody td.b09-bill__lamp.is-linked::before {
content: "✓"; /* 성공 램프는 ✓ 같이(색만으로 안 가름) */
background: none;
color: var(--color-success);
font-size: 9px;
font-weight: var(--font-weight-bold);
line-height: 8px;
text-align: center;
}
/* 184-3 설계내역서 고친 칸 — 자동 값과 다른 칸(풍선 = 자동 값) */
.b09-estimation .ui-data-table__table tbody td.is-edited {
background: color-mix(in srgb, var(--color-warning) 22%, var(--color-canvas));
@@ -94,7 +94,7 @@ export const buildBill: TabBuild = (ctx): BillTable => {
const r = rows[i];
const lamp =
key === "connect" && r && linkable(raw[i])
? `b09-bill__lamp${linkText(raw[i]) === "단가연결" ? "" : " is-linked"}`
? `ui-data-table__lamp ui-data-table__lamp--only b09-bill__lamp${linkText(raw[i]) === "단가연결" ? "" : " is-linked"}`
: "";
return [lamp, edited(r, key) ? "is-edited" : ""].filter(Boolean).join(" ") || null;
},
+5 -8
View File
@@ -7,7 +7,6 @@
* [등록] = POST /api/d01/project → 현재 프로젝트 · D02 로.
* ========================================================================== */
import "../B02_ProjRegister/B02_ProjRegister_UI_Style.css";
import { ROUTES } from "@config/config_frontend";
import {
createButton,
@@ -15,7 +14,7 @@ import {
createInputField,
createSelectField,
} from "@ui/ui_template_elements";
import { createGeneralLayout } from "@ui/ui_template_general_layout";
import { createFormGrid, createGeneralLayout } from "@ui/ui_template_general_layout";
import { createHelpButton } from "@ui/ui_template_help";
import { createText } from "@ui/ui_template_text";
import { t } from "@ui/ui_template_locale";
@@ -144,13 +143,11 @@ export function renderD01PlanRegister(root: HTMLElement): void {
);
};
const grid = document.createElement("div");
grid.className = "b02-proj__grid";
grid.append(
...[yearStart, yearEnd, region, kind, roadType, speed, name, startM, endM].map((f) => f.root),
const grid = createFormGrid(
[yearStart, yearEnd, region, kind, roadType, speed, name, startM, endM].map((f) => f.root),
);
const actions = document.createElement("div");
actions.className = "ui-general-block__actions b02-proj__actions";
actions.className = "ui-general-block__actions ui-general-block__actions--end";
actions.append(
createHelpButton(),
createButton({
@@ -161,7 +158,7 @@ export function renderD01PlanRegister(root: HTMLElement): void {
createButton({ label: t("DE_Reg_Submit"), variant: "filled", onClick: submit }),
);
const layout = createGeneralLayout({
pageClass: "b02-proj",
narrow: true,
title: t("DE_Reg_PlanTitle"),
subtitle: t("DE_Reg_PlanSubtitle"),
content: createCard({ body: [grid, actions], raised: true }),
+4 -7
View File
@@ -5,7 +5,6 @@
* [등록] = POST /api/e01/project → 현재 프로젝트 · E02 로.
* ========================================================================== */
import "../B02_ProjRegister/B02_ProjRegister_UI_Style.css";
import { API_BASE_URL, ROUTES } from "@config/config_frontend";
import {
createButton,
@@ -14,7 +13,7 @@ import {
createSelectField,
el,
} from "@ui/ui_template_elements";
import { createGeneralLayout } from "@ui/ui_template_general_layout";
import { createFormGrid, createGeneralLayout } from "@ui/ui_template_general_layout";
import { createHelpButton } from "@ui/ui_template_help";
import { t } from "@ui/ui_template_locale";
import { createGroupProject } from "../A00_Common/group_workflow";
@@ -74,11 +73,9 @@ export function renderE01StabRegister(root: HTMLElement): void {
if (!typed) name.input.value = autoName();
})();
const grid = document.createElement("div");
grid.className = "b02-proj__grid";
grid.append(source.root, name.root);
const grid = createFormGrid([source.root, name.root]);
const actions = document.createElement("div");
actions.className = "ui-general-block__actions b02-proj__actions";
actions.className = "ui-general-block__actions ui-general-block__actions--end";
actions.append(
createHelpButton(),
createButton({
@@ -89,7 +86,7 @@ export function renderE01StabRegister(root: HTMLElement): void {
submitButton,
);
const layout = createGeneralLayout({
pageClass: "b02-proj",
narrow: true,
title: t("DE_Reg_StabTitle"),
subtitle: t("DE_Reg_StabSubtitle"),
content: createCard({ body: [grid, note, actions], raised: true }),
@@ -53,7 +53,7 @@ new Function("exports", "module", "require", js(fs.readFileSync({table}, "utf8")
const LAMP_CLASS = {json.dumps(_lamp_class())};
const missing = ["", "BOX", ""];
const linked = [false, false, false];
const LINKED_CLASS = LAMP_CLASS + " is-linked";
const LINKED_CLASS = "ui-data-table__lamp " + LAMP_CLASS + " is-linked";
const cellClass = {cell.group(1)};
const html = box.exports.renderDataTableHtml({{
columns: [{{ key: "station", label: "측점" }}, {{ key: "c1", label: "조합" }}],
@@ -68,7 +68,10 @@ console.log(JSON.stringify({{
"""
)
assert got["count"] == 1
assert f'<td class="is-frozen is-edge {_lamp_class()}"' in got["row1"] # 측점 칸(첫 칸)
# 꼴 = 표 부품 칸 램프(557-2) · 표지 = 화면 class
assert (
f'<td class="is-frozen is-edge ui-data-table__lamp {_lamp_class()}"' in got["row1"]
) # 측점 칸(첫 칸)
assert _lamp_class() not in got["row0"]
@@ -106,11 +109,8 @@ def test_미지정_열_없음_줄_값_그대로():
def test_램프_경고_노랑_토큰():
css = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Page.css").read_text(encoding="utf-8")
m = re.search(
r'\[data-b08-summary="table"\] td\.' + re.escape(_lamp_class()) + r"::before\s*\{([^}]*)\}",
css,
)
css = (ROOT / "ui_template" / "ui_template_data_table.css").read_text(encoding="utf-8")
m = re.search(r"td\.ui-data-table__lamp::before\s*\{([^}]*)\}", css) # 표 부품 칸 램프(557-2)
assert m
assert re.search(r"background:\s*var\(--color-warning\)", m.group(1))
assert "border-radius: 50%" in m.group(1)
@@ -74,8 +74,12 @@ console.log(JSON.stringify({{ rows: rows.slice(0, 3), tail: html.split("</tbody>
"""
)
lamp = _lamp_class()
assert f'class="is-frozen is-edge {lamp} is-linked"' in got["rows"][0] # 0 = 모두 연결
assert f'class="is-frozen is-edge {lamp}"' in got["rows"][1] # 20 = 빈 칸 하나 = 노랑
assert (
f'class="is-frozen is-edge ui-data-table__lamp {lamp} is-linked"' in got["rows"][0]
) # 0 = 모두 연결
assert (
f'class="is-frozen is-edge ui-data-table__lamp {lamp}"' in got["rows"][1]
) # 20 = 빈 칸 하나 = 노랑
assert lamp not in got["rows"][2] # 40 = 시설 없음
assert got["lamps"] == 2 # 합계 줄 없음
@@ -100,8 +104,9 @@ console.log(JSON.stringify([text(cell(0)), text(cell(1))]));
def test_초록_성공_토큰():
css = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Page.css").read_text(encoding="utf-8")
m = re.search(r"td\.b08-summary__lamp\.is-linked::before\s*\{([^}]*)\}", css)
# 꼴 = 표 부품 칸 램프(557-2)
css = (ROOT / "ui_template" / "ui_template_data_table.css").read_text(encoding="utf-8")
m = re.search(r"td\.ui-data-table__lamp\.is-linked::before\s*\{([^}]*)\}", css)
assert (
m
and re.search(r"color:\s*var\(--color-success\)", m.group(1))
+6 -1
View File
@@ -1,5 +1,6 @@
"""PLAN 251-1 — 총괄자재표 → 운반 묶음(거리이정표 행 · B09 운반공 줄 한 원천)."""
import re
import sys
from pathlib import Path
@@ -138,7 +139,11 @@ console.log(JSON.stringify([refText(null),
"단가연결 — 일위대가 회사 조합",
]
# 램프 = 항목 칸 · 우클릭 = [단가연결] · 연결 있으면 [단가연결 풀기] · 창 = B09 와 같은 것
assert 'key !== "item" ? null : routeAt(i)?.ref ? `${LAMP} is-linked` : LAMP' in src
# 꼴 = 표 부품 칸 램프(557-2) · 표지 = LAMP
assert (
'key !== "item" ? null : routeAt(i)?.ref ? `ui-data-table__lamp ${LAMP} is-linked` : `ui-data-table__lamp ${LAMP}`'
in re.sub(r"\s+", " ", src)
)
assert "openHaulModal({" in src and '"단가연결 풀기"' in src # 263-1 = 운반 단가 모달 하나
diagram = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Milestone_Diagram.ts").read_text("utf-8")
assert "Math.abs(h - lastH) > 1" in diagram # 틀 높이 바뀜 = 다시 그림(아래 잘림 없음)
+17 -10
View File
@@ -35,14 +35,17 @@ def test_램프_칸_class_풍선_머리():
body = JS.replace("__ROWS__", json.dumps(ROWS, ensure_ascii=False))
out = json.loads(_node(_ts([*SRC, "UI_Tabs_Bill"]) + body))
assert out["col"] == {"label": "단가\n연결", "width": 44, "title": "단가연결"}
assert out["cls"] == [
"b09-bill__lamp is-linked", # 자동 줄 · 호표 연결
"b09-bill__lamp", # 연결 없음 = 노랑
"b09-bill__lamp is-linked", # 손 줄 · 비고 대가3
None, # 자재 줄 = 연결 대상 아님
None,
None, # 묶음 줄
]
assert (
out["cls"]
== [
"ui-data-table__lamp ui-data-table__lamp--only b09-bill__lamp is-linked", # 자동 줄 · 호표 연결
"ui-data-table__lamp ui-data-table__lamp--only b09-bill__lamp", # 연결 없음 = 노랑
"ui-data-table__lamp ui-data-table__lamp--only b09-bill__lamp is-linked", # 손 줄 · 비고 대가3
None, # 자재 줄 = 연결 대상 아님
None,
None, # 묶음 줄
]
)
assert out["tip"] == ["연결됨", "단가연결", "대가3", None, None, None]
assert out["other"] is None
labels = [g[0] for g in out["groups"]]
@@ -52,7 +55,11 @@ def test_램프_칸_class_풍선_머리():
def test_CSS_램프_초록_노랑_머리_줄바꿈():
css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
assert "td.b09-bill__lamp::before" in css and "var(--color-warning)" in css
assert "td.b09-bill__lamp.is-linked::before" in css and "var(--color-success)" in css
part = (ROOT / "ui_template" / "ui_template_data_table.css").read_text(
"utf-8"
) # 꼴 = 표 부품(557-2)
assert "td.ui-data-table__lamp::before" in part and "var(--color-warning)" in part
assert "td.ui-data-table__lamp.is-linked::before" in part and "var(--color-success)" in part
assert "td.ui-data-table__lamp--only" in part and "b09-bill__lamp" not in css
head = css.split("[data-b09-table] thead th {", 1)[1].split("}", 1)[0]
assert "white-space: pre-line" in head
@@ -28,7 +28,9 @@ def test_연결_있으면_운반_모달_없으면_옛_길():
def test_운반_모달_작게():
modal = _src("B09_Estimation/B09_Estimation_UI_Haul_Modal.ts")
assert '"--modal-max:min(680px,86vh);width:min(1100px,94vw);height:var(--modal-max)"' in modal
assert 'width: "1100px",' in modal # 부품 넓은 중간 창(557-2)
part = (ROOT / "ui_template" / "ui_template_modal.css").read_text(encoding="utf-8")
assert '.ui-modal.ui-modal--wide[data-width="1100px"]' in part and "min(680px, 86vh)" in part
assert "min-height:60vh" not in modal and "flex:1;min-height:0" in modal
+1 -1
View File
@@ -38,6 +38,6 @@ def test_옛_보라_이름은_별칭():
):
assert f"--color-{old}: var(--color-{new});" in root, old
assert "--color-accent: #1f7a4d;" in root and "--color-danger: #c0392b;" in root
assert "--color-danger-fill: #e5484d;" in root
assert "--color-danger-fill: #d6363c;" in root # 557-2 흰 글 대비 4.5 이상
dark = THEME[THEME.index(':root[data-theme="dark"]') :]
assert "deep-iris" not in dark and "royal-amethyst" not in dark # 어두움도 별칭으로 따라옴
+124
View File
@@ -0,0 +1,124 @@
"""557-2 부품 · 테마 작은 흠 지킴 시험 — 위험 채움 흰 글 대비 · 계산 줄 · 칸 램프 · 넓은 중간 창 · 값 목록 규칙 꼴 ·
구역 머리 · 입력 칸 2열(D01 · E01 이 B02 클래스를 안 빌림)."""
import re
from test_552_3_card import ROOT, _text, check_part, kit_line
def _lum(h: str) -> float:
c = [int(h[i : i + 2], 16) / 255 for i in (1, 3, 5)]
c = [v / 12.92 if v <= 0.03928 else ((v + 0.055) / 1.055) ** 2.4 for v in c]
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
def _ratio(a: str, b: str) -> float:
x, y = sorted((_lum(a), _lum(b)), reverse=True)
return (x + 0.05) / (y + 0.05)
def _tokens(block: str) -> dict[str, str]:
return dict(re.findall(r"--(color-[a-z-]+):\s*(#[0-9a-fA-F]{6})\b", block))
def test_위험_채움_흰_글_대비():
theme = _text("ui_template_theme.css")
light = _tokens(theme[theme.index(":root {") :].split("\n}\n", 1)[0])
dark = _tokens(theme[theme.index(':root[data-theme="dark"]') :].split("\n}\n", 1)[0])
# 채운 바탕 위 글 = canvas(밝음 흰 · 어두움 짙은 바탕)
assert _ratio(light["color-danger-fill"], light["color-canvas"]) >= 4.5
assert _ratio(dark["color-danger-fill"], dark["color-canvas"]) >= 4.5
assert _ratio(light["color-danger"], light["color-canvas"]) >= 4.5 # 위험 단추 · 글
assert _ratio(dark["color-danger"], dark["color-canvas"]) >= 4.5
assert light["color-danger"] == "#c0392b" # 위험 글 색 그대로
def test_계산_줄():
check_part(
"calc-row",
"ui_template_calc_row.ts",
["ui_template_calc_row.css"],
["createCalcRow", "createCalcFormulaRow", "createCalcFormula"],
[
"ui-calc-row",
"ui-calc-row--formula",
"ui-calc-row__label",
"ui-calc-row__out",
"ui-calc-row__in",
],
)
sheet = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Hydro_Sheet.ts").read_text(encoding="utf-8")
css = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Hydro.css").read_text(encoding="utf-8")
for old in (
"b08-hydro__row",
"b08-hydro__label",
"b08-hydro__formula",
"b08-hydro__out",
"b08-hydro__in",
):
assert old not in sheet and old not in css, old
def test_칸_램프():
css = _text("ui_template_data_table.css")
assert "td.ui-data-table__lamp::before" in css and "var(--color-warning)" in css
assert re.search(r"td\.ui-data-table__lamp\.is-linked::before\s*\{[^}]*content: \"✓\"", css)
assert "td.ui-data-table__lamp--only" in css
assert "칸 램프" in kit_line("table") and "kitLampTable()" in kit_line("table")
for page, old in (
("B08_Quantity/B08_Quantity_UI_Page.css", "b08-summary__lamp"),
("B08_Quantity/B08_Quantity_UI_Milestone.css", "b08-milestone__lamp"),
("B09_Estimation/B09_Estimation_UI_Page.css", "b09-bill__lamp"),
):
assert old not in (ROOT / page).read_text(encoding="utf-8"), page
def test_넓은_중간_창():
css = _text("ui_template_modal.css")
assert re.search(
r'\.ui-modal\.ui-modal--wide\[data-width="1100px"\]\s*\{[^}]*min\(1100px, 94vw\)', css
)
assert '"1100px"' in _text("ui_template_modal.ts") and "1100" in kit_line("modal")
haul = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Haul_Modal.ts").read_text(encoding="utf-8")
assert 'width: "1100px"' in haul and "--modal-max:min(680px" not in haul
def test_값_목록_규칙_꼴():
assert "ui-kv--rule" in _text("ui_template_kv.css") and "rule: true" in kit_line("kv")
app = (ROOT / "A00_Common" / "app_settings.ts").read_text(encoding="utf-8")
assert "createKv(rows, { rule: true })" in app
assert "app-settings__rules" not in (ROOT / "A00_Common" / "app_settings.css").read_text(
encoding="utf-8"
)
def test_구역_머리():
ts = _text("ui_template_collapsible.ts")
assert "export function createCollapsibleHead" in ts and "kitSection()" in kit_line(
"collapsible"
)
link = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Link.ts").read_text(encoding="utf-8")
assert "createCollapsibleHead(" in link and "b08-sheets__title" not in link
def test_입력_칸_2열_안_빌림():
css = _text("ui_template_general_layout.css")
assert (
".ui-form-grid" in css
and ".ui-general-layout--narrow" in css
and ".ui-general-block__actions--end" in css
)
for page in (
"D01_PlanRegister/D01_PlanRegister_UI_Page.ts",
"E01_StabRegister/E01_StabRegister_UI_Page.ts",
):
src = (ROOT / page).read_text(encoding="utf-8")
assert (
"b02-proj" not in src
and "B02_ProjRegister_UI_Style" not in src
and "createFormGrid(" in src
), page
for scene in ("D01", "E01"):
assert "b02-proj" not in (
ROOT / "A05_EduDetail" / "guide" / scene / "_scenes.json"
).read_text(encoding="utf-8")
+83
View File
@@ -0,0 +1,83 @@
/* 계산 줄(557-2) — 이름 | 값 112 | 덧칸 · 식 줄 = 이름 | 식 | 값 112 · 테두리는 입력 · 고르기 부품만 */
.ui-calc-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 112px minmax(0, 0.8fr);
align-items: center;
gap: var(--spacing-8);
min-height: var(--control-height-sm);
}
.ui-calc-row--formula {
grid-template-columns: max-content minmax(0, 1fr) 112px;
}
.ui-calc-row__label {
margin: 0;
color: var(--color-text-secondary);
}
.ui-calc-row__label--select {
display: flex;
align-items: center;
gap: var(--spacing-8);
}
.ui-calc-formula {
margin: 0 0 0 var(--spacing-8);
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
}
.ui-calc-row .ui-calc-formula {
margin: 0;
font-size: var(--text-caption);
}
.ui-calc-row__hint {
color: var(--color-text-secondary);
font-size: var(--text-caption);
}
/* 계산 칸 — 테두리 없이 글자만 · 숫자 자리 맞춤 */
.ui-calc-row__out,
.ui-calc-row__value {
padding: 0 var(--spacing-8);
color: var(--color-text-body);
font-variant-numeric: tabular-nums;
text-align: right;
}
.ui-calc-row__value {
text-align: left;
font-weight: var(--font-weight-medium);
}
.ui-calc-row__out.is-key {
font-weight: var(--font-weight-bold);
}
.ui-calc-row__in {
width: 100%;
min-width: 0;
box-sizing: border-box;
text-align: right;
font-variant-numeric: tabular-nums;
}
/* 줄에 바로 놓인 고르기는 값 칸 + 덧칸 — 선택지 글이 안 잘리게 */
.ui-calc-row > .ui-calc-row__select {
grid-column: 2 / 4;
justify-self: start;
width: auto;
}
/* 식 칸에 놓인 입력(하상경사 1 / ☐)은 값 칸 폭 */
.ui-calc-row--formula > .ui-calc-row__in:nth-child(2) {
justify-self: start;
width: 112px;
}
/* 숨김 — display 가 [hidden] 을 덮지 않게(공용 기본 꼴이 안 실린 곳 · 페이지 꾸밈 위에서도) */
:is(.ui-calc-row, .ui-calc-row__label--select)[hidden] {
display: none !important;
}
+65
View File
@@ -0,0 +1,65 @@
/* =============================================================================
* ui_template_calc_row.ts
* 부품 「계산 줄」(557-2) — 계산서 한 줄: 이름 · 값 칸(입력 또는 계산 결과) · 덧칸(식 · 안내 · 둘째 값).
* 꼴 = 값 줄(`createCalcRow` · 이름 | 값 112 | 덧칸) · 식 줄(`createCalcFormulaRow` · 이름 | 식 | 값 112)
* · 식 글(`createCalcFormula` · `^…^` 위첨자 · `_…_` 아래첨자) · 이름 칸 안 고르기(`ui-calc-row__label--select`)
* · 칸 조각 클래스 = 입력 `ui-calc-row__in` · 고르기 `__select`(값 칸 + 덧칸) · 결과 글 `__out`(숫자 오른쪽 · 중요 `is-key` 굵게) · 글 값 `__value` · 안내 `__hint`
* 크기 = 줄 최소 30(작은 입력) · 값 칸 112 · 사이 8 · 이름 14 · 식 · 안내 12
* 상태 = 기본만(입력 · 고르기 부품이 저마다 상태를 가짐 · 판정 색은 화면이 `is-key` 에 줌)
* 쓰는 법 = `createCalcRow("f : 유출계수", input)` · `createCalcFormulaRow("유속 (m/s)", createCalcFormula("V = 1/n × R^2/3^"), out)`
* 대신하는 옛 변종 = B08 수리집수면적표 `b08-hydro__row(--f)` · `__label` · `__formula` · `__out` · `__value` · `__in` · `__hint` · `__select`
* ========================================================================== */
import "./ui_template_calc_row.css";
function span(className: string, text = ""): HTMLElement {
const el = document.createElement("span");
el.className = className;
el.textContent = text;
return el;
}
/** 식 글 — `^…^` 사이는 위첨자 · `_…_` 사이는 아래첨자 · className 을 주면 이름 칸 등으로 씀 */
export function createCalcFormula(text: string, className = "ui-calc-formula"): HTMLElement {
const box = document.createElement("div");
box.className = className;
for (const part of text.split(/(\^[^^]+\^|_[^_]+_)/)) {
const tag =
part.startsWith("^") && part.endsWith("^")
? "sup"
: part.startsWith("_") && part.endsWith("_")
? "sub"
: "";
if (tag) {
const el = document.createElement(tag);
el.textContent = part.slice(1, -1);
box.append(el);
} else if (part) box.append(part);
}
return box;
}
/** 값 줄 — 이름 · 값 칸(입력 또는 계산) · 덧칸 */
export function createCalcRow(
label: string | HTMLElement,
value: HTMLElement,
extra?: HTMLElement,
): HTMLElement {
const row = document.createElement("div");
row.className = "ui-calc-row";
row.append(typeof label === "string" ? span("ui-calc-row__label", label) : label, value);
if (extra) row.append(extra);
return row;
}
/** 식 줄 — 이름 · 식(없으면 빈 칸) · 값 칸 */
export function createCalcFormulaRow(
label: string,
middle: HTMLElement | null,
value: HTMLElement,
): HTMLElement {
const row = document.createElement("div");
row.className = "ui-calc-row ui-calc-row--formula";
row.append(span("ui-calc-row__label", label), middle ?? document.createElement("span"), value);
return row;
}
+10 -1
View File
@@ -2,10 +2,11 @@
* ui_template_collapsible.ts
* 부품 「접이 카드」(552-3 · 목록 16) — 제목 줄을 누르면 몸이 접히고 펴지는 카드.
* 꼴 = 패널 카드(`createPanelCard` · 왼쪽 패널 · 공용 양식 붙음 · ⚙ 곁값) · 구역(아무 상자에 클래스만 + `attachCollapsible`)
* · 구역 머리(`createCollapsibleHead` · 557-2) = 패널 머리(제목 14 · 부제 12 둘째 줄)를 접는 제목으로
* 크기 = 제목 줄 32(글 14 · 캐럿 ▾ ▸ 글자 1.7 배)
* 상태 = 기본 · 마우스(캐럿 진해짐) · 펼침 / 접힘(`.is-collapsed`)
* 쓰는 법 = `const card = createPanelCard("구조물 배치", false); card.append(form); attachCollapsible(panel)`
* 대신하는 옛 변종 = 날 `<details>` — B06 넷 · E01 `e-missing` · E03 `e03-prob-src` · M01 넷 · M02 일곱 · A07 약관
* 대신하는 옛 변종 = 날 `<details>` — B06 넷 · E01 `e-missing` · E03 `e03-prob-src` · M01 넷 · M02 일곱 · A07 약관 · B08 구조물도 「조합 연결」 `b08-sheets__title`
* 모양 = 제목 · 캐럿 `ui_template_theme.css` · 카드 `ui_template_overlay.css` · 마우스 `ui_template_collapsible.css`
*
* 페이지 공용 "접기 컨테이너" 동작. (B05 컨테이너 템플릿 기준으로 재사용)
@@ -20,6 +21,7 @@
* ========================================================================== */
import "./ui_template_collapsible.css";
import { createPanelHead, type PanelHead, type PanelHeadOptions } from "./ui_template_panel_head";
/**
* 좌측 패널 컨테이너 공용 카드(269) — 접는 제목 + 공용 양식(`ui-panel-form`) · 내용은 카드에 바로 붙임 · 기본 닫힘(389-3).
@@ -51,6 +53,13 @@ export function createPanelCard(
return section;
}
/** 구역 머리(557-2) — 패널 머리(제목 · 부제)를 접는 제목으로 · 컨테이너에 `ui-collapsible` + `attachCollapsible` */
export function createCollapsibleHead(opts: PanelHeadOptions): PanelHead {
const head = createPanelHead(opts);
head.root.classList.add("ui-collapsible__title");
return head;
}
/** root 아래 `.ui-collapsible__title` 클릭을 위임 처리해 해당 컨테이너를 접거나 편다. */
export function attachCollapsible(root: HTMLElement): void {
root.addEventListener("click", (event) => {
+32 -1
View File
@@ -64,6 +64,37 @@
white-space-collapse: preserve-spaces; /* 앞 빈칸(들여쓰기) · 겹 빈칸을 접지 않음 · 줄바꿈은 안 함 */
}
/* 칸 램프(557-2) — 칸 글 앞 점 8 · 노랑 = 없음 · 연결 = 초록 ✓(색만으로 안 가름) · --only = 글 숨김 · 점만 가운데 */
.ui-data-table__table td.ui-data-table__lamp::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
margin-right: 6px;
border-radius: 50%;
background: var(--color-warning);
vertical-align: middle;
}
.ui-data-table__table td.ui-data-table__lamp.is-linked::before {
content: "✓";
background: none;
color: var(--color-success);
font-size: 9px;
font-weight: var(--font-weight-bold);
line-height: 8px;
text-align: center;
}
.ui-data-table__table td.ui-data-table__lamp--only {
font-size: 0;
text-align: center;
}
.ui-data-table__table td.ui-data-table__lamp--only::before {
margin-right: 0;
}
/* 숫자 — 오른쪽 맞춤 · 자리 폭 같게 */
.ui-data-table__table [data-num] {
text-align: right;
@@ -291,6 +322,6 @@
}
/* 숨김 — display 가 [hidden] 을 덮지 않게(공용 기본 꼴이 안 실린 곳 · 페이지 꾸밈 위에서도) */
.ui-data-table__icon[hidden] {
:is(.ui-data-table__icon, .ui-data-table__lamp)[hidden] {
display: none !important;
}
+2
View File
@@ -21,6 +21,8 @@
* 칸 누른 뒤 Enter · F2 = 그 칸 입력 시작(PLAN 243-1 · keyEdit false = 표 밖이 맡음)
* 같은 값 세로 병합(PLAN 201-1) = mergeSame [열] → 위아래로 같은 값이 이어지면 한 칸(rowSpan) · 본문만
* 앞 병합 열이 갈리면 뒤 병합 열도 갈림 · 빈 값 · 고칠 칸은 안 합침 · 칸 → 열 = `cellColumn`(병합 표는 data-c)
* 칸 램프(557-2) = cellClass 가 `ui-data-table__lamp` → 칸 글 앞 점 8 · 노랑 = 없음 · `is-linked` = 초록 ✓(색만으로 안 가름) ·
* `ui-data-table__lamp--only` = 글 숨기고 점만 가운데 · 대신하는 옛 변종 = B08 `b08-summary__lamp` · `b08-milestone__lamp` · B09 `b09-bill__lamp`
* ========================================================================== */
import "./ui_template_data_table.css";
@@ -33,6 +33,28 @@
gap: var(--spacing-24);
}
/* 좁은 폭 · 입력 칸 2열 · 끝 단추 줄(557-2) — 입력 칸 두 줄짜리 양식(D01 · E01) */
.ui-general-layout--narrow .ui-general-layout__inner {
max-width: 720px;
}
.ui-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--spacing-16);
}
@media (max-width: 640px) {
.ui-form-grid {
grid-template-columns: 1fr;
}
}
.ui-general-block__actions--end {
justify-content: flex-end;
margin-top: var(--spacing-16);
}
/* 일반 화면 틀(552-4) — 대시보드 세부 페이지 꼴(제목 20 · 설명 13 = 대시보드 값) */
.ui-page-frame {
display: flex;
@@ -166,6 +188,7 @@ html.is-bm-route .ui-general-block__section {
/* 숨김 — display 가 [hidden] 을 덮지 않게(공용 기본 꼴이 안 실린 곳 · 페이지 꾸밈 위에서도) */
:is(
.ui-btn-group,
.ui-form-grid,
.ui-general-block__actions,
.ui-general-block__section-header,
.ui-general-layout__content,
+14 -2
View File
@@ -2,17 +2,20 @@
* ui_template_general_layout.ts
* 일반 화면 틀 `page-frame`(552-4 틀) — 프로젝트 밖 화면(대시보드 세부 · 설정 · B02 · 결제) 몸통: 페이지 머리 + 몸.
* 꼴 = createPageFrame — 머리(제목 20 · 설명 13 · 오른쪽 주 단추 자리) + 몸(세로 쌓음) · 대시보드 세부 페이지 꼴
* createGeneralLayout — 바탕 · 가운데 폭(--page-max-width) · 큰 제목(옛 꼴 그대로)
* createGeneralLayout — 바탕 · 가운데 폭(--page-max-width) · 큰 제목(옛 꼴 그대로) · 좁은 폭(`narrow` · 720 · 557-2)
* createFormGrid — 입력 칸 2열 배치(좁은 화면 1열 · 사이 16 · 557-2) · 끝 단추 줄 = `ui-general-block__actions ui-general-block__actions--end`(오른쪽 · 위 16)
* 크기 = 폭 1120(대시보드 세부) · 머리 · 몸 사이 16 · 몸 안 사이 12
* 상태 = 없음
* 쓰는 법 = const f = createPageFrame(host, "구성원", "회사 사람 · 역할"); f.setAction(addBtn); f.body.append(table)
* 대신하는 옛 변종 = B01 `pageFrame`(`.b01-pg__head`) · `.b01-proj-head` · M01 `m01-head` · `.ui-wf`(안 씀)
* 대신하는 옛 변종 = B01 `pageFrame`(`.b01-pg__head`) · `.b01-proj-head` · M01 `m01-head` · `.ui-wf`(안 씀) · D01 · E01 이 빌려 쓰던 B02 `b02-proj` · `b02-proj__grid` · `b02-proj__actions`
* ========================================================================== */
import "./ui_template_general_layout.css";
export interface GeneralLayoutOptions {
pageClass?: string;
/** 좁은 폭 720 — 입력 칸 두 줄짜리 양식 */
narrow?: boolean;
title?: string;
subtitle?: string;
content: HTMLElement | readonly HTMLElement[];
@@ -27,6 +30,7 @@ export function createGeneralLayout(options: GeneralLayoutOptions): GeneralLayou
const root = document.createElement("div");
root.className = "ui-general-layout";
if (options.pageClass) root.classList.add(options.pageClass);
if (options.narrow) root.classList.add("ui-general-layout--narrow");
const inner = document.createElement("div");
inner.className = "ui-general-layout__inner";
@@ -55,6 +59,14 @@ export function createGeneralLayout(options: GeneralLayoutOptions): GeneralLayou
return { root, content };
}
/** 입력 칸 2열 배치(557-2) — 좁은 화면(640 아래) 1열 */
export function createFormGrid(fields: readonly HTMLElement[]): HTMLElement {
const grid = document.createElement("div");
grid.className = "ui-form-grid";
grid.append(...fields);
return grid;
}
export interface PageFrame {
root: HTMLElement;
body: HTMLElement;
+29 -1
View File
@@ -31,6 +31,34 @@
font-size: 18px;
font-weight: 700;
}
/* 규칙 꼴(557-2) — 왼쪽 15em · 설명 왼쪽 맞춤 · 줄마다 아래 선 · 값 없는 줄은 이름이 통째 */
.ui-kv--rule {
margin-bottom: var(--spacing-8);
}
.ui-kv--rule .ui-kv__row {
display: grid;
grid-template-columns: minmax(0, 15em) 1fr;
align-items: center;
gap: var(--spacing-12);
padding: var(--spacing-4) 0;
border-top: 0;
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
.ui-kv--rule .ui-kv__label {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-4);
color: inherit;
font-size: inherit;
}
.ui-kv--rule .ui-kv__label:only-child {
grid-column: 1 / -1;
}
.ui-kv--rule .ui-kv__value {
font-size: inherit;
text-align: left;
}
.ui-kv__mark {
margin-right: var(--spacing-4);
font-weight: 700;
@@ -43,6 +71,6 @@
}
/* 숨김 — display 가 [hidden] 을 덮지 않게(공용 기본 꼴이 안 실린 곳 · 페이지 꾸밈 위에서도) */
:is(.ui-kv, .ui-kv__row)[hidden] {
:is(.ui-kv, .ui-kv__row, .ui-kv__label)[hidden] {
display: none !important;
}
+14 -7
View File
@@ -2,27 +2,34 @@
* ui_template_kv.ts
* 부품 「값 목록」(552-3 · 목록 29) — 「이름 : 값」 줄 여럿(dl).
* 꼴 = 보통 줄 · 판정 줄(`ok: true` = ✓ · `false` = ✗ — 색만으로 안 가름) · 큰 값 줄(`big`)
* · 규칙 꼴(`createKv(rows, { rule: true })` · 557-2) = 왼쪽 15em 칸(키 칩 등 요소) · 오른쪽 설명 글 왼쪽 맞춤 · 값 없는 줄(`value` 안 줌)은 이름이 줄 통째
* 크기 = 줄 24 · 글 12(이름) / 14(값) · 큰 값 18
* 상태 = 기본만
* 쓰는 법 = `createKv([{ label: "연장", value: "1,080 m" }, { label: "종단 경사", value: "12 %", ok: true }])`
* 대신하는 옛 변종 = B04 `b04-surface__line` · B07 `.b07-info__row` · CAD `.cad-properties dl` · E04 `e04-result__list` · E07 `e07-summary` · B08 `b08-hydro__row`
* 대신하는 옛 변종 = B04 `b04-surface__line` · B07 `.b07-info__row` · CAD `.cad-properties dl` · E04 `e04-result__list` · E07 `e07-summary` · ☰ 설정 `app-settings__rules`
* ========================================================================== */
import { el } from "./ui_template_elements_base";
import "./ui_template_kv.css";
export interface KvRow {
label: string;
/** 없으면 「-」 */
value: string | number | Node | null | undefined;
label: string | Node;
/** 없으면 「-」 · 규칙 꼴에서 안 주면 이름이 줄 통째 */
value?: string | number | Node | null;
/** 판정 — true ✓ · false ✗ */
ok?: boolean;
big?: boolean;
}
export function createKv(rows: KvRow[]): HTMLDListElement {
const list = el("dl", { className: "ui-kv" });
export function createKv(rows: KvRow[], opts: { rule?: boolean } = {}): HTMLDListElement {
const list = el("dl", { className: `ui-kv${opts.rule ? " ui-kv--rule" : ""}` });
for (const row of rows) {
const label = el("dt", { className: "ui-kv__label" });
label.append(row.label);
if (opts.rule && row.value === undefined) {
list.append(el("div", { className: "ui-kv__row", children: [label] }));
continue;
}
const value = el("dd", { className: "ui-kv__value" });
const v = row.value;
value.append(
@@ -39,7 +46,7 @@ export function createKv(rows: KvRow[]): HTMLDListElement {
list.append(
el("div", {
className: `ui-kv__row${row.big ? " ui-kv__row--big" : ""}`,
children: [el("dt", { className: "ui-kv__label", text: row.label }), value],
children: [label, value],
}),
);
}
+6
View File
@@ -88,6 +88,12 @@
.ui-modal[data-width="960px"] {
width: min(960px, 92vw);
}
/* 넓은 중간 창(557-2) — wide 꼴 + 1100 × 680 · 몸 안 칸이 따로 스크롤 */
.ui-modal.ui-modal--wide[data-width="1100px"] {
--modal-max: min(680px, 86vh);
width: min(1100px, 94vw);
height: var(--modal-max);
}
.ui-confirm__title {
margin: 0 0 var(--spacing-8);
font-size: var(--text-body);
+3 -2
View File
@@ -2,6 +2,7 @@
* ui_template_modal.ts
* 부품 「모달 · 확인창」(552-3 · 목록 22) — 화면을 가리고 위에 뜨는 창.
* 꼴 = 모달(`openModal` · 제목줄 · 닫기 · 몸은 `mount`) · 넓음(`wide` · 96vw × 94vh) · 폭 단(`width`: 24rem · 40rem · 960px · 기본 720)
* · 넓은 중간 창(`wide` + `width: "1100px"` · 1100 × 680 · 몸 넘침만 스크롤)
* · 확인창(`askConfirm`) · 위험 확인(`danger` · 확인 단추 빨강 = `showConfirmDialog`) · 글 묻기(`askText` · window.prompt 대신)
* 크기 = 모달 여백 16 · 둥글기 12 · 확인창 폭 300 ~ 420 · 단추 = 단추 부품 기본
* 상태 = 기본만 · Esc = 닫기(한글 조합 중 · 몸이 막은 Esc 는 안 닫음) · 바깥 누르기 = 누름 · 뗌 둘 다 배경일 때만
@@ -26,8 +27,8 @@ export interface ModalOptions {
actions?: HTMLElement[];
/** 큰 창 — 96vw × 94vh · 몸 넘침만 스크롤 */
wide?: boolean;
/** 폭 단 — 없으면 720 */
width?: "24rem" | "40rem" | "960px";
/** 폭 단 — 없으면 720 · 1100px 은 `wide` 와 함께(높이 680) */
width?: "24rem" | "40rem" | "960px" | "1100px";
backdropClass?: string;
/** 닫기 단추 · Esc · 바깥 누르기 전에 물음 — false 면 안 닫음(몸의 `close` 는 묻지 않고 닫음) */
beforeClose?: () => boolean | Promise<boolean>;
+1 -1
View File
@@ -6,7 +6,7 @@
* 좁은 꼴 = `settingRow(제목, 설명, 오른쪽, { compact: true })` — 좁은 왼쪽 패널용 · 줄 최소 28 · 글 12 · 아래 선 없음 · 줄 통째가 label(`data-size="compact"`)
* 상태 = 기본만(조작 부품이 저마다 상태를 가짐)
* 쓰는 법 = `settingCard([settingRow("회사 이름", "보고서 머리에 들어감", valueText("산림설계"))])`
* 대신하는 옛 변종 = B01 `settingRow` · `settingCard`(`.b01-pg-row` · `.b01-pg-settings`) · `.b01-dashboard__setting-card` · E02 `e02-field` · E06 `e06-setting` · B08 `b08-hydro__row`
* 대신하는 옛 변종 = B01 `settingRow` · `settingCard`(`.b01-pg-row` · `.b01-pg-settings`) · `.b01-dashboard__setting-card` · E02 `e02-field` · E06 `e06-setting`
* ========================================================================== */
import { el } from "./ui_template_elements_base";
+1 -1
View File
@@ -38,7 +38,7 @@
--color-success: #1f8a4c; /* 성공/승인 */
--color-warning: #b45309; /* 경고 */
--color-danger: #c0392b; /* 에러/삭제 글자 · 선 */
--color-danger-fill: #e5484d; /* 위험 채운 바탕(알림 점 · 배지) */
--color-danger-fill: #d6363c; /* 위험 채운 바탕(알림 점 · 배지) · 흰 글 대비 4.5 이상 */
--color-info: #2e6fb5;
/* 패널 접기/펼치기 손잡이 테두리 — 강조색 */
--panel-handle-border: var(--color-accent);
+15 -1
View File
@@ -7,7 +7,8 @@
import { createIconButton } from "./ui_template_button";
import { createButton } from "./ui_template_elements";
import { createFloatPanel } from "./ui_template_float_panel";
import { createPageFrame } from "./ui_template_general_layout";
import { createFormGrid, createPageFrame } from "./ui_template_general_layout";
import { createInputField } from "./ui_template_input";
import { createListRow } from "./ui_template_list_row";
import { createWorkflowPanelHandle } from "./ui_template_overlay";
import { createPanelHead } from "./ui_template_panel_head";
@@ -93,6 +94,19 @@ function pageFrame(): HTMLElement {
const f = createPageFrame(null, "구성원", "회사 사람 · 역할 · 초대");
f.setAction(createButton({ label: "초대", onClick: noop }));
f.body.append(div("ui-general-block__empty", "몸 — 표 · 카드가 들어감"));
// 입력 칸 2열 · 끝 단추 줄(557-2 · D01 · E01 등록 양식)
const actions = div("ui-general-block__actions ui-general-block__actions--end");
actions.append(
createButton({ label: "취소", variant: "ghost", onClick: noop }),
createButton({ label: "등록", onClick: noop }),
);
f.body.append(
createFormGrid([
createInputField({ label: "프로젝트 이름" }).root,
createInputField({ label: "지역" }).root,
]),
actions,
);
return f.root;
}
+65 -10
View File
@@ -37,7 +37,9 @@ import { buildModal, confirmPanel } from "./ui_template_modal";
import { settingCard, settingRow, valueText } from "./ui_template_setting_row";
import { createTable, type Cell, type Column } from "./ui_template_table";
import { createCard } from "./ui_template_card";
import { createPanelCard } from "./ui_template_collapsible";
import { createCollapsibleHead, createPanelCard } from "./ui_template_collapsible";
import { createCalcFormula, createCalcFormulaRow, createCalcRow } from "./ui_template_calc_row";
import { createDataTable } from "./ui_template_data_table";
import { createBarChart, createDonut, createLineChart } from "./ui_template_elements_chart";
import { createEmpty } from "./ui_template_empty";
import { createLegend } from "./ui_template_flow_legend";
@@ -147,6 +149,56 @@ const col = (...children: HTMLElement[]): HTMLElement => {
box.style.alignItems = "stretch";
return box;
};
/** 557-2 견본 — 접이 구역 머리 · 칸 램프 표 · 규칙 꼴 키 칩 · 계산 줄 */
const kitSection = (): HTMLElement => {
const box = el("section", { className: "ui-collapsible" });
box.append(
createCollapsibleHead({ title: "조합 연결", sub: "연결됨 1 / 4", level: 4 }).root,
createText("배관 · 본체 미연결"),
);
return box;
};
const kitLampTable = (): HTMLElement =>
createDataTable({
columns: [
{ key: "st", label: "측점" },
{ key: "c", label: "단가연결" },
],
rows: [
{ st: "No.1", c: "단가연결" },
{ st: "No.2", c: "연결됨" },
],
cellClass: (i, key) =>
key === "st"
? `ui-data-table__lamp${i ? " is-linked" : ""}`
: `ui-data-table__lamp ui-data-table__lamp--only${i ? " is-linked" : ""}`,
}).root;
const kitKeys = (...keys: string[]): DocumentFragment => {
const f = document.createDocumentFragment();
f.append(...keys.map((k) => createBadge(k, "neutral")));
return f;
};
const kitCalc = (): HTMLElement => {
const input = (v: string): HTMLElement => {
const f = createInputField({ type: "number", compact: true });
f.input.value = v;
f.input.classList.add("ui-calc-row__in");
return f.input;
};
const out = (v: string, key = false): HTMLElement =>
el("span", { className: `ui-calc-row__out${key ? " is-key" : ""}`, text: v });
return col(
createCalcFormula("Q = 1/3.6 × f × rt × A"),
createCalcRow("f : 유출계수", input("0.8")),
createCalcRow("A : 유역면적 (㎢)", input("0.46"), out("46.00 ha")),
createCalcFormulaRow("경심", createCalcFormula("R = A / P"), out("0.250")),
createCalcFormulaRow(
"유속 (m/s)",
createCalcFormula("V = 1/n × R^2/3^ × S^1/2^"),
out("2.41", true),
),
);
};
/** 552-2 견본 조각 — 상태를 안쪽 요소에 굳힘 */
const OPTS = [
{ value: "a", text: "임도" },
@@ -257,31 +309,34 @@ export const KIT_PARTS: KitPart[] = [
{ key: "card", name: "카드", group: "compound", kinds: "선 카드 · 머리 카드 · 고르는 카드 · 여백 12 · 16", file: "ui_template_card.ts",
demo: (s) => { const pick = (): HTMLElement => markState(createCard({ title: "새 노선", body: ["노선을 그려 시작"], pad: 12, onSelect: noop }), s);
return s === "error" ? null : s === "default" ? col(createCard({ title: "개요", body: ["임도 1 노선 · 1,080 m"], pad: 16 }), createCard({ title: "머리 카드", body: ["몸"], pad: 12, actions: [createButton({ label: "열기", variant: "ghost", size: "sm", onClick: noop })] }), createCard({ title: "고른 카드", body: ["골라짐"], pad: 12, onSelect: noop, selected: true })) : pick(); } },
{ key: "collapsible", name: "접이 카드", group: "compound", kinds: "패널 카드 · 구역 · 캐럿 ▾ ▸ · ⚙ 곁값", file: "ui_template_collapsible.ts",
{ key: "collapsible", name: "접이 카드", group: "compound", kinds: "패널 카드 · 구역 · 구역 머리(제목 · 부제) · 캐럿 ▾ ▸ · ⚙ 곁값", file: "ui_template_collapsible.ts",
demo: (s) => { if (s !== "default" && s !== "hover") return null; const open = createPanelCard("구조물 배치", false, noop); open.append(row(createInputField({ placeholder: "측점" }).root));
if (s === "hover") { markState(open.querySelector<HTMLElement>(".ui-collapsible__title")!, s); return open; } return col(open, createPanelCard("접힌 카드")); } },
{ key: "table", name: "표", group: "compound", kinds: "일반표(줄 40) · 데이터표(줄 28) · 격자표(세로선 · 합친 칸) · ⋯ 칸 · 한글 배지 · 빈 줄 · 줄 고름 · 바뀜 · 지움", file: "ui_template_table.ts",
if (s === "hover") { markState(open.querySelector<HTMLElement>(".ui-collapsible__title")!, s); return open; } return col(open, createPanelCard("접힌 카드"), kitSection()); } },
{ key: "table", name: "표", group: "compound", kinds: "일반표(줄 40) · 데이터표(줄 28) · 격자표(세로선 · 합친 칸) · ⋯ 칸 · 한글 배지 · 빈 줄 · 줄 고름 · 바뀜 · 지움 · 칸 램프(노랑 점 · ✓)", file: "ui_template_table.ts",
demo: (s) => { if (s === "disabled" || s === "error") return null; const t = s === "default" ? createTable(KIT_COLS, kitRows()) : createTable(KIT_DATA, dataRows, { density: "data" });
const first = t.querySelector<HTMLElement>("tbody tr")!; if (s === "hover") markState(first, s); if (s === "active") first.classList.add("is-active");
if (s !== "default") return t; const data = createTable(KIT_DATA, dataRows, { density: "data" }); const rows = data.querySelectorAll("tbody tr"); rows[0].classList.add("is-changed"); rows[1].classList.add("is-removed");
return col(t, data, createTable(KIT_DATA, [], { empty: "노선 없음" }), createTable([], gridRows(), { density: "data", grid: true })); } },
return col(t, data, createTable(KIT_DATA, [], { empty: "노선 없음" }), createTable([], gridRows(), { density: "data", grid: true }), kitLampTable()); } },
{ key: "row-menu", name: "⋯ · 오른쪽 메뉴", group: "compound", kinds: "줄 끝 ⋯(28) · 펼친 판 · 위험 항목(빨강 + 확인창) · 막힌 항목", file: "ui_template_context_menu.ts",
demo: (s) => { if (s === "error") return null; if (s === "default") return createRowMenu(KIT_MENU); if (s === "hover") { const m = createRowMenu(KIT_MENU); markState(m.querySelector<HTMLElement>(".ui-row-menu__btn")!, s); return m; }
return openMenu(s, s === "disabled" ? [{ label: "내보내기", onClick: noop, disabled: true }, ...KIT_MENU] : KIT_MENU); } },
{ key: "empty", name: "빈 상태", group: "compound", kinds: "선 아이콘 · 한 줄 · 주 단추 · 사용법 · 작은 꼴", file: "ui_template_empty.ts",
demo: (s) => (s === "default" ? col(createEmpty({ icon: "folder", text: "아직 프로젝트가 없음", hint: "새 프로젝트를 만들어 시작", action: createButton({ label: "새 프로젝트", onClick: noop }), onGuide: noop }), createEmpty({ text: "고른 측점 없음", small: true })) : null) },
{ key: "modal", name: "모달 · 확인창", group: "compound", kinds: "보통(720) · 넓음 · 폭 단 24rem · 40rem · 960 · 긴 제목 = 한 줄 말줄임 · 확인 · 위험 확인 · 글 묻기(prompt 대신)", file: "ui_template_modal.ts",
{ key: "modal", name: "모달 · 확인창", group: "compound", kinds: "보통(720) · 넓음 · 폭 단 24rem · 40rem · 960 · 넓은 중간 1100 × 680 · 긴 제목 = 한 줄 말줄임 · 확인 · 위험 확인 · 글 묻기(prompt 대신)", file: "ui_template_modal.ts",
demo: (s) => { if (s !== "default") return null; const m = buildModal({ title: "구조물 고르기", closeLabel: "닫기", width: "40rem" }, noop); m.body.append("몸 — 표 · 양식이 들어감"); m.dialog.style.margin = "0";
const long = buildModal({ title: "배수관 · 횡배수관 · 측구 · 집수정 · 옹벽 구조물을 한꺼번에 고르기", closeLabel: "닫기", width: "24rem" }, noop); long.body.append("[닫기] 는 한 줄 · 제목은 말줄임"); long.dialog.style.margin = "0";
return col(m.dialog, long.dialog, confirmPanel({ message: "노선을 저장할까요?" }).panel, confirmPanel({ title: "삭제", message: "임도 2 노선을 지울까요? 되돌릴 수 없음", okLabel: "삭제", danger: true }).panel, confirmPanel({ message: "블록 이름", field: { placeholder: "예: 배수관" } }).panel); } },
const mid = buildModal({ title: "운반 단가 — 넓은 중간 창", closeLabel: "닫기", wide: true, width: "1100px" }, noop); mid.body.append("몸 — 두 칸이 따로 스크롤"); mid.dialog.style.cssText = "margin:0;max-width:100%;height:160px";
return col(m.dialog, long.dialog, mid.dialog, confirmPanel({ message: "노선을 저장할까요?" }).panel, confirmPanel({ title: "삭제", message: "임도 2 노선을 지울까요? 되돌릴 수 없음", okLabel: "삭제", danger: true }).panel, confirmPanel({ message: "블록 이름", field: { placeholder: "예: 배수관" } }).panel); } },
{ key: "toast", name: "알림", group: "compound", kinds: "토스트 4꼴(성공 · 오류 · 정보 · 경고) · 로딩 가림막 · 진행 가림막", file: "ui_template_toast.ts",
demo: (s) => (s === "default" ? col(toastNode("저장됨", "success"), toastNode("저장 못 함 — 다시 시도", "error"), toastNode("노선을 불러옴", "info"), toastNode("측점 간격이 넓음", "warning")) : null) },
{ key: "banner", name: "띠 알림", group: "compound", kinds: "왼쪽 띠 · 정보 · 경고 · 위험 · 성공(✓) · 펼침 · 안 단추 · 닫기", file: "ui_template_banner.ts",
demo: (s) => (s === "default" ? col(createBanner({ title: "안내", text: "초기값에서 시작함" }), createBanner({ tone: "warning", text: "저장 안 한 고침이 있음", actions: [createButton({ label: "저장", size: "sm", onClick: noop })] }), createBanner({ tone: "danger", text: "횡단 3 곳 계산 못 함", details: "No.12 · No.15 · No.20" }), createBanner({ tone: "success", text: "검토 끝", onClose: noop })) : null) },
{ key: "stat", name: "숫자 카드", group: "compound", kinds: "값 · 단위 · 변화 · 게이지 · 판정", file: "ui_template_stat.ts",
demo: (s) => (s === "default" ? statGrid([createStat({ label: "진행 중", value: 12, unit: "건", delta: "+3" }), createStat({ label: "디스크", value: "71 %", gauge: 0.71, tone: "warn" }), createStat({ label: "오류", value: 2, delta: "-1", tone: "bad" }), createStat({ label: "회사", value: 8, compact: true })]) : null) },
{ key: "kv", name: "값 목록", group: "compound", kinds: "이름 : 값 줄 · 판정(✓ ✗) · 큰 값", file: "ui_template_kv.ts",
demo: (s) => (s === "default" ? createKv([{ label: "연장", value: "1,080 m", big: true }, { label: "종단 경사", value: "12 %", ok: true }, { label: "최소 곡선반경", value: "12 m", ok: false }, { label: "비고", value: null }]) : null) },
{ key: "kv", name: "값 목록", group: "compound", kinds: "이름 : 값 줄 · 판정(✓ ✗) · 큰 값 · 규칙 꼴(키 칩 = 설명)", file: "ui_template_kv.ts",
demo: (s) => (s === "default" ? col(createKv([{ label: "연장", value: "1,080 m", big: true }, { label: "종단 경사", value: "12 %", ok: true }, { label: "최소 곡선반경", value: "12 m", ok: false }, { label: "비고", value: null }]), createKv([{ label: kitKeys("Ctrl", "Z"), value: "되돌리기" }, { label: kitKeys("휠 끌기"), value: "화면 옮기기" }, { label: kitKeys("Esc") }], { rule: true })) : null) },
{ key: "calc-row", name: "계산 줄", group: "compound", kinds: "값 줄(이름 · 값 112 · 덧칸) · 식 줄(이름 · 식 · 값) · 위 · 아래첨자 · 중요 결과 굵게", file: "ui_template_calc_row.ts",
demo: (s) => (s === "default" ? kitCalc() : null) },
{ key: "progress", name: "진행 막대", group: "compound", kinds: "막대 6 · 8 · 쌓임 막대 · 원 · 오류", file: "ui_template_progress.ts",
demo: (s) => { if (s === "error") return markState(createProgressBar({ value: 0.6, label: "실패" }).root, s); if (s !== "default") return null; const ring = createProgressCircle({ size: 48 }); ring.set(0.4);
return col(createProgressBar({ value: 0.4, label: "40 %" }).root, createProgressBar({ value: 0.7, thick: 6 }).root, createProgressBar({ parts: [{ value: 2, tone: "soft" }, { value: 3 }, { value: 1, tone: "warn" }], label: "6 건" }).root, ring.root); } },
@@ -298,7 +353,7 @@ export const KIT_PARTS: KitPart[] = [
// ── 552-4 틀 ───────────────────────────────────────────────
{ key: "work-frame", name: "작업 화면 틀", group: "frame", kinds: "왼쪽 패널(흰 판 · 1 px 선) · 본문 · 아래 단추 줄 · [?][<][>] · 머리 줄 없음 · ☰ 서랍", file: "ui_template_overlay.ts",
demo: FRAME_DEMOS["work-frame"] },
{ key: "page-frame", name: "일반 화면 틀", group: "frame", kinds: "페이지 머리(제목 · 설명 · 주 단추) · 몸통", file: "ui_template_general_layout.ts",
{ key: "page-frame", name: "일반 화면 틀", group: "frame", kinds: "페이지 머리(제목 · 설명 · 주 단추) · 몸통 · 좁은 폭 720 · 입력 칸 2열 · 끝 단추 줄", file: "ui_template_general_layout.ts",
demo: FRAME_DEMOS["page-frame"] },
{ key: "public-frame", name: "공개 화면 틀", group: "frame", kinds: "머리 띠(제목 · 설명 · 주 단추) · 구역 · 본문", file: "ui_template_public_layout.ts",
demo: FRAME_DEMOS["public-frame"] },