diff --git a/A00_Common/spreadsheet/spreadsheet_tabs.ts b/A00_Common/spreadsheet/spreadsheet_tabs.ts index f4cc8a551..efdb0fe82 100644 --- a/A00_Common/spreadsheet/spreadsheet_tabs.ts +++ b/A00_Common/spreadsheet/spreadsheet_tabs.ts @@ -12,7 +12,7 @@ * ========================================================================== */ import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu"; -import { el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; +import { createIconButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { emptySheet } from "./spreadsheet_commands"; import { hideSheet, unhideSheet } from "./spreadsheet_sheet_extra"; import { st } from "./spreadsheet_text"; @@ -164,11 +164,13 @@ export function mountTabs(ctx: SpreadsheetContext): PartHandle { } function addButton(): HTMLElement { - const btn = el("button", { - className: "ss-tabs__add", - text: "+", - attrs: { type: "button", title: st("SheetAdd") }, + const btn = createIconButton({ + glyph: "+", + title: st("SheetAdd"), + variant: "outline", + size: 22, }); + btn.classList.add("ss-tabs__add"); btn.addEventListener("click", () => { const name = nextSheetName(ctx.book, "Sheet"); const sheet = emptySheet(nextSheetId(ctx.book), name); diff --git a/A00_Common/spreadsheet/spreadsheet_toolbar.css b/A00_Common/spreadsheet/spreadsheet_toolbar.css index fc0ffa84f..92606abe7 100644 --- a/A00_Common/spreadsheet/spreadsheet_toolbar.css +++ b/A00_Common/spreadsheet/spreadsheet_toolbar.css @@ -27,24 +27,10 @@ border-right: 0; } -.ss-toolbar .ui-btn { - padding: 4px 8px; - font-size: 0.82rem; -} - +/* 단추 · 칸 = 부품 단추(24) · 입력 · 고르기(28) — 여기는 켬 · 폭만(553-6) */ .ss-toolbar .ui-btn.is-on { - background: var(--color-mist-violet); - box-shadow: inset 0 0 0 1px var(--color-royal-amethyst); -} - -.ss-toolbar__select { - height: 28px; - padding: 0 4px; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - background: var(--color-canvas); - color: var(--color-text-body); - font-size: 0.8rem; + background: var(--color-accent-soft); + box-shadow: inset 0 0 0 1px var(--color-accent); } .ss-toolbar__select option { @@ -53,14 +39,7 @@ } .ss-toolbar__input { - height: 28px; width: 72px; - padding: 0 4px; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - background: var(--color-canvas); - color: var(--color-text-body); - font-size: 0.8rem; } .ss-toolbar__input--font { @@ -85,15 +64,6 @@ cursor: pointer; } -.ss-toolbar__clear { - padding: 0 4px; - border: 0; - background: none; - color: var(--color-text-secondary); - font-size: 0.72rem; - cursor: pointer; -} - .ss-toolbar__popover { position: absolute; top: calc(100% + 4px); @@ -138,7 +108,7 @@ .ss-toolbar__unit-label { margin-bottom: var(--spacing-4); color: var(--color-text-muted); - font-size: 0.72rem; + font-size: var(--text-caption); } .ss-toolbar__unit-grid { @@ -147,9 +117,9 @@ gap: var(--spacing-4); } -.ss-toolbar__unit-btn { +.ss-toolbar__unit-grid > .ui-btn[data-size] { min-width: 0; - padding: 4px 2px; + padding-inline: 2px; } .ss-toolbar__hint { @@ -232,15 +202,6 @@ font: inherit; } -.ss-tabs__add { - padding: 2px 8px; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); - background: var(--color-canvas); - color: var(--color-text-secondary); - cursor: pointer; -} - /* ── 우클릭 메뉴(그리드 · 탭 공용, `ui_template_context_menu.ts`) — 모양 · 자리는 공용 `.ui-ctx-menu` ── */ .ss__context-menu-item.is-danger { @@ -261,7 +222,7 @@ display: flex; align-items: center; justify-content: center; - background: rgb(0 0 0 / 35%); + background: var(--color-scrim); } .ss-comment-editor__panel { diff --git a/A00_Common/spreadsheet/spreadsheet_toolbar.ts b/A00_Common/spreadsheet/spreadsheet_toolbar.ts index d9fccdb72..7ebda5a32 100644 --- a/A00_Common/spreadsheet/spreadsheet_toolbar.ts +++ b/A00_Common/spreadsheet/spreadsheet_toolbar.ts @@ -10,7 +10,7 @@ * 스스로 구독하지 않음. `destroy()` 는 부품을 뗄 때. * ========================================================================== */ -import { createButton, el } from "@ui/ui_template_elements"; +import { createButton, createIconButton, el, setUiSize } from "@ui/ui_template_elements"; import { confirmMerge } from "./spreadsheet_actions"; import { colName, rangeToA1, toA1 } from "./spreadsheet_address"; import { shownHAlign, shownVAlign } from "./spreadsheet_cellstyle"; @@ -144,14 +144,25 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { }; // ── 되돌리기 · 다시 ──────────────────────────────────────────────────── - const undoBtn = createButton({ label: "↶", variant: "ghost", onClick: () => ctx.undo() }); + const undoBtn = createButton({ + label: "↶", + variant: "ghost", + size: 24, + onClick: () => ctx.undo(), + }); undoBtn.title = st("Undo"); - const redoBtn = createButton({ label: "↷", variant: "ghost", onClick: () => ctx.redo() }); + const redoBtn = createButton({ + label: "↷", + variant: "ghost", + size: 24, + onClick: () => ctx.redo(), + }); redoBtn.title = st("Redo"); // 손가락 — 우클릭 대신 활성 칸 메뉴를 여는 ⋯(기기대응 3-5 · 터치 모드에서만 보임) const moreBtn = createButton({ label: "⋯", variant: "ghost", + size: 24, onClick: () => { const { r, c } = ctx.selection.활성; const { box } = ctx.grid.editorSlot(r, c); @@ -175,7 +186,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { // ── 글꼴 · 크기 ──────────────────────────────────────────────────────── const fontInput = el("input", { - className: "ss-toolbar__input ss-toolbar__input--font", + className: "ui-input ss-toolbar__input ss-toolbar__input--font", attrs: { list: "ss-toolbar-fonts", "aria-label": st("FontName") }, }) as HTMLInputElement; const fontList = el("datalist", { @@ -187,7 +198,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { fontInput.addEventListener("change", commitFont); const sizeInput = el("input", { - className: "ss-toolbar__input ss-toolbar__input--size", + className: "ui-input ss-toolbar__input ss-toolbar__input--size", attrs: { type: "number", min: "1", max: "409", step: "0.5", "aria-label": st("FontSize") }, }) as HTMLInputElement; sizeInput.addEventListener("change", () => { @@ -196,13 +207,13 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { }); // ── 굵게 · 기울임 · 밑줄 · 취소선 ────────────────────────────────────── - const boldBtn = createButton({ label: "B", variant: "ghost" }); + const boldBtn = createButton({ label: "B", variant: "ghost", size: 24 }); boldBtn.title = st("Bold"); - const italicBtn = createButton({ label: "I", variant: "ghost" }); + const italicBtn = createButton({ label: "I", variant: "ghost", size: 24 }); italicBtn.title = st("Italic"); - const underlineBtn = createButton({ label: "U", variant: "ghost" }); + const underlineBtn = createButton({ label: "U", variant: "ghost", size: 24 }); underlineBtn.title = st("Underline"); - const strikeBtn = createButton({ label: "S", variant: "ghost" }); + const strikeBtn = createButton({ label: "S", variant: "ghost", size: 24 }); strikeBtn.title = st("Strike"); const toggles: [HTMLButtonElement, keyof CellStyle][] = [ [boldBtn, "굵게"], @@ -220,11 +231,8 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { attrs: { type: "color", "aria-label": st("TextColor"), value: "#000000" }, }) as HTMLInputElement; textColor.addEventListener("change", () => patch({ 글자색: textColor.value })); - const textColorClear = el("button", { - className: "ss-toolbar__clear", - text: "✕", - attrs: { type: "button", title: st("ColorClear") }, - }); + const textColorClear = createIconButton({ glyph: "✕", title: st("ColorClear"), size: 18 }); + textColorClear.classList.add("ss-toolbar__clear"); textColorClear.addEventListener("click", () => patch({ 글자색: null })); const fillColor = el("input", { @@ -232,11 +240,8 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { attrs: { type: "color", "aria-label": st("FillColor"), value: "#ffffff" }, }) as HTMLInputElement; fillColor.addEventListener("change", () => patch({ 채움: fillColor.value })); - const fillColorClear = el("button", { - className: "ss-toolbar__clear", - text: "✕", - attrs: { type: "button", title: st("ColorClear") }, - }); + const fillColorClear = createIconButton({ glyph: "✕", title: st("ColorClear"), size: 18 }); + fillColorClear.classList.add("ss-toolbar__clear"); fillColorClear.addEventListener("click", () => patch({ 채움: null })); // ── 가로 · 세로 정렬 · 줄 바꿈 ───────────────────────────────────────── @@ -255,7 +260,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { } as const; const hAlignSelect = el("select", { - className: "ss-toolbar__select", + className: "ui-select ss-toolbar__select", attrs: { "aria-label": st("HAlign") }, children: (Object.keys(HALIGN_LABEL) as (keyof typeof HALIGN_LABEL)[]).map((v) => el("option", { attrs: { value: v }, text: HALIGN_LABEL[v] }), @@ -266,7 +271,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { ); const vAlignSelect = el("select", { - className: "ss-toolbar__select", + className: "ui-select ss-toolbar__select", attrs: { "aria-label": st("VAlign") }, children: (Object.keys(VALIGN_LABEL) as (keyof typeof VALIGN_LABEL)[]).map((v) => el("option", { attrs: { value: v }, text: VALIGN_LABEL[v] }), @@ -276,11 +281,11 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { patch({ 세로: vAlignSelect.value as CellStyle["세로"] }), ); - const wrapBtn = createButton({ label: st("Wrap"), variant: "ghost" }); + const wrapBtn = createButton({ label: st("Wrap"), variant: "ghost", size: 24 }); wrapBtn.addEventListener("click", () => patch({ 줄바꿈: !wrapBtn.classList.contains("is-on") })); // ── 병합 ─────────────────────────────────────────────────────────────── - const mergeBtn = createButton({ label: st("Merge"), variant: "ghost" }); + const mergeBtn = createButton({ label: st("Merge"), variant: "ghost", size: 24 }); mergeBtn.addEventListener("click", async () => { const range = ctx.selection.범위[0]; if (!range) return; @@ -290,10 +295,10 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { }); // ── 테두리 ─────────────────────────────────────────────────────────── - const borderBtn = createButton({ label: st("Border"), variant: "ghost" }); + const borderBtn = createButton({ label: st("Border"), variant: "ghost", size: 24 }); const borderPop = el("div", { className: "ss-toolbar__popover", attrs: { hidden: "true" } }); const lineSelect = el("select", { - className: "ss-toolbar__select", + className: "ui-select ss-toolbar__select", children: BORDER_LINES.map((v) => el("option", { attrs: { value: v }, text: v })), }) as HTMLSelectElement; const lineColor = el("input", { @@ -314,6 +319,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { const b = createButton({ label, variant: "ghost", + size: 24, onClick: () => { applyBorderPreset(preset); borderPop.hidden = true; @@ -393,7 +399,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { // ── 숫자 형식 ───────────────────────────────────────────────────────── const numfmtInput = el("input", { - className: "ss-toolbar__input ss-toolbar__input--numfmt", + className: "ui-input ss-toolbar__input ss-toolbar__input--numfmt", attrs: { list: "ss-toolbar-numfmt", "aria-label": st("NumberFormat"), @@ -414,7 +420,7 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { numfmtInput.addEventListener("change", () => patch({ 형식: numfmtInput.value || null })); // ── 단위 기호 ───────────────────────────────────────────────────────── - const unitBtn = createButton({ label: `${st("Unit")} ▾`, variant: "ghost" }); + const unitBtn = createButton({ label: `${st("Unit")} ▾`, variant: "ghost", size: 24 }); const unitPop = el("div", { className: "ss-toolbar__popover ss-toolbar__unit-pop", attrs: { hidden: "true" }, @@ -472,7 +478,12 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { recentGrid.textContent = ""; recentSection.hidden = list.length === 0; for (const u of list) { - const b = createButton({ label: u, variant: "ghost", onClick: () => insertUnit(u) }); + const b = createButton({ + label: u, + variant: "ghost", + size: 24, + onClick: () => insertUnit(u), + }); b.className += " ss-toolbar__unit-btn"; recentGrid.append(b); } @@ -483,7 +494,12 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { for (const [labelKey, items] of UNIT_GROUPS) { const grid = el("div", { className: "ss-toolbar__unit-grid" }); for (const u of items) { - const b = createButton({ label: u, variant: "ghost", onClick: () => insertUnit(u) }); + const b = createButton({ + label: u, + variant: "ghost", + size: 24, + onClick: () => insertUnit(u), + }); b.className += " ss-toolbar__unit-btn"; grid.append(b); } @@ -536,6 +552,8 @@ export function mountToolbar(ctx: SpreadsheetContext): PartHandle { group(unitBtn, unitPop), formulaHint, ); + // 칸 크기 단(553-6) — 단추 24 · 칸 28 + for (const f of root.querySelectorAll(".ui-input, .ui-select")) setUiSize(f, 28); function refresh(): void { undoBtn.disabled = !ctx.history.canUndo(); diff --git a/E01_StabRegister/E01_StabRegister_UI_Missing.css b/E01_StabRegister/E01_StabRegister_UI_Missing.css deleted file mode 100644 index 0e54a15cf..000000000 --- a/E01_StabRegister/E01_StabRegister_UI_Missing.css +++ /dev/null @@ -1,21 +0,0 @@ -/* 빠진 횡단 띠 — E02 · E03 화면 위(E01_StabRegister_UI_Missing.ts) */ -.e-missing { - margin: 0 0 8px; - padding: 6px 10px; - border: 1px solid var(--color-warning); - border-left-width: 4px; - border-radius: var(--radius-sm); - background: var(--color-surface-raised); - color: var(--color-text); - font-size: 13px; -} - -.e-missing > summary { - cursor: pointer; - font-weight: 600; -} - -.e-missing > p { - margin: 6px 0 0; - color: var(--color-text-secondary); -} diff --git a/E01_StabRegister/E01_StabRegister_UI_Missing.ts b/E01_StabRegister/E01_StabRegister_UI_Missing.ts index 252d0ccc9..eadd7b4bc 100644 --- a/E01_StabRegister/E01_StabRegister_UI_Missing.ts +++ b/E01_StabRegister/E01_StabRegister_UI_Missing.ts @@ -1,10 +1,11 @@ /* ============================================================================= * E01_StabRegister_UI_Missing.ts * 빠진 횡단 띠(요구 351 · PLAN 532-5) — E02 · E03 화면 위 「횡단 없는 측점 N 개 — 해석에서 빠짐」. - * 목록 = 서버 `missing_stations`(E01 복사본에 횡단 파일이 없는 측점 누가거리) · 누르면 펼침. + * 목록 = 서버 `missing_stations`(E01 복사본에 횡단 파일이 없는 측점 누가거리) · 「자세히」로 펼침. + * 모양 = 띠 알림 부품(경고). * ========================================================================== */ -import "./E01_StabRegister_UI_Missing.css"; +import { createBanner } from "@ui/ui_template_banner"; import { el } from "@ui/ui_template_elements"; import { t } from "@ui/ui_template_locale"; @@ -12,13 +13,13 @@ import { t } from "@ui/ui_template_locale"; export function buildMissingBanner(missing: number[] | undefined): HTMLElement { const list = missing ?? []; if (!list.length) return el("div"); - const box = el("details", { - className: "e-missing", - attrs: { "data-role": "missing-stations", "data-count": String(list.length) }, + const box = createBanner({ + tone: "warning", + text: t("E_Missing_Head").replace("{n}", String(list.length)), + details: el("p", { text: list.map((m) => `${Math.round(m * 100) / 100} m`).join(" · ") }), }); - box.append( - el("summary", { text: t("E_Missing_Head").replace("{n}", String(list.length)) }), - el("p", { text: list.map((m) => `${Math.round(m * 100) / 100} m`).join(" · ") }), - ); + box.classList.add("e-missing"); + box.dataset.role = "missing-stations"; + box.dataset.count = String(list.length); return box; } diff --git a/E02_StabGround/E02_StabGround_UI_Page.ts b/E02_StabGround/E02_StabGround_UI_Page.ts index b45bb974f..a483e2d71 100644 --- a/E02_StabGround/E02_StabGround_UI_Page.ts +++ b/E02_StabGround/E02_StabGround_UI_Page.ts @@ -18,10 +18,11 @@ import { showToast, } from "@ui/ui_template_elements"; import { attachHoverInfo } from "@ui/ui_template_hover_info"; +import { createText } from "@ui/ui_template_text"; import { t, type LocaleKey } from "@ui/ui_template_locale"; import { mountGroupStep } from "../A00_Common/group_workflow"; import { buildMissingBanner } from "../E01_StabRegister/E01_StabRegister_UI_Missing"; -import { buildCards, type E02View } from "./E02_StabGround_UI_Tables"; +import { buildCards, hook, type E02View } from "./E02_StabGround_UI_Tables"; import { checkGround, type CheckError, @@ -74,7 +75,7 @@ async function request(path: string, init?: RequestInit): Promise {}; @@ -104,22 +104,28 @@ export function renderE02StabGround(root: HTMLElement): void { if (view && !view.editable) lines.push(t("E02_ReadOnly")); if (dirty) lines.push(t("E02_Unsaved")); if (errors.length) lines.push(`${t("E02_Invalid")} ${errors.length}`); - status.textContent = lines.join("\n"); + status.replaceChildren( + ...(lines.length + ? [createText(lines.join("\n"), errors.length > 0 ? "error" : "warning")] + : []), + ); status.classList.toggle("is-error", errors.length > 0); }; - /** 틀린 칸 표시 — 칸마다 is-invalid + data-error(마우스 글) */ + /** 틀린 칸 표시 — 칸마다 is-error(부품 오류 상태) + data-error(마우스 글) */ const mark = (): CheckError[] => { if (!view) return []; const errors = checkGround(view.ground, view.defaults.limits); - for (const node of main.querySelectorAll(".is-invalid")) { - node.classList.remove("is-invalid"); + for (const node of main.querySelectorAll("[data-error]")) { + node.classList.remove("is-error"); + node.removeAttribute("aria-invalid"); delete node.dataset.error; } for (const error of errors) { const node = main.querySelector(`[data-path="${error.path}"]`); if (!node) continue; - node.classList.add("is-invalid"); + node.classList.add("is-error"); + node.setAttribute("aria-invalid", "true"); node.dataset.error = errorText(error, view.defaults); } paintStatus(errors); @@ -242,10 +248,10 @@ export function renderE02StabGround(root: HTMLElement): void { adopt(body, true); } catch (error) { main.replaceChildren( - el("p", { - className: "e02-empty", - text: error instanceof Error ? error.message : t("Common_Status_Error"), - }), + hook( + createText(error instanceof Error ? error.message : t("Common_Status_Error")), + "e02-empty", + ), ); } })(); diff --git a/E02_StabGround/E02_StabGround_UI_Style.css b/E02_StabGround/E02_StabGround_UI_Style.css index 88b33d304..3e715fb20 100644 --- a/E02_StabGround/E02_StabGround_UI_Style.css +++ b/E02_StabGround/E02_StabGround_UI_Style.css @@ -10,6 +10,10 @@ box-sizing: border-box; } +.e02-page > .ui-banner { + margin-bottom: var(--spacing-8); +} + .e02-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(560px, 100%), 1fr)); @@ -17,33 +21,19 @@ align-items: start; } +/* 카드 모양은 카드 부품(머리 카드) — 여기는 자리만 */ .e02-card { - display: flex; - flex-direction: column; - gap: var(--spacing-8); min-width: 0; - padding: var(--spacing-12) var(--spacing-16); - border-radius: var(--radius-sm); - background: var(--color-surface-raised); } -.e02-card__head { - display: flex; - align-items: center; - justify-content: space-between; +.e02-card .ui-card__body { gap: var(--spacing-8); } -.e02-card__title, .e02-card__sub { - margin: 0; + margin: var(--spacing-8) 0 0; color: var(--color-text); - font-size: var(--text-body); -} - -.e02-card__sub { font-size: var(--text-body-sm); - margin-top: var(--spacing-8); } .e02-card__actions { @@ -103,11 +93,6 @@ min-width: 96px; } -.e02-input.is-invalid { - border-color: var(--color-danger); - box-shadow: 0 0 0 1px var(--color-danger); -} - .e02-rowhead { white-space: nowrap; color: var(--color-text-secondary); @@ -135,22 +120,9 @@ color: var(--color-text-secondary); } -.e02-hint, -.e02-empty { - margin: 0; - font-size: var(--text-caption); - color: var(--color-text-secondary); -} - .e02-status { margin: var(--spacing-8) 0 0; white-space: pre-line; - font-size: var(--text-body-sm); - color: var(--color-warning); -} - -.e02-status.is-error { - color: var(--color-danger); } .e02-table .ui-btn { diff --git a/E02_StabGround/E02_StabGround_UI_Tables.ts b/E02_StabGround/E02_StabGround_UI_Tables.ts index 5b38bd0b1..7f9967e9e 100644 --- a/E02_StabGround/E02_StabGround_UI_Tables.ts +++ b/E02_StabGround/E02_StabGround_UI_Tables.ts @@ -6,8 +6,16 @@ * 값 바꿈 = view.change()(캐시 · 검사) · 줄 더함 · 지움 · [기본값] = view.rebuild(). * ========================================================================== */ -import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { + createButton, + createInputField, + createSelectField, + el, + showToast, +} from "@ui/ui_template_elements"; +import { createCard } from "@ui/ui_template_card"; import { createCheckbox } from "@ui/ui_template_checkbox"; +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"; import { @@ -32,6 +40,11 @@ export interface E02View { } const L = (key: string): string => t(key as LocaleKey); +/** 부품이 낸 요소에 촬영 장면 · 시험이 잡는 이름만 얹음(CSS 없음) */ +export const hook = (node: T, name: string): T => ( + node.classList.add(name), + node +); export const clone = (value: T): T => JSON.parse(JSON.stringify(value)) as T; const FIELD_LABEL: Record<(typeof MATERIAL_FIELDS)[number], LocaleKey> = { gamma_t: "E02_Col_GammaT", @@ -48,10 +61,8 @@ export function numInput( set: (v: number | null) => void, meta: { src?: string; def?: number | null; nullable?: boolean } = {}, ): HTMLInputElement { - const input = el("input", { - className: "ui-input ui-input--sm e02-input e02-input--num", - }) as HTMLInputElement; - input.type = "number"; + const input = createInputField({ type: "number", compact: true }).input; + input.classList.add("e02-input", "e02-input--num"); input.step = "any"; input.value = value === null || value === undefined ? "" : String(value); input.dataset.path = path; @@ -75,10 +86,8 @@ export function selectInput( set: (v: string) => void, meta: { src?: string; def?: string } = {}, ): HTMLSelectElement { - const select = el("select", { - className: "ui-select ui-select--sm e02-input", - }) as HTMLSelectElement; - for (const option of options) select.append(new Option(option.text, option.value)); + const select = createSelectField({ options, compact: true }).select; + select.classList.add("e02-input"); select.value = value ?? ""; select.dataset.path = path; if (meta.src) select.dataset.src = meta.src; @@ -95,10 +104,9 @@ export function card( title: LocaleKey, ...body: HTMLElement[] ): { section: HTMLElement; head: HTMLElement } { - const head = el("div", { className: "e02-card__head" }); - head.append(el("h3", { className: "e02-card__title", text: t(title) })); - const section = el("section", { className: "e02-card ui-sidebar-section" }); - section.append(head, ...body); + const head = el("div", { className: "e02-card__actions" }); + const section = createCard({ title: t(title), actions: [head], body, pad: 16 }); + section.classList.add("e02-card", "ui-sidebar-section"); return { section, head }; } @@ -108,9 +116,7 @@ export function headButtons( ...buttons: HTMLButtonElement[] ): void { if (!view.editable) return; - const row = el("div", { className: "e02-card__actions" }); - row.append(...buttons); - head.append(row); + head.append(...buttons); } export function table(headers: string[], rows: HTMLElement[][]): HTMLTableElement { @@ -141,9 +147,8 @@ function materialsCard(view: E02View): HTMLElement { Object.values(g.label_map).includes(id) || g.zones.some((z) => z.soil === id || z.rock === id); const rows = g.materials.map((m, i) => { const base = `materials.${i}`; - const name = el("input", { - className: "ui-input ui-input--sm e02-input e02-input--name", - }) as HTMLInputElement; + const name = createInputField({ compact: true }).input; + name.classList.add("e02-input", "e02-input--name"); name.value = m.name; name.dataset.path = `${base}.name`; name.disabled = !view.editable; @@ -293,15 +298,13 @@ function zonesCard(view: E02View): { section: HTMLElement; refresh: () => void } ], rows, ) - : el("p", { className: "e02-empty", text: t("E02_Zone_Empty") }); + : hook(createText(t("E02_Zone_Empty")), "e02-empty"); const stationsHost = el("div", { className: "e02-stations" }); const refresh = (): void => { stationsHost.replaceChildren(); if (!g.stations.length) - return void stationsHost.append( - el("p", { className: "e02-empty", text: t("E02_Stations_Empty") }), - ); + return void stationsHost.append(hook(createText(t("E02_Stations_Empty")), "e02-empty")); const names = new Map(g.materials.map((m) => [m.id, m.name])); const interval = inferStationInterval(g.stations); const fmt = (v: number | null): string => (v === null ? "-" : v.toFixed(2)); @@ -332,7 +335,7 @@ function zonesCard(view: E02View): { section: HTMLElement; refresh: () => void } const { section, head } = card( "E02_Card_Zones", - el("p", { className: "e02-hint", text: t("E02_Zone_Hint") }), + hook(createText(t("E02_Zone_Hint")), "e02-hint"), zoneBody, el("h4", { className: "e02-card__sub", text: t("E02_Card_Stations") }), stationsHost, diff --git a/E02_StabGround/E02_StabGround_UI_Water.ts b/E02_StabGround/E02_StabGround_UI_Water.ts index e613ae911..08d1fbba6 100644 --- a/E02_StabGround/E02_StabGround_UI_Water.ts +++ b/E02_StabGround/E02_StabGround_UI_Water.ts @@ -6,12 +6,14 @@ * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; +import { createText } from "@ui/ui_template_text"; import { t, type LocaleKey } from "@ui/ui_template_locale"; import { WATER_KEYS, WATER_RULES, WET_MODES, type WetMode } from "./logic/E02_StabGround_Check"; import { card, clone, headButtons, + hook, numInput, selectInput, table, @@ -40,7 +42,7 @@ function removeButton(view: E02View, onClick: () => void): HTMLButtonElement { /** 사용자 수위선 구간 표 — 줄마다 시작 · 끝 · 점(offset · 깊이) · [점 추가] · [지움] */ function linesBody(view: E02View): HTMLElement { const water = view.ground.groundwater; - if (!water.lines.length) return el("p", { className: "e02-empty", text: t("E02_Line_Empty") }); + if (!water.lines.length) return hook(createText(t("E02_Line_Empty")), "e02-empty"); const rows = water.lines.map((line, i) => { const base = `groundwater.lines.${i}`; const points = el("div", { className: "e02-points" }); @@ -136,11 +138,11 @@ export function waterCard(view: E02View): HTMLElement { ), ); if (water.wet_mode === "line") - body.push(el("p", { className: "e02-hint", text: t("E02_Line_Hint") }), linesBody(view)); + body.push(hook(createText(t("E02_Line_Hint")), "e02-hint"), linesBody(view)); const { section, head } = card( "E02_Card_Water", - el("p", { className: "e02-hint", text: t("E02_Water_Hint") }), + hook(createText(t("E02_Water_Hint")), "e02-hint"), grid, ...body, ); diff --git a/resources/tester/test_553_4_de_parts.py b/resources/tester/test_553_4_de_parts.py index 7cc5d81d6..9fc87bb68 100644 --- a/resources/tester/test_553_4_de_parts.py +++ b/resources/tester/test_553_4_de_parts.py @@ -57,3 +57,23 @@ 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 + + +def test_e01_missing_is_banner_part(): + ts = _src("E01_StabRegister/E01_StabRegister_UI_Missing.ts") + assert "createBanner" in ts and '"details"' not in ts + assert 'dataset.role = "missing-stations"' in ts # 시험 · 촬영이 잡음 + assert not (ROOT / "E01_StabRegister/E01_StabRegister_UI_Missing.css").exists() + + +def test_e02_parts(): + tables = _src("E02_StabGround/E02_StabGround_UI_Tables.ts") + page = _src("E02_StabGround/E02_StabGround_UI_Page.ts") + css = _src("E02_StabGround/E02_StabGround_UI_Style.css") + assert 'el("input"' not in tables and 'el("select"' not in tables + assert "createInputField(" in tables and "createSelectField(" in tables + assert "createCard({ title" in tables + assert "is-invalid" not in page + css and "is-error" in page + 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")