Merge remote-tracking branches 'origin/sub_laptop_5' and 'origin/sub_laptop_6' into sub_laptop_2

This commit is contained in:
2026-10-26 22:07:17 +09:00
8 changed files with 60 additions and 111 deletions
+29 -30
View File
@@ -25,6 +25,7 @@ import {
import { createBanner } from "@ui/ui_template_banner";
import { createCheckbox } from "@ui/ui_template_checkbox";
import { createPanelCard } from "@ui/ui_template_collapsible";
import { tableRow, tableShell, type Column } from "@ui/ui_template_table";
import { t } from "@ui/ui_template_locale";
import { stationLabel } from "../common_util/common_util_station";
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
@@ -212,42 +213,40 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise<void> {
function draw(): void {
const list = current();
const head = el("tr", {
children: [
"D04_Col_Year",
"D04_Col_Length",
"D04_Col_Start",
"D04_Col_End",
"D04_Col_Chainage",
"D04_Col_Confirmed",
"D04_Col_Assignees",
].map((key) => el("th", { text: t(key as Parameters<typeof t>[0]) })),
});
const rows = summary.years.map((row) => {
const columns: Column[] = [
"D04_Col_Year",
"D04_Col_Length",
"D04_Col_Start",
"D04_Col_End",
"D04_Col_Chainage",
"D04_Col_Confirmed",
"D04_Col_Assignees",
].map((key, i) => ({
label: t(key as Parameters<typeof t>[0]),
...(i === 1 ? { align: "end" as const } : {}),
}));
const { root: tableRoot, tbody } = tableShell(columns);
tableRoot.querySelector("table")?.classList.add("d04-confirm__table");
for (const row of summary.years) {
const firms = list.filter((item) => item.year === row.year).map(firmChip);
const chain = row.saved
? `${station(row.start_chainage_m)} ~ ${station(row.end_chainage_m)}`
: t("D04_NotSaved");
return el("tr", {
attrs: { "data-year": String(row.year) },
children: [
el("td", { text: String(row.year) }),
el("td", { text: fmt(row.length_m), attrs: { "data-num": "" } }),
el("td", { text: where(row.start) }),
el("td", { text: where(row.end) }),
el("td", { text: chain }),
el("td", { text: kstText(row.confirmed_at) }),
el("td", { children: editable ? [...firms, addForm(row.year)] : firms }),
],
});
});
const table = el("table", {
className: "d04-confirm__table",
children: [el("thead", { children: [head] }), el("tbody", { children: rows })],
});
const tr = tableRow(columns, [
String(row.year),
fmt(row.length_m),
where(row.start),
where(row.end),
chain,
kstText(row.confirmed_at),
el("div", { children: editable ? [...firms, addForm(row.year)] : firms }),
]);
tr.dataset.year = String(row.year);
tbody.append(tr);
}
const notes = draft() ? [createBanner({ tone: "warning", text: t("D04_Dirty") })] : [];
notes[0]?.classList.add("d04-confirm__dirty");
main.replaceChildren(el("h2", { text: summary.name }), ...notes, table);
main.replaceChildren(el("h2", { text: summary.name }), ...notes, tableRoot);
pickList.replaceChildren(
...summary.years
@@ -10,29 +10,6 @@
overflow: auto;
}
.d04-confirm__table {
border-collapse: collapse;
font-size: var(--text-body-sm);
color: var(--color-text-body);
}
.d04-confirm__table th,
.d04-confirm__table td {
padding: var(--spacing-8);
border: 1px solid var(--color-border);
text-align: left;
vertical-align: top;
}
.d04-confirm__table thead th {
background: var(--color-surface);
}
.d04-confirm__table td[data-num] {
text-align: right;
font-variant-numeric: tabular-nums;
}
.d04-confirm__firm,
.d04-confirm__add {
display: flex;
+1 -36
View File
@@ -41,38 +41,11 @@
gap: var(--spacing-4);
}
.e02-table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-body-sm);
color: var(--color-text-body);
}
.e02-table th {
padding: var(--spacing-4);
border-bottom: 1px solid var(--color-border);
color: var(--color-text-secondary);
font-weight: var(--font-weight-medium);
text-align: left;
white-space: nowrap;
}
.e02-table td {
padding: 2px var(--spacing-4);
vertical-align: middle;
}
/* 표 모양은 표 부품 — 여기는 읽기 표 줄 안 줄바꿈만 */
.e02-table--read td {
padding: 2px var(--spacing-8);
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
.e02-table--read tr.is-zoned td {
background: var(--color-mist-violet);
color: var(--color-text);
}
.e02-stations {
max-height: 320px;
overflow: auto;
@@ -112,14 +85,6 @@
font-size: var(--text-body-sm);
}
.e02-field {
display: flex;
flex-direction: column;
gap: 2px;
font-size: var(--text-caption);
color: var(--color-text-secondary);
}
.e02-status {
margin: var(--spacing-8) 0 0;
white-space: pre-line;
+20 -17
View File
@@ -15,6 +15,7 @@ import {
} from "@ui/ui_template_elements";
import { createCard } from "@ui/ui_template_card";
import { createCheckbox } from "@ui/ui_template_checkbox";
import { createTable } from "@ui/ui_template_table";
import { createText } from "@ui/ui_template_text";
import { t, type LocaleKey } from "@ui/ui_template_locale";
import { inferStationInterval, stationLabel } from "../common_util/common_util_station";
@@ -119,16 +120,15 @@ export function headButtons(
head.append(...buttons);
}
export function table(headers: string[], rows: HTMLElement[][]): HTMLTableElement {
const node = el("table", { className: "e02-table" }) as HTMLTableElement;
const tr = node.createTHead().insertRow();
for (const text of headers) tr.append(el("th", { text }));
const body = node.createTBody();
for (const cells of rows) {
const row = body.insertRow();
for (const cell of cells) row.insertCell().append(cell);
}
return node;
/** 표 부품(데이터표) — 머리 글 "" = 줄 끝 단추 칸 · 돌려주는 것 = 표 감싸개(속 `table.e02-table`) */
export function table(headers: string[], rows: HTMLElement[][]): HTMLElement {
const wrap = createTable(
headers.map((label) => ({ label })),
rows,
{ density: "data" },
);
wrap.querySelector("table")?.classList.add("e02-table");
return wrap;
}
function defaultMaterial(view: E02View, id: string): Material | undefined {
@@ -200,9 +200,9 @@ function materialsCard(view: E02View): HTMLElement {
const mapRow = el("div", { className: "e02-map" });
mapRow.append(el("span", { className: "e02-map__title", text: t("E02_LabelMap") }));
for (const label of LABELS) {
const field = el("label", { className: "e02-field" });
const field = el("label", { className: "ui-field e02-field" });
field.append(
el("span", { text: L(`E02_Label_${label}`) }),
el("span", { className: "ui-field__label", text: L(`E02_Label_${label}`) }),
selectInput(view, `label_map.${label}`, layerOptions(g), g.label_map[label], (v) => {
g.label_map[label] = v;
view.rebuild();
@@ -327,8 +327,11 @@ function zonesCard(view: E02View): { section: HTMLElement; refresh: () => void }
el("span", { text: fmt(r.soil_thickness_m) }),
]),
);
resolved.forEach((r, i) => node.tBodies[0].rows[i].classList.toggle("is-zoned", r.zoned));
node.classList.add("e02-table--read");
node.querySelectorAll("tbody tr").forEach((tr, i) => {
tr.classList.toggle("is-zoned", resolved[i].zoned);
tr.classList.toggle("is-active", resolved[i].zoned);
});
node.querySelector("table")?.classList.add("e02-table--read");
stationsHost.append(node);
};
refresh();
@@ -393,7 +396,7 @@ function safetyCard(view: E02View): HTMLElement {
]);
const { section, head } = card(
"E02_Card_Safety",
table(["", ...SLOPES.map((s) => L(`E02_Slope_${s}`))], rows),
table(["\u00a0", ...SLOPES.map((s) => L(`E02_Slope_${s}`))], rows),
);
headButtons(
view,
@@ -416,8 +419,8 @@ function loadsCard(view: E02View): HTMLElement {
const g = view.ground;
const d = view.defaults;
const field = (label: string, input: HTMLElement): HTMLElement => {
const node = el("label", { className: "e02-field" });
node.append(el("span", { text: label }), input);
const node = el("label", { className: "ui-field e02-field" });
node.append(el("span", { className: "ui-field__label", text: label }), input);
return node;
};
const kh = numInput(view, "seismic.kh", g.seismic.kh, (v) => (g.seismic.kh = v as number), {
+2 -2
View File
@@ -23,8 +23,8 @@ import {
const L = (key: string): string => t(key as LocaleKey);
function field(label: string, input: HTMLElement): HTMLElement {
const node = el("label", { className: "e02-field" });
node.append(el("span", { text: label }), input);
const node = el("label", { className: "ui-field e02-field" });
node.append(el("span", { className: "ui-field__label", text: label }), input);
return node;
}
+5
View File
@@ -57,6 +57,8 @@ def test_d04_parts():
assert "input:checked" in ts # [확정] 이 고른 연도를 읽는 길
assert "border:" not in css.split(".d04-confirm__input")[1].split("}")[0]
assert ".d04-confirm__dirty" not in css
assert 'el("table"' not in ts and "tableShell(columns)" in ts
assert "d04-confirm__table" not in css # 표 모양은 표 부품 · 이름은 촬영 장면용 걸이
def test_e01_missing_is_banner_part():
@@ -77,3 +79,6 @@ def test_e02_parts():
for dead in (".e02-card__head", ".e02-card__title", ".e02-hint", ".e02-empty"):
assert dead not in css, dead
_no_hex("E02_StabGround/E02_StabGround_UI_Style.css")
assert "createTable(" in tables and "createTHead" not in tables
assert 'className: "ui-field e02-field"' in tables # 칸 이름 줄은 입력 부품의 ui-field
assert ".e02-field" not in css and "border-collapse" not in css
+2 -2
View File
@@ -236,9 +236,9 @@ def test_저장된_표준단면이_브라우저까지_간다():
# 값으로 재는 확인은 화면 실측에 있음(보조 창 b269ea34 81.77m — 단 0 → 1 에서 성토
# 10.9027 → 10.6743㎡). 계획서에 그 수치가 남아 있다.
def test_다단_성토부선이_면적_트림에_들어간다():
# 다단 · 묻힌 구간 거르기는 `_Culvert_Tiers` 로 나뉘어 있어 두 파일을 함께 읽음.
# 다단 · 묻힌 구간 거르기는 `logic/_Cross_Culvert_Tiers` 로 나뉘어 있어 두 파일을 함께 읽음.
source = "".join(
(PROJECT_ROOT / "B06_Section" / f"B06_Section_UI_Cross_Culvert_{name}.ts").read_text(
(PROJECT_ROOT / "B06_Section" / "logic" / f"B06_Section_Cross_Culvert_{name}.ts").read_text(
encoding="utf-8"
)
for name in ("Geom", "Tiers")
+1 -1
View File
@@ -220,7 +220,7 @@ export const KIT_PARTS: KitPart[] = [
demo: (s) => (s === "error" ? null : row(...([["저장", "filled"], ["취소", "ghost"], ["보기", "pill"], ["삭제", "danger"], ["초기화", "danger-ghost"], ["사용법", "link"]] as const).map(([label, variant]) => markState(createButton({ label, variant, size: 28, onClick: noop }), s)))),
sizes: (z) => createButton({ label: String(z), size: z, iconStart: "plus", onClick: noop }) },
{ key: "icon-button", name: "아이콘 단추", group: "basic", kinds: "고스트 · 선 · 채움 · 켜짐(aria-pressed) · 글자 기호 · 여덟 단 · title 필수", file: "ui_template_button.ts",
demo: (s) => (s === "error" ? null : row(markState(createIconButton({ icon: "settings", title: "설정" }), s), markState(createIconButton({ icon: "refresh", title: "새로 고침", variant: "outline" }), s), markState(createIconButton({ icon: "plus", title: "더하기", variant: "filled" }), s), markState(createIconButton({ icon: "eye", title: "보기", pressed: true }), s), markState(createIconButton({ glyph: "▲", title: "위로", variant: "outline" }), s))),
demo: (s) => (s === "error" ? null : row(markState(createIconButton({ icon: "settings", title: "설정" }), s), markState(createIconButton({ icon: "refresh", title: "새로 고침", variant: "outline" }), s), markState(createIconButton({ icon: "plus", title: "추가", variant: "filled" }), s), markState(createIconButton({ icon: "eye", title: "보기", pressed: true }), s), markState(createIconButton({ glyph: "▲", title: "위로", variant: "outline" }), s))),
sizes: (z) => createIconButton({ icon: "settings", title: `설정 ${z}`, size: z, variant: "outline" }) },
{ key: "input", name: "입력", group: "basic", kinds: "글 · 수 · 단위 붙음 · 찾기 · 날짜 · 여러 줄 · 여덟 단", file: "ui_template_input.ts",
demo: (s) => fieldDemo(s, () => { const f = createInputField({ placeholder: "폭", type: "number", unit: "m", size: 32 }); return { root: f.root, node: f.input, setError: f.setError }; }),