From 271c46064142d5c1cd0b15672ed7a8ff0d86a75d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 27 Oct 2026 10:53:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(=EB=B6=80=ED=92=88=C2=B7=ED=85=8C=EB=A7=88)?= =?UTF-8?q?:=20=EC=9C=84=ED=97=98=20=EC=B1=84=EC=9B=80=20=EB=8C=80?= =?UTF-8?q?=EB=B9=84=204.72=20=C2=B7=20=EB=82=A8=EC=9D=80=20=EC=98=9B=20?= =?UTF-8?q?=EA=BC=B4=20=E2=86=92=20=EB=B6=80=ED=92=88(=EA=B3=84=EC=82=B0?= =?UTF-8?q?=20=EC=A4=84=20=C2=B7=20=EC=B9=B8=20=EB=9E=A8=ED=94=84=20=C2=B7?= =?UTF-8?q?=20=EB=84=93=EC=9D=80=20=EC=A4=91=EA=B0=84=20=EC=B0=BD=20=C2=B7?= =?UTF-8?q?=20=EA=B7=9C=EC=B9=99=20=EA=BC=B4=20=C2=B7=20=EA=B5=AC=EC=97=AD?= =?UTF-8?q?=20=EB=A8=B8=EB=A6=AC=20=C2=B7=20=EC=9E=85=EB=A0=A5=20=EC=B9=B8?= =?UTF-8?q?=202=EC=97=B4)=20(PLAN=20557-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 위험 채움 토큰 밝음 #e5484d → #d6363c(흰 글 3.91 → 4.72) · 어두움 5.6 · 위험 글 #c0392b 그대로 - 새 부품 계산 줄(ui_template_calc_row) — B08 수리집수면적표 b08-hydro__row · 식 칸 배치를 대신함 - 표 부품 칸 램프(ui-data-table__lamp · --only · ✓) — B08 집계표 · 거리이정표 · B09 설계내역 램프 CSS 세 벌을 걷음(표지 class 는 풍선 · 가이드용으로 남김) - 모달 넓은 중간 창(wide + width 1100px · 1100 × 680) — B09 운반 창 손 인라인 크기를 걷음 - 값 목록 규칙 꼴(rule) — ☰ 설정 규칙 줄 ul/li 를 걷음 - 접이 구역 머리(createCollapsibleHead = 패널 머리 + 접는 제목) — B08 구조물도 「조합 연결」 제목(옛 b08-sheets__title)을 대신함 - 일반 화면 틀 좁은 폭 · 입력 칸 2열 · 끝 단추 줄 — D01 · E01 이 B02 CSS 를 빌리던 것을 끊음(가이드 장면 선택자 맞춤) - 부품 모음 견본 · 지킴 시험 test_557_2 · 낡은 시험(198_1 · 247_1 · 251_1 · 257_1 · 276_1 · 551_1)은 부품 CSS · 새 class 로 맞춤 Co-Authored-By: Claude Opus 5.5 --- A00_Common/app_settings.css | 28 +--- A00_Common/app_settings.ts | 19 +-- A05_EduDetail/guide/D01/_scenes.json | 14 +- A05_EduDetail/guide/E01/_scenes.json | 10 +- B08_Quantity/B08_Quantity_Summary_Table.ts | 14 +- B08_Quantity/B08_Quantity_UI_Drawing.css | 15 --- B08_Quantity/B08_Quantity_UI_Hydro.css | 80 +---------- B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts | 51 +++---- B08_Quantity/B08_Quantity_UI_Link.ts | 18 +-- B08_Quantity/B08_Quantity_UI_Milestone.css | 22 ---- B08_Quantity/B08_Quantity_UI_Milestone.ts | 9 +- B08_Quantity/B08_Quantity_UI_Page.css | 23 ---- .../B09_Estimation_UI_Haul_Modal.ts | 7 +- B09_Estimation/B09_Estimation_UI_Page.css | 26 ---- B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts | 2 +- D01_PlanRegister/D01_PlanRegister_UI_Page.ts | 13 +- E01_StabRegister/E01_StabRegister_UI_Page.ts | 11 +- .../tester/test_198_1_b08_summary_lamp.py | 14 +- .../test_247_1_b08_summary_green_lamp.py | 13 +- resources/tester/test_251_1_haul_groups.py | 7 +- resources/tester/test_257_1_b09_tabs_lamp.py | 27 ++-- resources/tester/test_276_1_haul_link_flow.py | 4 +- resources/tester/test_551_1_theme_tokens.py | 2 +- resources/tester/test_557_2_part_fixes.py | 124 ++++++++++++++++++ ui_template/ui_template_calc_row.css | 83 ++++++++++++ ui_template/ui_template_calc_row.ts | 65 +++++++++ ui_template/ui_template_collapsible.ts | 11 +- ui_template/ui_template_data_table.css | 33 ++++- ui_template/ui_template_data_table.ts | 2 + ui_template/ui_template_general_layout.css | 23 ++++ ui_template/ui_template_general_layout.ts | 16 ++- ui_template/ui_template_kv.css | 30 ++++- ui_template/ui_template_kv.ts | 21 ++- ui_template/ui_template_modal.css | 6 + ui_template/ui_template_modal.ts | 5 +- ui_template/ui_template_setting_row.ts | 2 +- ui_template/ui_template_theme.css | 2 +- ui_template/ui_template_ui_kit_frames.ts | 16 ++- ui_template/ui_template_ui_kit_parts.ts | 75 +++++++++-- 39 files changed, 604 insertions(+), 339 deletions(-) create mode 100644 resources/tester/test_557_2_part_fixes.py create mode 100644 ui_template/ui_template_calc_row.css create mode 100644 ui_template/ui_template_calc_row.ts diff --git a/A00_Common/app_settings.css b/A00_Common/app_settings.css index a1cdc7da4..c6b727fa6 100644 --- a/A00_Common/app_settings.css +++ b/A00_Common/app_settings.css @@ -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; -} diff --git a/A00_Common/app_settings.ts b/A00_Common/app_settings.ts index 17e6cbb27..fc4e4d242 100644 --- a/A00_Common/app_settings.ts +++ b/A00_Common/app_settings.ts @@ -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 { diff --git a/A05_EduDetail/guide/D01/_scenes.json b/A05_EduDetail/guide/D01/_scenes.json index cb344948a..020ba9eeb 100644 --- a/A05_EduDetail/guide/D01/_scenes.json +++ b/A05_EduDetail/guide/D01/_scenes.json @@ -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)"}]} ] diff --git a/A05_EduDetail/guide/E01/_scenes.json b/A05_EduDetail/guide/E01/_scenes.json index 6888fda5a..726f261f6 100644 --- a/A05_EduDetail/guide/E01/_scenes.json +++ b/A05_EduDetail/guide/E01/_scenes.json @@ -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": []} ] diff --git a/B08_Quantity/B08_Quantity_Summary_Table.ts b/B08_Quantity/B08_Quantity_Summary_Table.ts index 01a4ea70a..cf9feae6f 100644 --- a/B08_Quantity/B08_Quantity_Summary_Table.ts +++ b/B08_Quantity/B08_Quantity_Summary_Table.ts @@ -78,10 +78,10 @@ async function fetchPipeSummary(projectId: string): Promise { } 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"), diff --git a/B08_Quantity/B08_Quantity_UI_Drawing.css b/B08_Quantity/B08_Quantity_UI_Drawing.css index 3e517bed6..5dbb98d29 100644 --- a/B08_Quantity/B08_Quantity_UI_Drawing.css +++ b/B08_Quantity/B08_Quantity_UI_Drawing.css @@ -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); diff --git a/B08_Quantity/B08_Quantity_UI_Hydro.css b/B08_Quantity/B08_Quantity_UI_Hydro.css index ea3ab52e7..4364f63d3 100644 --- a/B08_Quantity/B08_Quantity_UI_Hydro.css +++ b/B08_Quantity/B08_Quantity_UI_Hydro.css @@ -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); diff --git a/B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts b/B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts index e47659aee..4ddd9bb4b 100644 --- a/B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts +++ b/B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts @@ -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( @@ -131,17 +132,8 @@ function node( 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(); 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"); diff --git a/B08_Quantity/B08_Quantity_UI_Link.ts b/B08_Quantity/B08_Quantity_UI_Link.ts index 546c70d47..6d5e359df 100644 --- a/B08_Quantity/B08_Quantity_UI_Link.ts +++ b/B08_Quantity/B08_Quantity_UI_Link.ts @@ -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"); diff --git a/B08_Quantity/B08_Quantity_UI_Milestone.css b/B08_Quantity/B08_Quantity_UI_Milestone.css index 81b49cafb..3d309df48 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone.css +++ b/B08_Quantity/B08_Quantity_UI_Milestone.css @@ -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; -} diff --git a/B08_Quantity/B08_Quantity_UI_Milestone.ts b/B08_Quantity/B08_Quantity_UI_Milestone.ts index fcc744443..5712b5b3c 100644 --- a/B08_Quantity/B08_Quantity_UI_Milestone.ts +++ b/B08_Quantity/B08_Quantity_UI_Milestone.ts @@ -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]; diff --git a/B08_Quantity/B08_Quantity_UI_Page.css b/B08_Quantity/B08_Quantity_UI_Page.css index a30f8cd67..108340f93 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.css +++ b/B08_Quantity/B08_Quantity_UI_Page.css @@ -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; diff --git a/B09_Estimation/B09_Estimation_UI_Haul_Modal.ts b/B09_Estimation/B09_Estimation_UI_Haul_Modal.ts index a932921ab..c1ca20c3e 100644 --- a/B09_Estimation/B09_Estimation_UI_Haul_Modal.ts +++ b/B09_Estimation/B09_Estimation_UI_Haul_Modal.ts @@ -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" }, diff --git a/B09_Estimation/B09_Estimation_UI_Page.css b/B09_Estimation/B09_Estimation_UI_Page.css index be4fa3db5..82bc79315 100644 --- a/B09_Estimation/B09_Estimation_UI_Page.css +++ b/B09_Estimation/B09_Estimation_UI_Page.css @@ -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)); diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts index 1c37d2653..7ae93caf2 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts @@ -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; }, diff --git a/D01_PlanRegister/D01_PlanRegister_UI_Page.ts b/D01_PlanRegister/D01_PlanRegister_UI_Page.ts index aad82e483..38345f802 100644 --- a/D01_PlanRegister/D01_PlanRegister_UI_Page.ts +++ b/D01_PlanRegister/D01_PlanRegister_UI_Page.ts @@ -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 }), diff --git a/E01_StabRegister/E01_StabRegister_UI_Page.ts b/E01_StabRegister/E01_StabRegister_UI_Page.ts index 461f78469..0ef5c900a 100644 --- a/E01_StabRegister/E01_StabRegister_UI_Page.ts +++ b/E01_StabRegister/E01_StabRegister_UI_Page.ts @@ -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 }), diff --git a/resources/tester/test_198_1_b08_summary_lamp.py b/resources/tester/test_198_1_b08_summary_lamp.py index 1937ba50a..b221af661 100644 --- a/resources/tester/test_198_1_b08_summary_lamp.py +++ b/resources/tester/test_198_1_b08_summary_lamp.py @@ -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' """ ) 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)) diff --git a/resources/tester/test_251_1_haul_groups.py b/resources/tester/test_251_1_haul_groups.py index 3ae95ee38..7ea3689de 100644 --- a/resources/tester/test_251_1_haul_groups.py +++ b/resources/tester/test_251_1_haul_groups.py @@ -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 # 틀 높이 바뀜 = 다시 그림(아래 잘림 없음) diff --git a/resources/tester/test_257_1_b09_tabs_lamp.py b/resources/tester/test_257_1_b09_tabs_lamp.py index 4a15af2be..46ba7e0c0 100644 --- a/resources/tester/test_257_1_b09_tabs_lamp.py +++ b/resources/tester/test_257_1_b09_tabs_lamp.py @@ -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 diff --git a/resources/tester/test_276_1_haul_link_flow.py b/resources/tester/test_276_1_haul_link_flow.py index 0a0353374..baa60cc0d 100644 --- a/resources/tester/test_276_1_haul_link_flow.py +++ b/resources/tester/test_276_1_haul_link_flow.py @@ -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 diff --git a/resources/tester/test_551_1_theme_tokens.py b/resources/tester/test_551_1_theme_tokens.py index 483e1a507..ec0f80bba 100644 --- a/resources/tester/test_551_1_theme_tokens.py +++ b/resources/tester/test_551_1_theme_tokens.py @@ -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 # 어두움도 별칭으로 따라옴 diff --git a/resources/tester/test_557_2_part_fixes.py b/resources/tester/test_557_2_part_fixes.py new file mode 100644 index 000000000..73223de69 --- /dev/null +++ b/resources/tester/test_557_2_part_fixes.py @@ -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") diff --git a/ui_template/ui_template_calc_row.css b/ui_template/ui_template_calc_row.css new file mode 100644 index 000000000..ff01ae8f1 --- /dev/null +++ b/ui_template/ui_template_calc_row.css @@ -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; +} diff --git a/ui_template/ui_template_calc_row.ts b/ui_template/ui_template_calc_row.ts new file mode 100644 index 000000000..86b713527 --- /dev/null +++ b/ui_template/ui_template_calc_row.ts @@ -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; +} diff --git a/ui_template/ui_template_collapsible.ts b/ui_template/ui_template_collapsible.ts index 836c998e6..9efdeb2fa 100644 --- a/ui_template/ui_template_collapsible.ts +++ b/ui_template/ui_template_collapsible.ts @@ -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)` - * 대신하는 옛 변종 = 날 `
` — B06 넷 · E01 `e-missing` · E03 `e03-prob-src` · M01 넷 · M02 일곱 · A07 약관 + * 대신하는 옛 변종 = 날 `
` — 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) => { diff --git a/ui_template/ui_template_data_table.css b/ui_template/ui_template_data_table.css index 966c54bf5..dc3f2fcb1 100644 --- a/ui_template/ui_template_data_table.css +++ b/ui_template/ui_template_data_table.css @@ -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; } diff --git a/ui_template/ui_template_data_table.ts b/ui_template/ui_template_data_table.ts index 40a7c1a5d..6fd8c5c2d 100644 --- a/ui_template/ui_template_data_table.ts +++ b/ui_template/ui_template_data_table.ts @@ -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"; diff --git a/ui_template/ui_template_general_layout.css b/ui_template/ui_template_general_layout.css index 9dd1e3d50..a69df5daa 100644 --- a/ui_template/ui_template_general_layout.css +++ b/ui_template/ui_template_general_layout.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, diff --git a/ui_template/ui_template_general_layout.ts b/ui_template/ui_template_general_layout.ts index 6cf887e0a..111770479 100644 --- a/ui_template/ui_template_general_layout.ts +++ b/ui_template/ui_template_general_layout.ts @@ -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; diff --git a/ui_template/ui_template_kv.css b/ui_template/ui_template_kv.css index fb825ee03..226aca608 100644 --- a/ui_template/ui_template_kv.css +++ b/ui_template/ui_template_kv.css @@ -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; } diff --git a/ui_template/ui_template_kv.ts b/ui_template/ui_template_kv.ts index bdbff289c..deaa0a477 100644 --- a/ui_template/ui_template_kv.ts +++ b/ui_template/ui_template_kv.ts @@ -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], }), ); } diff --git a/ui_template/ui_template_modal.css b/ui_template/ui_template_modal.css index 7b0edfd4c..732afa4b3 100644 --- a/ui_template/ui_template_modal.css +++ b/ui_template/ui_template_modal.css @@ -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); diff --git a/ui_template/ui_template_modal.ts b/ui_template/ui_template_modal.ts index e09a9bb7a..83d18e36c 100644 --- a/ui_template/ui_template_modal.ts +++ b/ui_template/ui_template_modal.ts @@ -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; diff --git a/ui_template/ui_template_setting_row.ts b/ui_template/ui_template_setting_row.ts index 6849932b5..3e84ee20c 100644 --- a/ui_template/ui_template_setting_row.ts +++ b/ui_template/ui_template_setting_row.ts @@ -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"; diff --git a/ui_template/ui_template_theme.css b/ui_template/ui_template_theme.css index 51feea2a2..0493e0bca 100644 --- a/ui_template/ui_template_theme.css +++ b/ui_template/ui_template_theme.css @@ -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); diff --git a/ui_template/ui_template_ui_kit_frames.ts b/ui_template/ui_template_ui_kit_frames.ts index 0681ea42d..5b5c4326f 100644 --- a/ui_template/ui_template_ui_kit_frames.ts +++ b/ui_template/ui_template_ui_kit_frames.ts @@ -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; } diff --git a/ui_template/ui_template_ui_kit_parts.ts b/ui_template/ui_template_ui_kit_parts.ts index aa5024271..9d46124da 100644 --- a/ui_template/ui_template_ui_kit_parts.ts +++ b/ui_template/ui_template_ui_kit_parts.ts @@ -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(".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(".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("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(".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"] },