refactor(E06): 절리 표 · 측점 목록 · 모드 표 · 판정 램프 · 보기 고르기 · 범례 · 갱신 띠를 부품으로 (PLAN 553-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { e06Table, note } from "./E06_StabRock_UI_Parts";
|
||||
|
||||
export interface JointSet {
|
||||
id: string;
|
||||
@@ -55,7 +56,14 @@ export interface JointsView {
|
||||
rebuild: () => void; // 줄 수가 바뀜
|
||||
}
|
||||
|
||||
/** 숫자 칸 — 범위 밖 · 빈 칸(필수)이면 is-invalid · 바꾸면 set 부르고 change */
|
||||
/** 틀린 칸 표시 — 부품 오류 상태(`is-error` · aria-invalid) */
|
||||
function markInvalid(input: HTMLElement, bad: boolean): void {
|
||||
input.classList.toggle("is-error", bad);
|
||||
if (bad) input.setAttribute("aria-invalid", "true");
|
||||
else input.removeAttribute("aria-invalid");
|
||||
}
|
||||
|
||||
/** 숫자 칸 — 범위 밖 · 빈 칸(필수)이면 오류 표시 · 바꾸면 set 부르고 change */
|
||||
function numberInput(
|
||||
view: JointsView,
|
||||
value: number | null,
|
||||
@@ -64,17 +72,17 @@ function numberInput(
|
||||
opts: { optional?: boolean; step?: string; path: string },
|
||||
): HTMLInputElement {
|
||||
const [lo, hi] = view.defaults.limits[limit] ?? [-Infinity, Infinity];
|
||||
const input = el("input", {
|
||||
className: "e06-input",
|
||||
attrs: { type: "number", step: opts.step ?? "any", "data-path": opts.path },
|
||||
}) as HTMLInputElement;
|
||||
const input = createInputField({ type: "number", size: 24 }).input;
|
||||
input.classList.add("e06-input");
|
||||
input.step = opts.step ?? "any";
|
||||
input.dataset.path = opts.path;
|
||||
input.value = value === null ? "" : String(value);
|
||||
input.disabled = !view.editable;
|
||||
const check = (): boolean => {
|
||||
const raw = input.value.trim();
|
||||
const num = Number(raw);
|
||||
const ok = raw === "" ? Boolean(opts.optional) : Number.isFinite(num) && num >= lo && num <= hi;
|
||||
input.classList.toggle("is-invalid", !ok);
|
||||
markInvalid(input, !ok);
|
||||
input.title = ok ? "" : `${t("E06_Err_Range")} (${lo} ~ ${hi})`;
|
||||
return ok;
|
||||
};
|
||||
@@ -107,64 +115,61 @@ const COLUMNS: LocaleKey[] = [
|
||||
|
||||
function jointTable(view: JointsView): HTMLElement {
|
||||
const wrap = el("div", { className: "e06-table-wrap" });
|
||||
const table = el("table", { className: "e06-table" });
|
||||
const head = el("tr");
|
||||
for (const key of COLUMNS) head.append(el("th", { text: t(key) }));
|
||||
head.append(el("th"));
|
||||
table.append(el("thead"), el("tbody"));
|
||||
table.tHead!.append(head);
|
||||
view.doc.sets.forEach((row, k) => {
|
||||
const tr = el("tr", { attrs: { "data-id": row.id } });
|
||||
const name = el("input", {
|
||||
className: "e06-input e06-input--name",
|
||||
attrs: { type: "text", "data-path": `sets.${k}.name` },
|
||||
}) as HTMLInputElement;
|
||||
const sets = view.doc.sets;
|
||||
const fields: [keyof JointSet, string, boolean][] = [
|
||||
["from_m", "chainage_m", false],
|
||||
["to_m", "chainage_m", false],
|
||||
["dip_direction", "dip_direction", false],
|
||||
["dip", "dip", false],
|
||||
["phi", "phi", false],
|
||||
["c", "c", false],
|
||||
["persistence_m", "persistence_m", true],
|
||||
];
|
||||
const rows = sets.map((row, k) => {
|
||||
const name = createInputField({ size: 24 }).input;
|
||||
name.classList.add("e06-input", "e06-input--name");
|
||||
name.dataset.path = `sets.${k}.name`;
|
||||
name.value = row.name;
|
||||
name.disabled = !view.editable;
|
||||
name.addEventListener("input", () => {
|
||||
row.name = name.value;
|
||||
name.classList.toggle("is-invalid", !name.value.trim());
|
||||
markInvalid(name, !name.value.trim());
|
||||
view.change();
|
||||
});
|
||||
tr.append(el("td", { children: [name] }));
|
||||
const fields: [keyof JointSet, string, boolean][] = [
|
||||
["from_m", "chainage_m", false],
|
||||
["to_m", "chainage_m", false],
|
||||
["dip_direction", "dip_direction", false],
|
||||
["dip", "dip", false],
|
||||
["phi", "phi", false],
|
||||
["c", "c", false],
|
||||
["persistence_m", "persistence_m", true],
|
||||
];
|
||||
const cells: HTMLElement[] = [name];
|
||||
for (const [key, limit, optional] of fields) {
|
||||
const input = numberInput(
|
||||
view,
|
||||
row[key] as number | null,
|
||||
limit,
|
||||
(v) => ((row as unknown as Record<string, number | null>)[key] = v),
|
||||
{ optional, path: `sets.${k}.${key}` },
|
||||
cells.push(
|
||||
numberInput(
|
||||
view,
|
||||
row[key] as number | null,
|
||||
limit,
|
||||
(v) => ((row as unknown as Record<string, number | null>)[key] = v),
|
||||
{ optional, path: `sets.${k}.${key}` },
|
||||
),
|
||||
);
|
||||
tr.append(el("td", { children: [input] }));
|
||||
}
|
||||
const remove = createButton({
|
||||
label: "×",
|
||||
variant: "danger-ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
view.doc.sets.splice(k, 1);
|
||||
sets.splice(k, 1);
|
||||
view.rebuild();
|
||||
},
|
||||
});
|
||||
remove.title = t("E06_Remove");
|
||||
remove.disabled = !view.editable;
|
||||
tr.append(el("td", { children: [remove] }));
|
||||
table.tBodies[0].append(tr);
|
||||
cells.push(remove);
|
||||
return cells;
|
||||
});
|
||||
wrap.append(table);
|
||||
if (!view.doc.sets.length) wrap.append(el("p", { className: "e06-hint", text: t("E06_NoSets") }));
|
||||
const list = e06Table([...COLUMNS.map((key) => t(key)), ""], rows, "e06-table");
|
||||
list.rows.forEach((tr, k) => (tr.dataset.id = sets[k].id));
|
||||
wrap.append(list.root);
|
||||
if (!sets.length) wrap.append(note(t("E06_NoSets")));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/** 좁은 패널 설정 줄 — 설정 줄 부품(줄 52 · 큰 글)은 이 폭에 안 맞아 옛 꼴 그대로 */
|
||||
function settingRow(label: LocaleKey, control: HTMLElement): HTMLElement {
|
||||
const row = el("label", { className: "e06-setting" });
|
||||
row.append(el("span", { text: t(label) }), control);
|
||||
@@ -178,11 +183,12 @@ function select<T extends string>(
|
||||
set: (v: T) => void,
|
||||
path: string,
|
||||
): HTMLSelectElement {
|
||||
const box = el("select", {
|
||||
className: "e06-input",
|
||||
attrs: { "data-path": path },
|
||||
}) as HTMLSelectElement;
|
||||
for (const [v, key] of options) box.append(el("option", { text: t(key), attrs: { value: v } }));
|
||||
const box = createSelectField({
|
||||
options: options.map(([v, key]) => ({ value: v, text: t(key) })),
|
||||
size: 24,
|
||||
}).select;
|
||||
box.classList.add("e06-input");
|
||||
box.dataset.path = path;
|
||||
box.value = value;
|
||||
box.disabled = !view.editable;
|
||||
box.addEventListener("change", () => {
|
||||
@@ -241,7 +247,7 @@ export function buildJointCards(view: JointsView): HTMLElement[] {
|
||||
});
|
||||
add.disabled = !view.editable;
|
||||
setsCard.append(
|
||||
el("p", { className: "e06-hint", text: t("E06_JointsHint") }),
|
||||
note(t("E06_JointsHint")),
|
||||
jointTable(view),
|
||||
el("div", { className: "e06-row-actions", children: [add] }),
|
||||
);
|
||||
@@ -324,8 +330,8 @@ export function buildJointCards(view: JointsView): HTMLElement[] {
|
||||
path: "settings.bolt.plunge",
|
||||
}),
|
||||
),
|
||||
el("p", { className: "e06-hint", text: t("E06_SettingsHint") }),
|
||||
el("p", { className: "e06-hint", text: t("E06_MinBlockHint") }),
|
||||
note(t("E06_SettingsHint")),
|
||||
note(t("E06_MinBlockHint")),
|
||||
);
|
||||
return [setsCard, setCard];
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import "./E06_StabRock_UI_Style.css";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import {
|
||||
createButton,
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { createSidePanels } from "@ui/ui_template_side_panels";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import {
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
type StationEntry,
|
||||
} from "./E06_StabRock_UI_Result";
|
||||
import { drawStereonet, netLegend, type NetData, type NetMode } from "./E06_StabRock_UI_Stereonet";
|
||||
import { emptyBox, note, toggle } from "./E06_StabRock_UI_Parts";
|
||||
|
||||
interface RockResponse {
|
||||
joints: JointsDoc;
|
||||
@@ -76,33 +78,16 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return body;
|
||||
}
|
||||
|
||||
function toggle<T extends string>(options: [LocaleKey, T][], value: T, onPick: (v: T) => void) {
|
||||
const group = el("div", { className: "e06-toggle" });
|
||||
const buttons = options.map(([key, v]) => {
|
||||
const button = createButton({ label: t(key), variant: "ghost", size: "sm" });
|
||||
button.dataset.value = v;
|
||||
button.classList.toggle("is-on", v === value);
|
||||
button.addEventListener("click", () => {
|
||||
buttons.forEach((b) => b.classList.toggle("is-on", b === button));
|
||||
onPick(v);
|
||||
});
|
||||
return button;
|
||||
});
|
||||
group.append(...buttons);
|
||||
return group;
|
||||
}
|
||||
|
||||
export function renderE06StabRock(root: HTMLElement): void {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const panel = el("div", { className: "ui-sidebar-panel e06-panel" });
|
||||
const main = el("div", { className: "e06-page" });
|
||||
mountGroupStep(root, ROUTES.E06_STAB_ROCK, { mainContent: main, leftPanel: panel });
|
||||
if (!projectId)
|
||||
return void main.append(el("p", { className: "e06-empty", text: t("E06_NoProject") }));
|
||||
if (!projectId) return void main.append(emptyBox(t("E06_NoProject")));
|
||||
|
||||
// ---- 좌 ----
|
||||
const cards = el("div", { className: "e06-cards" });
|
||||
const status = el("p", { className: "e06-status" });
|
||||
const status = el("div", { className: "e06-status" });
|
||||
const saveButton = createButton({
|
||||
label: t("Common_Btn_Save"),
|
||||
variant: "ghost",
|
||||
@@ -121,11 +106,12 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
saveButton.disabled = resetButton.disabled = analyzeButton.disabled = true;
|
||||
const actions = el("div", { className: "ui-sidebar-actions" });
|
||||
actions.append(resetButton, saveButton, analyzeButton);
|
||||
panel.append(el("p", { className: "e06-hint", text: t("E06_Note") }), cards, status, actions);
|
||||
panel.append(note(t("E06_Note")), cards, status, actions);
|
||||
attachCollapsible(cards);
|
||||
|
||||
// ---- 가운데 · 우 ----
|
||||
const staleBar = el("div", { className: "e06-stale", text: t("E06_Stale") });
|
||||
const staleBar = createBanner({ tone: "warning", text: t("E06_Stale") });
|
||||
staleBar.classList.add("e06-stale");
|
||||
staleBar.hidden = true;
|
||||
const bandSlot = el("div");
|
||||
const title = el("strong", { className: "e06-title" });
|
||||
@@ -140,10 +126,10 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
projectionToggle,
|
||||
toggle<NetMode>(
|
||||
[
|
||||
["E06_View_All", "all"],
|
||||
["E06_Mode_planar", "planar"],
|
||||
["E06_Mode_wedge", "wedge"],
|
||||
["E06_Mode_toppling", "toppling"],
|
||||
[t("E06_View_All"), "all"],
|
||||
[t("E06_Mode_planar"), "planar"],
|
||||
[t("E06_Mode_wedge"), "wedge"],
|
||||
[t("E06_Mode_toppling"), "toppling"],
|
||||
],
|
||||
netMode,
|
||||
(v) => {
|
||||
@@ -178,14 +164,14 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
let active: string | null = null;
|
||||
let shown: StationResponse | null = null;
|
||||
|
||||
const invalidCount = (): number => cards.querySelectorAll(".is-invalid").length;
|
||||
const invalidCount = (): number => cards.querySelectorAll('[aria-invalid="true"]').length;
|
||||
const paintStatus = (): void => {
|
||||
const lines: string[] = [];
|
||||
if (view && !view.editable) lines.push(t("E06_ReadOnly"));
|
||||
if (dirty) lines.push(t("E06_Unsaved"));
|
||||
const bad = invalidCount();
|
||||
if (bad) lines.push(`${t("E06_Invalid")} ${bad}`);
|
||||
status.textContent = lines.join("\n");
|
||||
status.replaceChildren(...(lines.length ? [note(lines.join("\n"), "e06-status__text")] : []));
|
||||
status.classList.toggle("is-error", bad > 0);
|
||||
};
|
||||
|
||||
@@ -211,8 +197,8 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
projectionToggle.replaceChildren(
|
||||
toggle<Projection>(
|
||||
[
|
||||
["E06_Proj_Angle", "equal_angle"],
|
||||
["E06_Proj_Area", "equal_area"],
|
||||
[t("E06_Proj_Angle"), "equal_angle"],
|
||||
[t("E06_Proj_Area"), "equal_area"],
|
||||
],
|
||||
projection,
|
||||
(v) => {
|
||||
@@ -227,7 +213,7 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
legendSlot.replaceChildren(netLegend(netMode));
|
||||
if (!shown) {
|
||||
title.textContent = "";
|
||||
netSlot.replaceChildren(el("p", { className: "e06-empty", text: t("E06_PickStation") }));
|
||||
netSlot.replaceChildren(emptyBox(t("E06_PickStation")));
|
||||
return;
|
||||
}
|
||||
const e = shown.entry;
|
||||
@@ -247,7 +233,7 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
listSlot.replaceChildren(
|
||||
stations.length
|
||||
? buildList(stations, active, (id) => void select(id))
|
||||
: el("p", { className: "e06-hint", text: t(result ? "E06_NoRock" : "E06_NoResult") }),
|
||||
: note(t(result ? "E06_NoRock" : "E06_NoResult")),
|
||||
);
|
||||
detailSlot.replaceChildren(shown ? buildDetail(shown.entry) : el("span"));
|
||||
paintNet();
|
||||
@@ -346,7 +332,7 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
async function analyze(): Promise<void> {
|
||||
if (!view?.editable) return;
|
||||
analyzeButton.disabled = true;
|
||||
status.textContent = t("E06_Running");
|
||||
status.replaceChildren(note(t("E06_Running"), "e06-status__text"));
|
||||
try {
|
||||
if (dirty && !(await putJoints())) return;
|
||||
const body = await api<{ status: string; summary?: RockResult["summary"] }>(
|
||||
@@ -387,10 +373,7 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
await loadResult();
|
||||
} catch (error) {
|
||||
main.replaceChildren(
|
||||
el("p", {
|
||||
className: "e06-empty",
|
||||
text: error instanceof Error ? error.message : t("Common_Status_Error"),
|
||||
}),
|
||||
emptyBox(error instanceof Error ? error.message : t("Common_Status_Error")),
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/* =============================================================================
|
||||
* E06_StabRock_UI_Parts.ts
|
||||
* E06 안 부품 걸이(553-4) — 부품(보조 글 · 빈 자리 · 표 · 배지 · 분할 고르기)을 부르고
|
||||
* 촬영 장면 · 시험이 잡는 이름(`e06-hint` · `e06-empty` · `e06-table` · `e06-list` · `e06-cases` · `e06-verdict` · `e06-toggle`)만 얹음.
|
||||
* 모양은 부품 · 이 파일에 CSS 없음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createTable, type Cell } from "@ui/ui_template_table";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
|
||||
/** 보조 글 — 걸이 이름(`e06-hint` · `e06-kin` · `e06-status` …) */
|
||||
export function note(text: string, hook = "e06-hint"): HTMLElement {
|
||||
const node = createText(text, "muted");
|
||||
node.classList.add(hook);
|
||||
return node;
|
||||
}
|
||||
|
||||
export function emptyBox(text: string): HTMLElement {
|
||||
const node = createEmpty({ text, small: true });
|
||||
node.classList.add("e06-empty");
|
||||
return node;
|
||||
}
|
||||
|
||||
export interface E06Table {
|
||||
root: HTMLElement;
|
||||
table: HTMLTableElement;
|
||||
rows: HTMLTableRowElement[];
|
||||
}
|
||||
|
||||
/** 표 부품(데이터표) — `className` = 표에 얹는 걸이 이름 · `role` = data-role */
|
||||
export function e06Table(
|
||||
headers: string[],
|
||||
rows: Cell[][],
|
||||
className: string,
|
||||
role?: string,
|
||||
): E06Table {
|
||||
const root = createTable(
|
||||
headers.map((label) => ({ label })),
|
||||
rows,
|
||||
{ density: "data" },
|
||||
);
|
||||
const table = root.querySelector("table") as HTMLTableElement;
|
||||
table.classList.add(className);
|
||||
if (role) table.dataset.role = role;
|
||||
return { root, table, rows: [...table.tBodies[0].rows] };
|
||||
}
|
||||
|
||||
const TONE: Record<string, BadgeTone> = {
|
||||
ng: "danger",
|
||||
check: "warning",
|
||||
ok: "success",
|
||||
none: "neutral",
|
||||
};
|
||||
|
||||
/** 판정 램프 — verdict = NG · check · OK · none */
|
||||
export function verdictLamp(verdict: string, text: string): HTMLElement {
|
||||
const key = verdict.toLowerCase();
|
||||
const lamp = createBadge(text, TONE[key] ?? "neutral", { kind: "lamp" });
|
||||
lamp.classList.add("e06-verdict", `e06-verdict--${key}`);
|
||||
return lamp;
|
||||
}
|
||||
|
||||
/** 보기 고르기 — 단추 모양은 분할 고르기 부품 · `data-value` 는 그대로 */
|
||||
export function toggle<T extends string>(
|
||||
options: [string, T][],
|
||||
value: T,
|
||||
onPick: (v: T) => void,
|
||||
): HTMLElement {
|
||||
const group = createSegment({
|
||||
options: options.map(([label, v]) => ({ value: v, label })),
|
||||
value,
|
||||
size: 24,
|
||||
onChange: (v) => onPick(v as T),
|
||||
});
|
||||
group.root.classList.add("e06-toggle");
|
||||
return group.root;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { e06Table, note, verdictLamp } from "./E06_StabRock_UI_Parts";
|
||||
|
||||
export type Verdict = "NG" | "check" | "OK" | "none";
|
||||
|
||||
@@ -66,10 +67,7 @@ const fmt = (v: number | null | undefined, d = 2): string =>
|
||||
v === null || v === undefined ? "–" : v.toFixed(d);
|
||||
|
||||
function badge(verdict: Verdict): HTMLElement {
|
||||
return el("span", {
|
||||
className: `e06-verdict e06-verdict--${verdict.toLowerCase()}`,
|
||||
text: t(`E06_V_${verdict}` as LocaleKey),
|
||||
});
|
||||
return verdictLamp(verdict, t(`E06_V_${verdict}` as LocaleKey));
|
||||
}
|
||||
|
||||
/** 위험 띠 — 측점마다 가장 나쁜 판정 · 이웃 측점 가운데서 끊음 */
|
||||
@@ -119,7 +117,7 @@ export function buildBand(
|
||||
el("span", { text: `${hi.toFixed(0)} m` }),
|
||||
);
|
||||
box.append(legend, strip, ticks);
|
||||
} else box.append(legend, el("p", { className: "e06-hint", text: t("E06_NoRock") }));
|
||||
} else box.append(legend, note(t("E06_NoRock")));
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -128,34 +126,28 @@ export function buildList(
|
||||
active: string | null,
|
||||
onSelect: (id: string) => void,
|
||||
): HTMLElement {
|
||||
const table = el("table", { className: "e06-list", attrs: { "data-role": "station-list" } });
|
||||
const head = el("tr");
|
||||
for (const key of [
|
||||
"E06_L_Station",
|
||||
"E06_L_Side",
|
||||
"E06_L_Modes",
|
||||
"E06_L_MinF",
|
||||
"E06_L_Verdict",
|
||||
] as LocaleKey[])
|
||||
head.append(el("th", { text: t(key) }));
|
||||
table.append(el("thead", { children: [head] }), el("tbody"));
|
||||
for (const e of stations) {
|
||||
const heads = ["E06_L_Station", "E06_L_Side", "E06_L_Modes", "E06_L_MinF", "E06_L_Verdict"];
|
||||
const list = e06Table(
|
||||
heads.map((key) => t(key as LocaleKey)),
|
||||
stations.map((e) => [
|
||||
e.label ?? e.key,
|
||||
t(e.slope.side === "left" ? "E06_Left" : "E06_Right"),
|
||||
`${e.counts.planar} · ${e.counts.wedge} · ${e.counts.toppling}`,
|
||||
fmt(e.min_F),
|
||||
badge(e.verdict),
|
||||
]),
|
||||
"e06-list",
|
||||
"station-list",
|
||||
);
|
||||
list.rows.forEach((tr, i) => {
|
||||
const e = stations[i];
|
||||
const id = entryId(e);
|
||||
const tr = el("tr", {
|
||||
className: id === active ? "is-active" : "",
|
||||
attrs: { "data-id": id, "data-verdict": e.verdict },
|
||||
});
|
||||
tr.append(
|
||||
el("td", { text: e.label ?? e.key }),
|
||||
el("td", { text: t(e.slope.side === "left" ? "E06_Left" : "E06_Right") }),
|
||||
el("td", { text: `${e.counts.planar} · ${e.counts.wedge} · ${e.counts.toppling}` }),
|
||||
el("td", { text: fmt(e.min_F) }),
|
||||
el("td", { children: [badge(e.verdict)] }),
|
||||
);
|
||||
tr.dataset.id = id;
|
||||
tr.dataset.verdict = e.verdict;
|
||||
tr.classList.toggle("is-active", id === active);
|
||||
tr.addEventListener("click", () => onSelect(id));
|
||||
table.tBodies[0].append(tr);
|
||||
}
|
||||
return table;
|
||||
});
|
||||
return list.root;
|
||||
}
|
||||
|
||||
export function buildDetail(e: StationEntry): HTMLElement {
|
||||
@@ -163,15 +155,14 @@ export function buildDetail(e: StationEntry): HTMLElement {
|
||||
const s = e.slope;
|
||||
box.append(
|
||||
el("h4", { text: `${e.label ?? e.key} · ${t(s.side === "left" ? "E06_Left" : "E06_Right")}` }),
|
||||
el("p", {
|
||||
className: "e06-hint",
|
||||
text: t("E06_SlopeLine")
|
||||
note(
|
||||
t("E06_SlopeLine")
|
||||
.replace("{dd}", s.dip_direction.toFixed(0))
|
||||
.replace("{dip}", s.dip.toFixed(1))
|
||||
.replace("{h}", s.height_m.toFixed(2))
|
||||
.replace("{up}", s.upper_dip.toFixed(1))
|
||||
.replace("{g}", e.gamma_r.toFixed(1)),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const kin = e.kinematic;
|
||||
const kinText = [
|
||||
@@ -179,57 +170,47 @@ export function buildDetail(e: StationEntry): HTMLElement {
|
||||
`${t("E06_Mode_wedge")} ${kin.wedge.map((w) => `${w.ids.join("·")}(${w.trend.toFixed(0)}/${w.plunge.toFixed(0)})`).join(", ") || "–"}`,
|
||||
`${t("E06_Mode_toppling")} ${kin.toppling.join(", ") || "–"}`,
|
||||
];
|
||||
box.append(el("p", { className: "e06-kin", text: kinText.join("\n") }));
|
||||
box.append(note(kinText.join("\n"), "e06-kin"));
|
||||
if (!e.cases.length) {
|
||||
box.append(
|
||||
el("p", {
|
||||
className: "e06-hint",
|
||||
text: t(e.joints.length ? "E06_NoRisk" : "E06_NoJointsHere"),
|
||||
}),
|
||||
);
|
||||
box.append(note(t(e.joints.length ? "E06_NoRisk" : "E06_NoJointsHere")));
|
||||
return box;
|
||||
}
|
||||
const table = el("table", { className: "e06-cases" });
|
||||
const head = el("tr");
|
||||
for (const key of [
|
||||
const heads = [
|
||||
"E06_C_Mode",
|
||||
"E06_C_Joints",
|
||||
"E06_C_Cond",
|
||||
"E06_C_F",
|
||||
"E06_C_Req",
|
||||
"E06_L_Verdict",
|
||||
] as LocaleKey[])
|
||||
head.append(el("th", { text: t(key) }));
|
||||
table.append(el("thead", { children: [head] }), el("tbody"));
|
||||
for (const c of e.cases) {
|
||||
const verdict: Verdict = c.F === null ? "check" : c.ok === false ? "NG" : "OK";
|
||||
const note = !c.formed
|
||||
? t("E06_NotFormed")
|
||||
: c.small
|
||||
? t("E06_Small")
|
||||
: c.persistence_short
|
||||
? t("E06_Persist")
|
||||
: "";
|
||||
const tr = el("tr", { attrs: { "data-mode": c.mode, "data-cond": c.condition } });
|
||||
tr.append(
|
||||
el("td", { text: t(`E06_Mode_${c.mode}` as LocaleKey) }),
|
||||
el("td", { text: c.ids.join("·") }),
|
||||
el("td", { text: t(`E06_Cond_${c.condition}` as LocaleKey) }),
|
||||
el("td", { text: `${fmt(c.F, 3)}${note ? ` ${note}` : ""}` }),
|
||||
el("td", { text: fmt(c.required) }),
|
||||
el("td", { children: [c.small ? el("span", { text: "–" }) : badge(verdict)] }),
|
||||
);
|
||||
table.tBodies[0].append(tr);
|
||||
}
|
||||
box.append(table);
|
||||
if (e.small_blocks)
|
||||
box.append(
|
||||
el("p", {
|
||||
className: "e06-hint",
|
||||
text: t("E06_SmallCount").replace("{n}", String(e.small_blocks)),
|
||||
}),
|
||||
);
|
||||
if (kin.toppling.length)
|
||||
box.append(el("p", { className: "e06-hint", text: t("E06_ToppleNote") }));
|
||||
];
|
||||
const cases = e06Table(
|
||||
heads.map((key) => t(key as LocaleKey)),
|
||||
e.cases.map((c) => {
|
||||
const verdict: Verdict = c.F === null ? "check" : c.ok === false ? "NG" : "OK";
|
||||
const text = !c.formed
|
||||
? t("E06_NotFormed")
|
||||
: c.small
|
||||
? t("E06_Small")
|
||||
: c.persistence_short
|
||||
? t("E06_Persist")
|
||||
: "";
|
||||
return [
|
||||
t(`E06_Mode_${c.mode}` as LocaleKey),
|
||||
c.ids.join("·"),
|
||||
t(`E06_Cond_${c.condition}` as LocaleKey),
|
||||
`${fmt(c.F, 3)}${text ? ` ${text}` : ""}`,
|
||||
fmt(c.required),
|
||||
c.small ? "–" : badge(verdict),
|
||||
];
|
||||
}),
|
||||
"e06-cases",
|
||||
);
|
||||
cases.rows.forEach((tr, i) => {
|
||||
tr.dataset.mode = e.cases[i].mode;
|
||||
tr.dataset.cond = e.cases[i].condition;
|
||||
});
|
||||
box.append(cases.root);
|
||||
if (e.small_blocks) box.append(note(t("E06_SmallCount").replace("{n}", String(e.small_blocks))));
|
||||
if (kin.toppling.length) box.append(note(t("E06_ToppleNote")));
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 일광 엔벨로프(평면) · 사면 대원 = 굵은 선. 위험 판정된 절리 · 교선 = 테두리 강조.
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createLegend } from "@ui/ui_template_flow_legend";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
@@ -169,8 +169,19 @@ export function drawStereonet(net: NetData, mode: NetMode, marks: NetMarks): SVG
|
||||
return root;
|
||||
}
|
||||
|
||||
/** 범례 — 범례 부품(선 · 점선 · 칸 · 점) · 삼각(교선)만 걸이 모양 */
|
||||
const LEGEND_MARK: Record<string, { color: string; kind: "chip" | "line" | "dash" | "dot" }> = {
|
||||
slope: { color: "var(--color-text)", kind: "line" },
|
||||
friction: { color: "var(--color-text-secondary)", kind: "dash" },
|
||||
planar: { color: "color-mix(in srgb, var(--e06-planar) 35%, transparent)", kind: "chip" },
|
||||
daylight: { color: "var(--e06-planar)", kind: "dash" },
|
||||
wedge: { color: "color-mix(in srgb, var(--e06-wedge) 35%, transparent)", kind: "chip" },
|
||||
line: { color: "var(--e06-wedge)", kind: "chip" },
|
||||
toppling: { color: "color-mix(in srgb, var(--e06-toppling) 35%, transparent)", kind: "chip" },
|
||||
pole: { color: "var(--e06-pole)", kind: "dot" },
|
||||
};
|
||||
|
||||
export function netLegend(mode: NetMode): HTMLElement {
|
||||
const box = el("div", { className: "e06-legend" });
|
||||
const items: [string, LocaleKey][] = [
|
||||
["slope", "E06_Lg_Slope"],
|
||||
["friction", "E06_Lg_Friction"],
|
||||
@@ -181,13 +192,13 @@ export function netLegend(mode: NetMode): HTMLElement {
|
||||
items.push(["wedge", "E06_Lg_Wedge"], ["line", "E06_Lg_Line"]);
|
||||
if (mode === "all" || mode === "toppling") items.push(["toppling", "E06_Lg_Toppling"]);
|
||||
if (mode !== "wedge") items.push(["pole", "E06_Lg_Pole"]);
|
||||
for (const [cls, key] of items) {
|
||||
const row = el("span", { className: "e06-legend__row" });
|
||||
row.append(
|
||||
el("i", { className: `e06-legend__mark e06-legend__mark--${cls}` }),
|
||||
el("span", { text: t(key) }),
|
||||
);
|
||||
box.append(row);
|
||||
}
|
||||
const box = createLegend(
|
||||
items.map(([cls, key]) => ({ label: t(key), ...LEGEND_MARK[cls] })),
|
||||
{ vertical: true },
|
||||
);
|
||||
box.classList.add("e06-legend");
|
||||
box.querySelectorAll(".ui-legend__mark").forEach((mark, i) => {
|
||||
if (items[i][0] === "line") mark.classList.add("e06-legend__mark--line");
|
||||
});
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
--e06-planar: #d03b3b;
|
||||
--e06-wedge: #f1963b;
|
||||
--e06-toppling: #9b6dde;
|
||||
--e06-pole: #2f6fd6;
|
||||
height: 100dvh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -31,39 +32,21 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.e06-empty,
|
||||
/* 보조 글 · 빈 자리 · 띠 알림 · 표 · 설정 줄 · 판정 램프는 부품 — 여기는 자리만 */
|
||||
.e06-status__text,
|
||||
.e06-hint,
|
||||
.e06-status,
|
||||
.e06-kin {
|
||||
margin: 0;
|
||||
white-space: pre-line;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.e06-empty {
|
||||
padding: var(--spacing-16);
|
||||
}
|
||||
|
||||
.e06-status.is-error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.e06-kin {
|
||||
color: var(--color-text);
|
||||
margin: var(--spacing-16);
|
||||
}
|
||||
|
||||
.e06-panel .ui-sidebar-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.e06-stale {
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--color-warning) 18%, var(--color-surface));
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.e06-stale[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@@ -73,32 +56,8 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.e06-table {
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.e06-table th {
|
||||
padding: 2px 3px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.e06-table td {
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
.e06-input {
|
||||
width: 4.2em;
|
||||
padding: 2px 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.e06-input--name {
|
||||
@@ -109,11 +68,6 @@ select.e06-input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.e06-input.is-invalid {
|
||||
border-color: var(--color-danger);
|
||||
background: color-mix(in srgb, var(--color-danger) 12%, var(--color-surface));
|
||||
}
|
||||
|
||||
.e06-row-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -152,17 +106,6 @@ select.e06-input {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.e06-toggle {
|
||||
display: inline-flex;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.e06-toggle .ui-btn.is-on {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
.e06-stage {
|
||||
display: flex;
|
||||
gap: var(--spacing-12);
|
||||
@@ -289,58 +232,9 @@ select.e06-input {
|
||||
color: #0e7490;
|
||||
}
|
||||
|
||||
.e06-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.e06-legend__row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.e06-legend__mark {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.e06-legend__mark--slope {
|
||||
height: 3px;
|
||||
background: var(--color-text);
|
||||
}
|
||||
.e06-legend__mark--friction {
|
||||
height: 0;
|
||||
border-top: 1px dashed var(--color-text-secondary);
|
||||
}
|
||||
.e06-legend__mark--daylight {
|
||||
height: 0;
|
||||
border-top: 1px dashed var(--e06-planar);
|
||||
}
|
||||
.e06-legend__mark--planar {
|
||||
background: color-mix(in srgb, var(--e06-planar) 35%, transparent);
|
||||
}
|
||||
.e06-legend__mark--wedge {
|
||||
background: color-mix(in srgb, var(--e06-wedge) 35%, transparent);
|
||||
}
|
||||
.e06-legend__mark--toppling {
|
||||
background: color-mix(in srgb, var(--e06-toppling) 35%, transparent);
|
||||
}
|
||||
.e06-legend__mark--line {
|
||||
width: 10px;
|
||||
background: var(--e06-wedge);
|
||||
clip-path: polygon(50% 0, 100% 100%, 0 100%);
|
||||
}
|
||||
.e06-legend__mark--pole {
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
background: #2f6fd6;
|
||||
}
|
||||
|
||||
/* ---- 위험 띠 ---- */
|
||||
.e06-band {
|
||||
@@ -418,74 +312,25 @@ select.e06-input {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.e06-list,
|
||||
.e06-cases {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
color: var(--color-text);
|
||||
.e06-list-slot .ui-table-wrap {
|
||||
overflow: visible;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.e06-list th,
|
||||
.e06-cases th {
|
||||
.e06-list-slot th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 3px 4px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.e06-list td,
|
||||
.e06-cases td {
|
||||
padding: 3px 4px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
z-index: 1;
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.e06-list tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.e06-list tbody tr:hover {
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.e06-list tr.is-active {
|
||||
background: color-mix(in srgb, var(--color-primary) 16%, transparent);
|
||||
}
|
||||
|
||||
.e06-detail h4 {
|
||||
margin: 0 0 4px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.e06-verdict {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.e06-verdict::before {
|
||||
content: "";
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--e06-none);
|
||||
}
|
||||
|
||||
.e06-verdict--ng::before {
|
||||
background: var(--e06-ng);
|
||||
border-radius: 2px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.e06-verdict--ok::before {
|
||||
background: var(--e06-ok);
|
||||
}
|
||||
.e06-verdict--check::before {
|
||||
background: var(--e06-check);
|
||||
border-radius: 0;
|
||||
clip-path: polygon(50% 0, 100% 100%, 0 100%);
|
||||
}
|
||||
|
||||
@@ -145,3 +145,32 @@ def test_e05_parts():
|
||||
assert ".e05-stale[hidden]" in css # 띠 알림은 flex 라 hidden 을 덮음
|
||||
for dead in (".e05-preview {", ".e05-preview__head", ".e05-status.is-error"):
|
||||
assert dead not in css, dead
|
||||
|
||||
|
||||
def test_e06_parts():
|
||||
names = ["Joints", "Page", "Result", "Stereonet"]
|
||||
ts = {n: _src(f"E06_StabRock/E06_StabRock_UI_{n}.ts") for n in names}
|
||||
parts = _src("E06_StabRock/E06_StabRock_UI_Parts.ts")
|
||||
css = _src("E06_StabRock/E06_StabRock_UI_Style.css")
|
||||
for n, src in ts.items():
|
||||
assert 'el("table"' not in src and 'el("input"' not in src, n
|
||||
assert 'el("select"' not in src and 'el("p"' not in src, n
|
||||
for part in ("createTable(", "createBadge(", "createSegment(", "createText(", "createEmpty("):
|
||||
assert part in parts, part
|
||||
assert "createLegend(" in ts["Stereonet"] and "createBanner(" in ts["Page"]
|
||||
assert "createInputField(" in ts["Joints"] and "createSelectField(" in ts["Joints"]
|
||||
assert (
|
||||
'classList.add("e06-toggle")' in parts
|
||||
) # 촬영 장면이 `.e06-toggle button[data-value]` 로 잡음
|
||||
assert '"station-list"' in ts["Result"] # 촬영 장면 · 시험이 잡는 data-role
|
||||
for dead in (
|
||||
".e06-verdict",
|
||||
".e06-list th",
|
||||
".e06-cases",
|
||||
".e06-table th",
|
||||
".e06-legend__row",
|
||||
".e06-toggle .ui-btn",
|
||||
"is-invalid",
|
||||
):
|
||||
assert dead not in css, dead
|
||||
assert "is-invalid" not in ts["Joints"] + ts["Page"]
|
||||
|
||||
Reference in New Issue
Block a user