Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<HTMLElement>(".ui-input, .ui-select")) setUiSize(f, 28);
|
||||
|
||||
function refresh(): void {
|
||||
undoBtn.disabled = !ctx.history.canUndo();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<GroundResponse
|
||||
export function renderE02StabGround(root: HTMLElement): void {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const main = el("div", { className: "e02-page" });
|
||||
const status = el("p", { className: "e02-status" });
|
||||
const status = el("div", { className: "e02-status" });
|
||||
const saveButton = createButton({
|
||||
label: t("Common_Btn_Save"),
|
||||
variant: "filled",
|
||||
@@ -89,10 +90,9 @@ export function renderE02StabGround(root: HTMLElement): void {
|
||||
const actions = el("div", { className: "ui-sidebar-actions" });
|
||||
actions.append(resetButton, saveButton);
|
||||
const panel = el("div", { className: "ui-sidebar-panel e02-panel" });
|
||||
panel.append(el("p", { className: "e02-hint", text: t("E02_Note") }), status, actions);
|
||||
panel.append(hook(createText(t("E02_Note")), "e02-hint"), status, actions);
|
||||
mountGroupStep(root, ROUTES.E02_STAB_GROUND, { mainContent: main, leftPanel: panel });
|
||||
if (!projectId)
|
||||
return void main.append(el("p", { className: "e02-empty", text: t("E02_NoProject") }));
|
||||
if (!projectId) return void main.append(hook(createText(t("E02_NoProject")), "e02-empty"));
|
||||
|
||||
let view: E02View | null = null;
|
||||
let refreshStations = (): void => {};
|
||||
@@ -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<HTMLElement>(".is-invalid")) {
|
||||
node.classList.remove("is-invalid");
|
||||
for (const node of main.querySelectorAll<HTMLElement>("[data-error]")) {
|
||||
node.classList.remove("is-error");
|
||||
node.removeAttribute("aria-invalid");
|
||||
delete node.dataset.error;
|
||||
}
|
||||
for (const error of errors) {
|
||||
const node = main.querySelector<HTMLElement>(`[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",
|
||||
),
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = <T extends HTMLElement>(node: T, name: string): T => (
|
||||
node.classList.add(name),
|
||||
node
|
||||
);
|
||||
export const clone = <T>(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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user