refactor(E07): 보기 고르기 · 범례 판 · 요약 값 목록 · 등급 표 · 측점 표 · 진행 막대 · 띠 알림을 부품으로 (PLAN 553-4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,17 @@ import {
|
||||
showToast,
|
||||
type InputFieldHandle,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { createLegend } from "@ui/ui_template_flow_legend";
|
||||
import { createKv } from "@ui/ui_template_kv";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { createProgressBar } from "@ui/ui_template_progress";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createTable } from "@ui/ui_template_table";
|
||||
import { createSidePanels } from "@ui/ui_template_side_panels";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import { createAreaMap, type MapLayer, type StationPoint } from "./E07_StabArea_UI_Map";
|
||||
import {
|
||||
@@ -107,11 +116,26 @@ const SCENARIOS = ["sinmap", "saturated"];
|
||||
const scenarioName = (value: unknown): string =>
|
||||
SCENARIOS.includes(String(value)) ? t(`E07_Scenario_${value}` as LocaleKey) : String(value ?? "");
|
||||
|
||||
/** 안내 글 한 줄 — 걸이 이름(`e07-hint` 같은)만 얹음 */
|
||||
function note(text: string, hook: string): HTMLElement {
|
||||
const node = createText(text, "muted");
|
||||
node.classList.add(hook);
|
||||
return node;
|
||||
}
|
||||
|
||||
function emptyBox(text: string): HTMLElement {
|
||||
const node = createEmpty({ text, small: true });
|
||||
node.classList.add("e07-empty");
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 지도 범례 — 떠 있는 판(머리 = 제목) 안에 범례 부품 */
|
||||
function legend(layer: MapLayer, scenario: unknown): HTMLElement {
|
||||
const box = el("div", { className: "e07-legend" });
|
||||
box.append(el("strong", { text: t(layer === "cls" ? "E07_Legend_Class" : "E07_Legend_FS") }));
|
||||
if (scenario)
|
||||
box.append(el("small", { className: "e07-legend__sub", text: scenarioName(scenario) }));
|
||||
const panel = createFloatPanel({
|
||||
title: t(layer === "cls" ? "E07_Legend_Class" : "E07_Legend_FS"),
|
||||
});
|
||||
panel.root.classList.add("e07-legend");
|
||||
if (scenario) panel.body.append(note(scenarioName(scenario), "e07-legend__sub"));
|
||||
const rows =
|
||||
layer === "cls"
|
||||
? CLASS_KEYS.map((k, i) => ({
|
||||
@@ -127,12 +151,8 @@ function legend(layer: MapLayer, scenario: unknown): HTMLElement {
|
||||
? `≥ ${FS_STEPS[i - 1].below.toFixed(1)}`
|
||||
: `${FS_STEPS[i - 1].below.toFixed(1)} – ${s.below.toFixed(1)}`,
|
||||
}));
|
||||
for (const row of rows) {
|
||||
const item = el("span", { className: "e07-legend__row" });
|
||||
item.append(swatch(row.color), el("span", { text: row.label }));
|
||||
box.append(item);
|
||||
}
|
||||
return box;
|
||||
panel.body.append(createLegend(rows, { vertical: true }));
|
||||
return panel.root;
|
||||
}
|
||||
|
||||
export function renderE07StabArea(root: HTMLElement): void {
|
||||
@@ -140,8 +160,7 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
const panel = el("div", { className: "ui-sidebar-panel e07-panel" });
|
||||
const main = el("div", { className: "e07-page" });
|
||||
mountGroupStep(root, ROUTES.E07_STAB_AREA, { mainContent: main, leftPanel: panel });
|
||||
if (!projectId)
|
||||
return void main.append(el("p", { className: "e07-empty", text: t("E07_NoProject") }));
|
||||
if (!projectId) return void main.append(emptyBox(t("E07_NoProject")));
|
||||
|
||||
// ---- 가운데 ----
|
||||
const map = createAreaMap();
|
||||
@@ -150,25 +169,21 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
const legendSlot = el("div", { className: "e07-legend-slot" });
|
||||
const paintLegend = (): void =>
|
||||
legendSlot.replaceChildren(legend(layer, overview?.result?.settings.scenario));
|
||||
const toggle = el("div", { className: "e07-toggle" });
|
||||
const layerButtons = (
|
||||
[
|
||||
["E07_Layer_Class", "cls"],
|
||||
["E07_Layer_FS", "fs"],
|
||||
] as [LocaleKey, MapLayer][]
|
||||
).map(([key, value], i) => {
|
||||
const button = createButton({ label: t(key), variant: "ghost", size: "sm" });
|
||||
button.dataset.value = value;
|
||||
button.classList.toggle("is-on", i === 0);
|
||||
button.addEventListener("click", () => {
|
||||
layerButtons.forEach((b) => b.classList.toggle("is-on", b === button));
|
||||
layer = value;
|
||||
const layerToggle = createSegment({
|
||||
options: [
|
||||
{ value: "cls", label: t("E07_Layer_Class") },
|
||||
{ value: "fs", label: t("E07_Layer_FS") },
|
||||
],
|
||||
value: "cls",
|
||||
size: 24,
|
||||
onChange: (value) => {
|
||||
layer = value as MapLayer;
|
||||
map.setLayer(layer);
|
||||
paintLegend();
|
||||
});
|
||||
return button;
|
||||
},
|
||||
});
|
||||
toggle.append(...layerButtons);
|
||||
const toggle = layerToggle.root;
|
||||
toggle.classList.add("e07-toggle");
|
||||
const stage = el("div", { className: "e07-stage" });
|
||||
stage.append(map.root, legendSlot);
|
||||
const center = el("div", { className: "e07-center" });
|
||||
@@ -203,7 +218,7 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
});
|
||||
cards.soil.append(material.root);
|
||||
cards.rain.append(scenario.root);
|
||||
const sourceHint = el("p", { className: "e07-hint", text: t("E07_Defaults_Source") });
|
||||
const sourceHint = note(t("E07_Defaults_Source"), "e07-hint");
|
||||
for (const [key, label, card] of NUMBER_FIELDS) {
|
||||
const field = createInputField({ label: t(label), type: "number", compact: true });
|
||||
field.input.step = "any";
|
||||
@@ -211,10 +226,10 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
fields.set(key, field);
|
||||
cards[card].append(field.root);
|
||||
}
|
||||
const progressBar = el("progress", { className: "e07-progress", attrs: { max: "100" } });
|
||||
const progressText = el("p", { className: "e07-progress__text" });
|
||||
const progressBar = createProgressBar();
|
||||
const progressText = note("", "e07-progress__text");
|
||||
const progressBox = el("div", { className: "e07-progress-box" });
|
||||
progressBox.append(progressBar, progressText);
|
||||
progressBox.append(progressBar.root, progressText);
|
||||
progressBox.hidden = true;
|
||||
const defaultsButton = createButton({
|
||||
label: t("E07_Defaults"),
|
||||
@@ -233,10 +248,10 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
});
|
||||
const actions = el("div", { className: "ui-sidebar-actions" });
|
||||
actions.append(defaultsButton, reanalyzeButton, analyzeButton);
|
||||
const readOnly = el("p", { className: "e07-hint", text: t("E07_ReadOnly") });
|
||||
const readOnly = note(t("E07_ReadOnly"), "e07-hint");
|
||||
readOnly.hidden = true;
|
||||
panel.append(
|
||||
el("p", { className: "e07-hint", text: t("E07_Hint") }),
|
||||
note(t("E07_Hint"), "e07-hint"),
|
||||
readOnly,
|
||||
cards.soil,
|
||||
cards.rain,
|
||||
@@ -304,11 +319,10 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
function paintResult(): void {
|
||||
const result = overview?.result;
|
||||
if (!result) {
|
||||
resultBody.replaceChildren(el("p", { className: "e07-empty", text: t("E07_Result_None") }));
|
||||
resultBody.replaceChildren(emptyBox(t("E07_Result_None")));
|
||||
return;
|
||||
}
|
||||
const s = result.summary;
|
||||
const list = el("dl", { className: "e07-summary" });
|
||||
const pairs: [LocaleKey, string][] = [
|
||||
["E07_Scenario", scenarioName(result.settings.scenario)],
|
||||
["E07_Sum_FsMin", s.fs_min === null ? "-" : s.fs_min.toFixed(3)],
|
||||
@@ -326,88 +340,76 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
`${result.elapsed_s.toFixed(1)} s · ${result.computed_at.replace("T", " ")}`,
|
||||
],
|
||||
];
|
||||
for (const [key, value] of pairs)
|
||||
list.append(
|
||||
el("dt", { text: t(key) }),
|
||||
el("dd", { text: value, attrs: { "data-field": key } }),
|
||||
);
|
||||
const areas = el("table", { className: "e07-table e07-areas" });
|
||||
const list = createKv(pairs.map(([key, value]) => ({ label: t(key), value })));
|
||||
list.classList.add("e07-summary");
|
||||
list.querySelectorAll("dd").forEach((dd, i) => dd.setAttribute("data-field", pairs[i][0]));
|
||||
const total = Object.values(s.areas_m2).reduce((a, b) => a + b, 0) || 1;
|
||||
areas.append(
|
||||
el("tr", {
|
||||
children: [
|
||||
el("th", { text: t("E07_Col_Class") }),
|
||||
el("th", { text: t("E07_Col_Area") }),
|
||||
el("th", { text: "%" }),
|
||||
],
|
||||
}),
|
||||
...CLASS_KEYS.map((k, i) => {
|
||||
const name = el("td");
|
||||
name.append(
|
||||
swatch(CLASS_COLORS[i]),
|
||||
el("span", { text: t(`E07_Class_${k}` as LocaleKey) }),
|
||||
);
|
||||
return el("tr", {
|
||||
attrs: { "data-class": k },
|
||||
children: [
|
||||
name,
|
||||
el("td", { text: Math.round(s.areas_m2[k] ?? 0).toLocaleString() }),
|
||||
el("td", { text: (((s.areas_m2[k] ?? 0) / total) * 100).toFixed(1) }),
|
||||
],
|
||||
});
|
||||
}),
|
||||
/** 색 칩 + 글 한 칸 */
|
||||
const chipCell = (color: string, text: string): Node => {
|
||||
const cell = document.createDocumentFragment();
|
||||
cell.append(swatch(color), el("span", { text }));
|
||||
return cell;
|
||||
};
|
||||
const areas = createTable(
|
||||
[
|
||||
{ label: t("E07_Col_Class") },
|
||||
{ label: t("E07_Col_Area"), align: "end" },
|
||||
{ label: "%", align: "end" },
|
||||
],
|
||||
CLASS_KEYS.map((k, i) => [
|
||||
chipCell(CLASS_COLORS[i], t(`E07_Class_${k}` as LocaleKey)),
|
||||
Math.round(s.areas_m2[k] ?? 0).toLocaleString(),
|
||||
(((s.areas_m2[k] ?? 0) / total) * 100).toFixed(1),
|
||||
]),
|
||||
{ density: "data" },
|
||||
);
|
||||
const table = el("table", { className: "e07-table e07-stations" });
|
||||
table.append(
|
||||
el("tr", {
|
||||
children: [
|
||||
el("th", { text: t("E07_Col_Station") }),
|
||||
el("th", { text: t("E07_Col_FsMin") }),
|
||||
el("th", { text: t("E07_Col_Ratio") }),
|
||||
el("th", { text: t("E07_Col_Level") }),
|
||||
el("th", { text: t("E07_Col_RiskArea") }),
|
||||
],
|
||||
}),
|
||||
...result.stations.map((r) => {
|
||||
const level = el("td");
|
||||
if (r.level)
|
||||
level.append(
|
||||
swatch(LEVEL_COLORS[r.level]),
|
||||
el("span", { text: t(`E07_Level_${r.level}` as LocaleKey) }),
|
||||
);
|
||||
const tr = el("tr", {
|
||||
className: r.level === "high" ? "is-risk" : "",
|
||||
attrs: { "data-chainage": String(r.chainage_m) },
|
||||
children: [
|
||||
el("td", { text: shortLabel(r.label, r.chainage_m) }),
|
||||
el("td", { text: r.fs_min === undefined ? "-" : r.fs_min.toFixed(2) }),
|
||||
el("td", {
|
||||
text: r.risk_ratio === undefined ? "-" : (r.risk_ratio * 100).toFixed(1),
|
||||
}),
|
||||
level,
|
||||
el("td", {
|
||||
text:
|
||||
r.risk_area_m2 === undefined ? "-" : Math.round(r.risk_area_m2).toLocaleString(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
tr.addEventListener("click", () => {
|
||||
table.querySelectorAll("tr.is-on").forEach((x) => x.classList.remove("is-on"));
|
||||
tr.classList.add("is-on");
|
||||
map.focus(r.chainage_m);
|
||||
});
|
||||
return tr;
|
||||
}),
|
||||
areas.querySelector("table")!.classList.add("e07-table", "e07-areas");
|
||||
areas
|
||||
.querySelectorAll("tbody tr")
|
||||
.forEach((tr, i) => ((tr as HTMLElement).dataset.class = CLASS_KEYS[i]));
|
||||
const stations = createTable(
|
||||
[
|
||||
{ label: t("E07_Col_Station") },
|
||||
{ label: t("E07_Col_FsMin"), align: "end" },
|
||||
{ label: t("E07_Col_Ratio"), align: "end" },
|
||||
{ label: t("E07_Col_Level") },
|
||||
{ label: t("E07_Col_RiskArea"), align: "end" },
|
||||
],
|
||||
result.stations.map((r) => [
|
||||
shortLabel(r.label, r.chainage_m),
|
||||
r.fs_min === undefined ? "-" : r.fs_min.toFixed(2),
|
||||
r.risk_ratio === undefined ? "-" : (r.risk_ratio * 100).toFixed(1),
|
||||
r.level ? chipCell(LEVEL_COLORS[r.level], t(`E07_Level_${r.level}` as LocaleKey)) : "",
|
||||
r.risk_area_m2 === undefined ? "-" : Math.round(r.risk_area_m2).toLocaleString(),
|
||||
]),
|
||||
{ density: "data" },
|
||||
);
|
||||
const table = stations.querySelector("table")!;
|
||||
table.classList.add("e07-table", "e07-stations");
|
||||
table.querySelectorAll("tbody tr").forEach((row, i) => {
|
||||
const tr = row as HTMLElement;
|
||||
const r = result.stations[i];
|
||||
tr.dataset.chainage = String(r.chainage_m);
|
||||
tr.classList.toggle("is-risk", r.level === "high");
|
||||
tr.addEventListener("click", () => {
|
||||
table.querySelectorAll("tr.is-on").forEach((x) => x.classList.remove("is-on", "is-active"));
|
||||
tr.classList.add("is-on", "is-active");
|
||||
map.focus(r.chainage_m);
|
||||
});
|
||||
});
|
||||
const notes: HTMLElement[] = [];
|
||||
if (result.stale) notes.push(el("p", { className: "e07-warn", text: t("E07_Result_Stale") }));
|
||||
notes.push(el("p", { className: "e07-note", text: t("E07_Result_Note") }));
|
||||
if (result.stale) {
|
||||
const warn = createBanner({ tone: "warning", text: t("E07_Result_Stale") });
|
||||
warn.classList.add("e07-warn");
|
||||
notes.push(warn);
|
||||
}
|
||||
notes.push(note(t("E07_Result_Note"), "e07-note"));
|
||||
resultBody.replaceChildren(
|
||||
list,
|
||||
el("h4", { text: t("E07_Areas_Title") }),
|
||||
areas,
|
||||
el("h4", { text: t("E07_Stations_Title") }),
|
||||
table,
|
||||
stations,
|
||||
...notes,
|
||||
);
|
||||
}
|
||||
@@ -430,8 +432,8 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
if (!p) break;
|
||||
running = p.running;
|
||||
progressBox.hidden = !p.running;
|
||||
progressBar.value = p.percent;
|
||||
progressText.textContent = `${t("E07_Running")} ${p.percent}% · ${p.message ?? ""}`;
|
||||
progressBar.set(p.percent / 100);
|
||||
progressText.replaceChildren(`${t("E07_Running")} ${p.percent}% · ${p.message ?? ""}`);
|
||||
paintButtons();
|
||||
if (!p.running) {
|
||||
if (p.error) showToast(p.error, "error");
|
||||
@@ -469,12 +471,7 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
try {
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
main.replaceChildren(
|
||||
el("p", {
|
||||
className: "e07-empty",
|
||||
text: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
main.replaceChildren(emptyBox(error instanceof Error ? error.message : String(error)));
|
||||
return;
|
||||
}
|
||||
const ov = overview!;
|
||||
|
||||
@@ -23,17 +23,6 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.e07-toggle {
|
||||
display: inline-flex;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.e07-toggle .ui-btn.is-on {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
.e07-stage {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
@@ -77,6 +66,7 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 판 · 범례 · 값 목록 · 표 · 띠 알림 · 진행 막대는 부품 — 여기는 자리만 */
|
||||
.e07-legend-slot {
|
||||
position: absolute;
|
||||
left: var(--spacing-8);
|
||||
@@ -84,27 +74,12 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.e07-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: var(--spacing-8);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: 12px;
|
||||
.e07-legend-slot .e07-legend {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.e07-legend__sub {
|
||||
max-width: 180px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.e07-legend__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.e07-chip {
|
||||
@@ -121,10 +96,7 @@
|
||||
/* 좌측 */
|
||||
.e07-hint,
|
||||
.e07-note {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.e07-panel .ui-field[hidden] {
|
||||
@@ -142,16 +114,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.e07-progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.e07-progress__text {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 우측 */
|
||||
.e07-result {
|
||||
padding: var(--spacing-12);
|
||||
@@ -163,74 +125,25 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.e07-summary {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--spacing-8) var(--spacing-12);
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.e07-summary dt {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.e07-summary dd {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.e07-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.e07-table th,
|
||||
.e07-table td {
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
text-align: right;
|
||||
.e07-result .ui-table th,
|
||||
.e07-result .ui-table td {
|
||||
padding: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.e07-table th:first-child,
|
||||
.e07-table td:first-child,
|
||||
.e07-stations td:nth-child(4) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.e07-table th {
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.e07-stations tr[data-chainage] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.e07-stations tr[data-chainage]:hover {
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.e07-stations tr.is-risk td {
|
||||
color: var(--color-danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.e07-stations tr.is-on {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.e07-warn {
|
||||
margin: var(--spacing-12) 0 0;
|
||||
color: var(--color-danger);
|
||||
font-size: 12px;
|
||||
margin-top: var(--spacing-12);
|
||||
}
|
||||
|
||||
.e07-empty {
|
||||
margin: var(--spacing-16);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
@@ -174,3 +174,31 @@ def test_e06_parts():
|
||||
):
|
||||
assert dead not in css, dead
|
||||
assert "is-invalid" not in ts["Joints"] + ts["Page"]
|
||||
|
||||
|
||||
def test_e07_parts():
|
||||
page = _src("E07_StabArea/E07_StabArea_UI_Page.ts")
|
||||
css = _src("E07_StabArea/E07_StabArea_UI_Style.css")
|
||||
assert 'el("table"' not in page and 'el("progress"' not in page and 'el("p"' not in page
|
||||
for part in (
|
||||
"createSegment(",
|
||||
"createFloatPanel(",
|
||||
"createLegend(",
|
||||
"createKv(",
|
||||
"createTable(",
|
||||
):
|
||||
assert part in page, part
|
||||
for part in ("createProgressBar()", "createBanner(", "createEmpty("):
|
||||
assert part in page, part
|
||||
assert (
|
||||
'toggle.classList.add("e07-toggle")' in page
|
||||
) # 촬영 장면이 `.e07-toggle button[data-value]` 로 잡음
|
||||
assert "data-chainage" in css or "dataset.chainage" in page # 줄 누름 = 지도 이동
|
||||
for dead in (
|
||||
".e07-toggle .ui-btn",
|
||||
".e07-legend__row",
|
||||
".e07-summary",
|
||||
".e07-table th",
|
||||
".e07-progress {",
|
||||
):
|
||||
assert dead not in css, dead
|
||||
|
||||
Reference in New Issue
Block a user