Merge remote-tracking branches 'origin/sub_laptop_3', 'origin/sub_laptop_4', 'origin/sub_laptop_5' and 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
/* ☰ [설정] 창(PLAN 332-2) — 탭 줄 · 휠 방향 고름 · 표 */
|
||||
/* ☰ [설정] 창(PLAN 332-2) — 탭 줄 = 부품 보조 탭 · 휠 방향 = 부품 라디오 · CAD 별칭 표 = 부품 표 */
|
||||
.app-settings .ui-text[hidden] {
|
||||
display: none; /* 부품 보조 글은 display:flex 라 hidden 을 이김 */
|
||||
}
|
||||
|
||||
.app-settings__tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
padding-bottom: var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.app-settings__radios {
|
||||
@@ -13,25 +14,13 @@
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
.app-settings__radios > label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
.app-settings__radios > .ui-checkbox {
|
||||
min-height: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-settings__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.app-settings__table th,
|
||||
.app-settings__table td {
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
/* 표 머리는 창 몸통을 따라 붙음 — 부품 표 겉틀의 가로 스크롤은 풂 */
|
||||
.app-settings .ui-table-wrap {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.app-settings__table thead th {
|
||||
@@ -40,18 +29,6 @@
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.app-settings__table input[aria-invalid="true"] {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.app-settings__error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.app-settings__error:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-settings__rules {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
padding: 0;
|
||||
@@ -77,14 +54,3 @@
|
||||
.app-settings__keys:only-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.app-settings__keys > kbd {
|
||||
padding: 0 var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.85em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
+20
-25
@@ -7,12 +7,15 @@
|
||||
* - 로그인 전엔 이 브라우저에만 남음(계정 올리기는 `b_page_state` 가 로그인 뒤에)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { askLeave } from "@ui/ui_template_leave_dialog";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { writeState } from "./b_page_state";
|
||||
import { inputPrefs, type InputPrefs, type WheelZoom } from "./input_prefs";
|
||||
import { fillTabs } from "./b_page_tabs";
|
||||
import { buildCadAliasTab, checkCadAliases } from "./app_settings_cad";
|
||||
import "./app_settings.css";
|
||||
|
||||
@@ -66,7 +69,7 @@ function ruleList(keys: LocaleKey[]): HTMLElement {
|
||||
children: [
|
||||
el("span", {
|
||||
className: "app-settings__keys",
|
||||
children: input.split(" · ").map((text) => el("kbd", { text })),
|
||||
children: input.split(" · ").map((text) => createBadge(text, "neutral")),
|
||||
}),
|
||||
...(at < 0 ? [] : [el("span", { text: line.slice(at + 3) })]),
|
||||
],
|
||||
@@ -81,12 +84,14 @@ function mouseTab(draft: InputPrefs): HTMLElement {
|
||||
attrs: { role: "radiogroup", "aria-label": L("App_Settings_Wheel") },
|
||||
});
|
||||
for (const [mode, key] of WHEEL) {
|
||||
const input = el("input", {
|
||||
attrs: { type: "radio", name: "app-settings-wheel", value: mode },
|
||||
});
|
||||
input.checked = draft.wheelZoom === mode;
|
||||
input.addEventListener("change", () => (draft.wheelZoom = mode));
|
||||
group.append(el("label", { children: [input, L(key)] }));
|
||||
group.append(
|
||||
createCheckbox(L(key), {
|
||||
radio: "app-settings-wheel",
|
||||
value: mode,
|
||||
checked: draft.wheelZoom === mode,
|
||||
onChange: () => (draft.wheelZoom = mode),
|
||||
}).root,
|
||||
);
|
||||
}
|
||||
return el("div", {
|
||||
children: [
|
||||
@@ -114,25 +119,15 @@ export function openAppSettings(): void {
|
||||
cad: cad.root,
|
||||
keys: keysTab(),
|
||||
};
|
||||
const tabs = el("div", {
|
||||
className: "app-settings__tabs ui-subtabs",
|
||||
attrs: { role: "tablist" },
|
||||
});
|
||||
const tabs = el("div", { className: "app-settings__tabs" });
|
||||
const show = (id: TabId): void => {
|
||||
tabs.replaceChildren(
|
||||
...TABS.map(([tab, key]) => {
|
||||
const button = createButton({
|
||||
label: L(key),
|
||||
variant: tab === id ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => show(tab),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(tab === id));
|
||||
button.dataset.settingsTab = tab;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
fillTabs(tabs, {
|
||||
tabs: TABS.map(([tab, key]) => ({ key: tab, label: L(key) })),
|
||||
value: id,
|
||||
kind: "pill",
|
||||
data: "settingsTab",
|
||||
onChange: (tab) => show(tab as TabId),
|
||||
});
|
||||
for (const [tab, panel] of Object.entries(panels)) panel.hidden = tab !== id;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import catalog from "../B07_DesignDetail/openwebcad/src/commands/command-catalog.json";
|
||||
import { setUiSize } from "@ui/ui_template_button";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createTable } from "@ui/ui_template_table";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
|
||||
export interface CadCommandRow {
|
||||
@@ -67,13 +70,15 @@ export function buildCadAliasTab(aliases: Record<string, string[]>): {
|
||||
root: HTMLElement;
|
||||
mark: (errors: Record<string, AliasError>) => void;
|
||||
} {
|
||||
const message = el("p", { className: "app-settings__error", attrs: { role: "alert" } });
|
||||
const message = createText("", "error");
|
||||
message.hidden = true;
|
||||
const inputs = new Map<string, HTMLInputElement>();
|
||||
const rows = CAD_COMMANDS.map((command) => {
|
||||
const input = el("input", {
|
||||
className: "ui-input ui-input--sm",
|
||||
className: "ui-input",
|
||||
attrs: { type: "text", "aria-label": `${command.id} ${L("App_Settings_Cad_Mine")}` },
|
||||
});
|
||||
setUiSize(input, 24);
|
||||
input.value = (aliases[command.id] ?? []).join(", ");
|
||||
input.dataset.cadCommand = command.id;
|
||||
input.addEventListener("input", () => {
|
||||
@@ -82,25 +87,21 @@ export function buildCadAliasTab(aliases: Record<string, string[]>): {
|
||||
else delete aliases[command.id];
|
||||
});
|
||||
inputs.set(command.id, input);
|
||||
const cells = [command.id, command.label, command.aliases.join(", ")].map((text) =>
|
||||
el("td", { text }),
|
||||
);
|
||||
return el("tr", { children: [...cells, el("td", { children: [input] })] });
|
||||
return [command.id, command.label, command.aliases.join(", "), input];
|
||||
});
|
||||
const head = el("tr", {
|
||||
children: (
|
||||
const table = createTable(
|
||||
(
|
||||
[
|
||||
"App_Settings_Cad_Command",
|
||||
"App_Settings_Cad_Label",
|
||||
"App_Settings_Cad_Default",
|
||||
"App_Settings_Cad_Mine",
|
||||
] as const
|
||||
).map((key) => el("th", { text: L(key) })),
|
||||
});
|
||||
const table = el("table", {
|
||||
className: "app-settings__table",
|
||||
children: [el("thead", { children: [head] }), el("tbody", { children: rows })],
|
||||
});
|
||||
).map((key) => ({ label: L(key) })),
|
||||
rows,
|
||||
{ density: "data" },
|
||||
);
|
||||
table.classList.add("app-settings__table");
|
||||
const mark = (errors: Record<string, AliasError>): void => {
|
||||
for (const [id, input] of inputs) {
|
||||
const error = errors[id];
|
||||
@@ -109,7 +110,8 @@ export function buildCadAliasTab(aliases: Record<string, string[]>): {
|
||||
input.title = error ? L(ERROR_TEXT[error]) : "";
|
||||
}
|
||||
const kinds = [...new Set(Object.values(errors))];
|
||||
message.textContent = kinds.map((k) => L(ERROR_TEXT[k])).join(" · ");
|
||||
message.lastChild!.textContent = kinds.map((k) => L(ERROR_TEXT[k])).join(" · ");
|
||||
message.hidden = !kinds.length;
|
||||
};
|
||||
const root = el("div", {
|
||||
children: [el("p", { text: L("App_Settings_Cad_Help") }), message, table],
|
||||
|
||||
+14
-28
@@ -9,6 +9,7 @@
|
||||
|
||||
import { ui_locales, currentLanguageIndex, setLanguage } from "@ui/ui_template_locale";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { THEME_STORAGE_KEY, LANG_STORAGE_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { navigateTo, isAuthenticated, setLeaveGuard, takeLeaveGuard } from "./router";
|
||||
import { readTouchMode, setTouchMode, type TouchMode } from "./device_context";
|
||||
@@ -246,35 +247,23 @@ function buildTouchModeSwitch(): HTMLElement {
|
||||
box.className = "app-drawer__touch";
|
||||
const label = document.createElement("span");
|
||||
label.textContent = L("App_TouchMode");
|
||||
const group = document.createElement("div");
|
||||
group.className = "app-drawer__row";
|
||||
group.setAttribute("role", "radiogroup");
|
||||
group.setAttribute("aria-label", L("App_TouchMode"));
|
||||
const modes: [TouchMode, keyof typeof ui_locales][] = [
|
||||
["auto", "App_TouchMode_Auto"],
|
||||
["on", "App_TouchMode_On"],
|
||||
["off", "App_TouchMode_Off"],
|
||||
];
|
||||
const buttons = modes.map(([mode, key]) => {
|
||||
const b = createButton({
|
||||
label: L(key),
|
||||
variant: "pill",
|
||||
onClick: () => {
|
||||
setTouchMode(mode);
|
||||
mark();
|
||||
},
|
||||
});
|
||||
b.setAttribute("role", "radio");
|
||||
b.dataset.touchMode = mode;
|
||||
return b;
|
||||
// 부품 「분할 고르기」(36) — 칸마다 data-touch-mode
|
||||
const group = createSegment({
|
||||
options: modes.map(([mode, key]) => ({ value: mode, label: L(key) })),
|
||||
value: readTouchMode(),
|
||||
size: 36,
|
||||
ariaLabel: L("App_TouchMode"),
|
||||
onChange: (mode) => setTouchMode(mode as TouchMode),
|
||||
});
|
||||
const mark = (): void => {
|
||||
const now = readTouchMode();
|
||||
for (const b of buttons) b.setAttribute("aria-checked", String(b.dataset.touchMode === now));
|
||||
};
|
||||
mark();
|
||||
group.append(...buttons);
|
||||
box.append(label, group);
|
||||
group.root.querySelectorAll<HTMLElement>("[data-value]").forEach((b) => {
|
||||
b.dataset.touchMode = b.dataset.value;
|
||||
});
|
||||
box.append(label, group.root);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -522,11 +511,8 @@ html[data-touch="on"] :is(.app-drawer__nav > *, .app-drawer__site > summary, .ap
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
.app-drawer__touch .app-drawer__row { padding-inline: 0; }
|
||||
.app-drawer__touch [aria-checked="true"] {
|
||||
background: var(--color-mist-violet);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.app-drawer__touch .ui-segment { display: flex; }
|
||||
.app-drawer__touch .ui-segment__item { flex: 1 1 0; min-width: 0; }
|
||||
/* 휴대폰 폭 — 오른쪽 단추(언어 · 테마 · 로그인)는 서랍 안으로 */
|
||||
html[data-size="compact"] .app-header { padding: 0 var(--spacing-16); gap: var(--spacing-8); }
|
||||
html[data-size="compact"] .app-actions { display: none; }
|
||||
|
||||
@@ -105,7 +105,8 @@ export function buildCutSlopeControl(section: CrossSection, control: CutSlopeCon
|
||||
|
||||
const reset = document.createElement("button");
|
||||
reset.type = "button";
|
||||
reset.className = "b06-design__rockb-btn is-reset";
|
||||
reset.className = "ui-icon-btn ui-icon-btn--ghost b06-design__rockb-btn is-reset"; // 부품 아이콘 단추 22
|
||||
reset.setAttribute("data-size", "22");
|
||||
reset.textContent = "↺";
|
||||
reset.title = `표준값으로 되돌리기 (1:${standard.toFixed(2)} · ${ratioToDegrees(standard).toFixed(1)}°)`;
|
||||
reset.disabled = sameRatio(ratio, standard);
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
* 병합된다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, setUiSize } from "@ui/ui_template_button";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createToggle } from "@ui/ui_template_toggle";
|
||||
import type {
|
||||
CrossSection,
|
||||
DitchSide,
|
||||
@@ -156,25 +159,19 @@ function segment<T extends string>(
|
||||
legendEl.textContent = legend;
|
||||
wrap.append(legendEl);
|
||||
}
|
||||
const group = document.createElement("div");
|
||||
group.className = "b06-design__seg-buttons";
|
||||
for (const [value, labelKey] of options) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = `b06-design__btn${value === selected ? " b06-design__btn--active" : ""}`;
|
||||
button.textContent = L(labelKey);
|
||||
button.setAttribute("aria-pressed", value === selected ? "true" : "false");
|
||||
// 선행 조건 미충족 시 비활성(상시 노출하되 클릭 불가·사유 툴팁) — E-6.
|
||||
button.disabled = disabled;
|
||||
if (disabled && reason) button.title = reason;
|
||||
else button.addEventListener("click", () => onPick(value));
|
||||
group.append(button);
|
||||
}
|
||||
// 부품 분할 고르기 20(553-2) — 선행 조건 미충족 시 비활성(클릭 불가·사유 툴팁) — E-6.
|
||||
const group = createSegment({
|
||||
options: options.map(([value, labelKey]) => ({ value, label: L(labelKey), disabled })),
|
||||
value: selected ?? "",
|
||||
size: 20,
|
||||
onChange: (value) => onPick(value as T),
|
||||
}).root;
|
||||
if (disabled && reason) group.title = reason;
|
||||
wrap.append(group);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/** 온/오프 토글 하나(포장·2단계 경사 공용). 세그먼트와 같은 컨테이너/버튼 스타일을 쓴다. */
|
||||
/** 온/오프 토글 하나(포장·2단계 경사 공용) — 부품 켜기 칩 20(553-2). */
|
||||
function toggle(
|
||||
legend: string,
|
||||
on: boolean,
|
||||
@@ -192,18 +189,16 @@ function toggle(
|
||||
legendEl.textContent = legend;
|
||||
wrap.append(legendEl);
|
||||
}
|
||||
const buttons = document.createElement("div");
|
||||
buttons.className = "b06-design__seg-buttons";
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = `b06-design__btn${on ? " b06-design__btn--active" : ""}`;
|
||||
button.textContent = on ? onLabel : offLabel;
|
||||
button.setAttribute("aria-pressed", on ? "true" : "false");
|
||||
button.disabled = disabled;
|
||||
const button = createToggle({
|
||||
kind: "chip",
|
||||
label: on ? onLabel : offLabel,
|
||||
checked: on,
|
||||
size: 20,
|
||||
disabled,
|
||||
onChange: onToggle,
|
||||
}).root;
|
||||
if (disabled && reason) button.title = reason;
|
||||
else button.addEventListener("click", onToggle);
|
||||
buttons.append(button);
|
||||
wrap.append(buttons);
|
||||
wrap.append(button);
|
||||
return { wrap, button };
|
||||
}
|
||||
|
||||
@@ -375,18 +370,26 @@ export function buildDesignControls(
|
||||
const more = document.createElement("details");
|
||||
more.className = "b06-design__more";
|
||||
const moreSummary = document.createElement("summary");
|
||||
moreSummary.className = "b06-design__more-summary";
|
||||
// 부품 아이콘 단추(선) 22 꼴 — 펼침 = 눌림(aria-pressed)
|
||||
moreSummary.className = "ui-icon-btn ui-icon-btn--outline b06-design__more-summary";
|
||||
setUiSize(moreSummary, 22);
|
||||
moreSummary.textContent = "⋯";
|
||||
moreSummary.title = L("B06_Design_More");
|
||||
moreSummary.setAttribute("aria-pressed", "false");
|
||||
more.addEventListener("toggle", () =>
|
||||
moreSummary.setAttribute("aria-pressed", String(more.open)),
|
||||
);
|
||||
const morePanel = document.createElement("div");
|
||||
morePanel.className = "b06-design__more-panel";
|
||||
more.append(moreSummary, morePanel);
|
||||
// [상세](138-4) — 맨 오른쪽 · 넘침 정리(moveable) 밖이라 늘 보임
|
||||
const detail = document.createElement("button");
|
||||
detail.type = "button";
|
||||
detail.className = "b06-design__btn b06-design__detail";
|
||||
detail.textContent = L("B06_Design_Detail");
|
||||
detail.addEventListener("click", () => openCrossDetail(section));
|
||||
const detail = createButton({
|
||||
label: L("B06_Design_Detail"),
|
||||
variant: "ghost",
|
||||
size: 20,
|
||||
onClick: () => openCrossDetail(section),
|
||||
});
|
||||
detail.classList.add("b06-design__detail");
|
||||
bar.append(slopeDirSeg, ...moveable, more, detail);
|
||||
refreshCrossDetail(section);
|
||||
|
||||
|
||||
@@ -87,7 +87,9 @@ export function buildLineCell(): LineCell {
|
||||
const buttons = SLOTS.map(([slot, text]) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = `b06-design__rockb-btn ${CELL_BTN}${slot}`;
|
||||
// 부품 아이콘 단추 22(553-2) — 클래스 · data-size 계약(시험 흉내 DOM 이 @ui 를 안 풂)
|
||||
button.className = `ui-icon-btn ui-icon-btn--ghost b06-design__rockb-btn ${CELL_BTN}${slot}`;
|
||||
button.setAttribute("data-size", "22");
|
||||
if (slot === "reset") button.classList.add("is-reset");
|
||||
button.textContent = text;
|
||||
// 길게 누름 — 누르면 즉시 1회 · 반복 예약 · 이어지는 click 은 삼킴
|
||||
|
||||
@@ -414,29 +414,7 @@
|
||||
stroke-width: 8;
|
||||
}
|
||||
|
||||
/* 암 경계선 상/하/리셋 제어 (B05 측점 선 제어 ▲/▼/↺ 버튼 패턴 재활용) */
|
||||
.b06-design__rockb-btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-surface);
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-design__rockb-btn:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.b06-design__rockb-btn:hover {
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
/* 암 경계선 상/하/리셋 제어 — 단추 = 부품 아이콘 단추 22(553-2) · ↺ 만 경고 글색 */
|
||||
.b06-design__rockb-btn.is-reset {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
@@ -206,19 +206,8 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ⋯ = 부품 아이콘 단추(선) 22 — 펼침 표시만 지움 */
|
||||
.b06-design__more-summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 22px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-inputs);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@@ -226,12 +215,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.b06-design__more[open] .b06-design__more-summary {
|
||||
color: var(--color-surface);
|
||||
background: var(--color-royal-amethyst);
|
||||
border-color: var(--color-royal-amethyst);
|
||||
}
|
||||
|
||||
.b06-design__more-panel {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -254,7 +237,7 @@
|
||||
}
|
||||
|
||||
.b06-design__seg-legend {
|
||||
font-size: 0.72rem;
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -271,31 +254,11 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.b06-design__btn {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-surface);
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b06-design__btn:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.b06-design__btn--active {
|
||||
color: var(--color-surface);
|
||||
background: var(--color-royal-amethyst);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.b06-design__areas {
|
||||
display: inline-flex;
|
||||
gap: var(--spacing-8);
|
||||
margin-left: auto;
|
||||
font-size: 0.72rem;
|
||||
font-size: var(--text-caption);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
@@ -329,13 +292,11 @@
|
||||
|
||||
.b06-cross-card__linecell .b06-design__rockb-btn {
|
||||
flex: 0 0 auto;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.b06-cross-card__linecell
|
||||
.b06-design__rockb-btn:not([hidden])
|
||||
~ .b06-design__rockb-btn:not([hidden]) {
|
||||
border-left: 1px solid var(--color-border);
|
||||
/* 부품 아이콘 단추의 display 가 hidden 을 덮음 — 상태에 없는 칸은 숨김 */
|
||||
.b06-design__rockb-btn[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 측점별 암 절토 경사(2026-09-07) — 행의 **오른쪽 끝**(암 경계 = 왼쪽). */
|
||||
@@ -349,7 +310,7 @@
|
||||
.b06-design__cutslope-input {
|
||||
width: 44px;
|
||||
padding: 0 2px;
|
||||
font-size: 0.72rem;
|
||||
font-size: var(--text-caption);
|
||||
font-family: var(--font-mono);
|
||||
text-align: right;
|
||||
color: var(--color-text);
|
||||
@@ -367,7 +328,7 @@
|
||||
|
||||
.b06-design__cutslope-unit {
|
||||
padding: 0 2px 0 1px;
|
||||
font-size: 0.72rem;
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-secondary);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@
|
||||
--cad-text-dim: var(--color-text-secondary);
|
||||
--cad-text-muted: var(--color-text-muted);
|
||||
--cad-accent: var(--color-accent);
|
||||
/* 마우스 바탕 = 부품 상자와 같은 값 · 켜짐 바탕은 --color-accent-soft */
|
||||
--cad-hover: var(--color-hover);
|
||||
/* 켜짐 바탕 = 앱 켜기 꼴(553-6) */
|
||||
--cad-on: var(--color-accent-soft);
|
||||
--cad-canvas: var(--draw-canvas);
|
||||
}
|
||||
|
||||
@@ -113,21 +112,12 @@ body > canvas[data-id="canvas"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
/* 접기 단추 = 아이콘 단추 부품(단 20) · 여기는 자리만 */
|
||||
.cad-ribbon-tabs__collapse {
|
||||
width: 24px;
|
||||
height: 20px;
|
||||
align-self: center;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
.cad-ribbon-tabs__collapse:hover {
|
||||
background: var(--cad-hover);
|
||||
}
|
||||
.cad-ribbon-tabs button {
|
||||
/* 리본 탭 = CAD 고유 꼴 그대로(토큰만) */
|
||||
.cad-ribbon-tabs > button:not(.ui-icon-btn) {
|
||||
padding: 0 14px;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: 0;
|
||||
@@ -136,10 +126,10 @@ body > canvas[data-id="canvas"] {
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
.cad-ribbon-tabs button:hover {
|
||||
.cad-ribbon-tabs > button:not(.ui-icon-btn):hover {
|
||||
background: var(--cad-hover);
|
||||
}
|
||||
.cad-ribbon-tabs button[data-active="true"] {
|
||||
.cad-ribbon-tabs > button[data-active="true"] {
|
||||
background: var(--cad-chrome);
|
||||
border-color: var(--cad-line);
|
||||
color: var(--color-text);
|
||||
@@ -187,53 +177,25 @@ body > canvas[data-id="canvas"] {
|
||||
.cad-ribbon-group__label[data-expandable="true"]:hover {
|
||||
color: var(--cad-accent);
|
||||
}
|
||||
/* 꼴 = 메뉴 부품(`ui-ctx-menu`) · 여기는 자리만 */
|
||||
.cad-ribbon-overflow {
|
||||
/* 리본 패널 줄이 가로 스크롤 상자라 그 안에서는 잘린다 — 화면 좌표로 띄운다 */
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 170px;
|
||||
max-height: 72vh;
|
||||
padding: 4px;
|
||||
overflow-y: auto;
|
||||
background: var(--cad-chrome-raised);
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: var(--radius-cards);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.cad-ribbon-overflow button {
|
||||
.cad-ribbon-overflow > .ui-ctx-menu__item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 5px 8px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
}
|
||||
.cad-ribbon-overflow button:hover {
|
||||
background: var(--cad-hover);
|
||||
}
|
||||
/* ↶ ↷ = 아이콘 단추 부품(단 20) · 여기는 자리만 */
|
||||
.cad-quick-access {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-self: center;
|
||||
}
|
||||
.cad-ribbon-tabs .cad-quick-access button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
font-size: 13px;
|
||||
}
|
||||
.cad-quick-access button:hover {
|
||||
border-color: var(--cad-line);
|
||||
background: var(--cad-hover);
|
||||
}
|
||||
.cad-ribbon-props {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -254,6 +216,7 @@ body > canvas[data-id="canvas"] {
|
||||
width: 52px;
|
||||
text-align: left;
|
||||
}
|
||||
/* 리본 안 칸 = CAD 고유 꼴(토큰만) · 높이 = 크기 단 18 · 둥글기 4 */
|
||||
.cad-prop select,
|
||||
.cad-prop input[type="number"] {
|
||||
height: 18px;
|
||||
@@ -285,7 +248,6 @@ input[type="color"][data-mixed="true"]::-webkit-color-swatch { background: trans
|
||||
.cad-ribbon-props .cad-tool[data-size="small"] {
|
||||
height: 22px;
|
||||
min-width: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cad-tool {
|
||||
display: flex;
|
||||
@@ -296,7 +258,7 @@ input[type="color"][data-mixed="true"]::-webkit-color-swatch { background: trans
|
||||
min-width: 48px;
|
||||
padding: 3px 6px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-buttons);
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
@@ -331,7 +293,7 @@ input[type="color"][data-mixed="true"]::-webkit-color-swatch { background: trans
|
||||
}
|
||||
.cad-tool[data-active="true"] {
|
||||
border-color: var(--cad-accent);
|
||||
background: var(--cad-on);
|
||||
background: var(--color-accent-soft);
|
||||
box-shadow: inset 0 -2px var(--cad-accent);
|
||||
}
|
||||
.cad-tool:disabled {
|
||||
@@ -376,23 +338,14 @@ input[type="color"][data-mixed="true"]::-webkit-color-swatch { background: trans
|
||||
background: var(--cad-chrome-raised);
|
||||
color: var(--cad-text-dim);
|
||||
}
|
||||
/* 꼴 = 탭 부품(`ui-tabs`) · 여기는 칸 둘이 폭을 나누는 배치만 */
|
||||
.cad-inspector-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
height: 36px;
|
||||
border-bottom: 1px solid var(--cad-line);
|
||||
gap: 0;
|
||||
}
|
||||
.cad-inspector-tabs button {
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: var(--cad-chrome-sunken);
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
.cad-inspector-tabs button[data-active="true"] {
|
||||
border-bottom-color: var(--cad-accent);
|
||||
background: var(--cad-chrome-raised);
|
||||
color: var(--color-text);
|
||||
.cad-inspector-tabs > .ui-tabs__tab {
|
||||
justify-content: center;
|
||||
}
|
||||
.cad-properties {
|
||||
padding: 14px 12px;
|
||||
|
||||
@@ -74,7 +74,6 @@ export interface Layer {
|
||||
|
||||
export enum LOCAL_STORAGE_KEY {
|
||||
DRAWING = 'OPEN_WEB_CAD__DRAWING',
|
||||
DROPDOWN = 'OPEN_WEB_CAD__DROPDOWN',
|
||||
}
|
||||
|
||||
export interface StartAndEndpointEntity {
|
||||
|
||||
@@ -14,22 +14,22 @@
|
||||
padding-right: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
/* 도면층 버튼 — 색은 토큰으로만, 상태는 Button이 붙이는 data-* 속성으로 가른다. */
|
||||
/* 도면층 버튼 — 목록 줄 부품(`ui-sidebar-row`) 꼴 · 상태는 Button이 붙이는 data-* 속성으로 가른다. */
|
||||
.cad-layer-manager button[data-type="regular"] {
|
||||
background: var(--cad-chrome-sunken);
|
||||
color: var(--cad-accent);
|
||||
background: transparent;
|
||||
color: var(--color-text-body);
|
||||
}
|
||||
.cad-layer-manager button[data-type="transparent"] {
|
||||
background: transparent;
|
||||
color: var(--cad-accent);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.cad-layer-manager button:hover {
|
||||
background: var(--cad-hover);
|
||||
background: var(--color-hover);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.cad-layer-manager button[data-active="true"] {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.cad-view-controls {
|
||||
@@ -87,21 +87,7 @@ body.cad-file-dragging::after {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cad-view-controls button {
|
||||
width: 34px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-right: 1px solid var(--cad-line);
|
||||
background: transparent;
|
||||
font-size: 17px;
|
||||
}
|
||||
.cad-view-controls button:hover {
|
||||
background: var(--cad-hover);
|
||||
}
|
||||
.cad-view-controls__fit {
|
||||
color: var(--cad-accent);
|
||||
font-size: 15px;
|
||||
}
|
||||
/* 단추 = 아이콘 단추 부품(단 32) · 여기는 배율 글만 */
|
||||
.cad-view-controls span {
|
||||
min-width: 48px;
|
||||
color: var(--cad-text-dim);
|
||||
@@ -133,30 +119,16 @@ body.cad-file-dragging::after {
|
||||
color: var(--cad-text-muted);
|
||||
font: 11px Consolas, monospace;
|
||||
}
|
||||
/* 꼴 = 메뉴 부품(`ui-ctx-menu`) · 여기는 자리와 명령 글꼴만 */
|
||||
.cad-command-suggestions {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
bottom: 100%;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 220px;
|
||||
padding: 4px;
|
||||
background: var(--cad-chrome-raised);
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: var(--radius-cards);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.cad-command-suggestions button {
|
||||
padding: 4px 8px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
font: 11px Consolas, monospace;
|
||||
text-align: left;
|
||||
}
|
||||
.cad-command-suggestions button:hover {
|
||||
background: var(--cad-hover);
|
||||
.cad-command-suggestions > .ui-ctx-menu__item {
|
||||
font: 12px Consolas, monospace;
|
||||
}
|
||||
.cad-prop--wide select {
|
||||
min-width: 132px;
|
||||
@@ -180,12 +152,13 @@ body.cad-file-dragging::after {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* 명령 칸 = CAD 고유 꼴(토큰만) · 높이 = 크기 단 28 */
|
||||
.cad-command-area input {
|
||||
flex: 1;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: var(--radius-inputs);
|
||||
border-radius: var(--radius-buttons);
|
||||
outline: none;
|
||||
background: var(--cad-chrome-sunken);
|
||||
color: var(--cad-text);
|
||||
@@ -210,23 +183,7 @@ body.cad-file-dragging::after {
|
||||
background: var(--cad-chrome);
|
||||
border-top: 1px solid var(--cad-line);
|
||||
}
|
||||
.cad-statusbar button {
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 10px;
|
||||
}
|
||||
.cad-statusbar button:hover {
|
||||
background: var(--cad-hover);
|
||||
}
|
||||
.cad-statusbar button[data-active="true"] {
|
||||
border-color: var(--cad-accent);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
/* 켜기 = 켜기 칩 부품(`ui-toggle--chip` · 단 22) */
|
||||
.cad-statusbar__hint {
|
||||
margin-left: auto;
|
||||
color: var(--cad-text-muted);
|
||||
@@ -261,6 +218,7 @@ body.cad-file-dragging::after {
|
||||
font-size: 11px;
|
||||
color: var(--cad-text-dim);
|
||||
}
|
||||
/* 칸 = 크기 단 24(둥글기 6) · 토큰만 */
|
||||
.cad-properties-editor select,
|
||||
.cad-properties-editor input {
|
||||
height: 24px;
|
||||
@@ -292,59 +250,19 @@ body.cad-file-dragging::after {
|
||||
color: var(--cad-text);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.cad-properties__empty {
|
||||
margin: 0;
|
||||
color: var(--cad-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 빠른 특성 — 화면 위에 뜨는 작은 상자 */
|
||||
.cad-quick-properties {
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
top: calc(var(--cad-title-height) + var(--cad-ribbon-height) + 12px);
|
||||
right: 14px;
|
||||
width: 240px;
|
||||
padding: 8px 10px 10px;
|
||||
background: var(--cad-chrome-raised);
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: var(--radius-cards);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.cad-quick-properties__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
.cad-quick-properties__header button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--cad-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 블록 라이브러리 팔레트 (INSERT/BLOCK) */
|
||||
/* 빠른 특성 · 블록 라이브러리 — 꼴 = 떠 있는 판 부품(`ui-float-panel`) · 여기는 자리 · 폭만 */
|
||||
.cad-quick-properties,
|
||||
.cad-block-library {
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
top: calc(var(--cad-title-height) + var(--cad-ribbon-height) + 12px);
|
||||
right: 14px;
|
||||
width: 240px;
|
||||
padding: 8px 10px 10px;
|
||||
background: var(--cad-chrome-raised);
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: var(--radius-cards);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.cad-block-library__empty {
|
||||
margin: 0;
|
||||
color: var(--cad-text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 블록 라이브러리 팔레트 (INSERT/BLOCK) */
|
||||
.cad-block-library__list {
|
||||
max-height: 220px;
|
||||
margin: 0;
|
||||
@@ -357,41 +275,26 @@ body.cad-file-dragging::after {
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.cad-block-library__list li button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--cad-text);
|
||||
font-size: 12px;
|
||||
}
|
||||
/* 블록 줄 = 목록 줄 부품(`ui-sidebar-row`) · 지우기 = 아이콘 단추 부품(단 20) · 여기는 줄 폭만 */
|
||||
.cad-block-library__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 3px 4px;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cad-block-library__name:hover {
|
||||
background: var(--cad-chrome-sunken);
|
||||
}
|
||||
/* 단추 = 단추 부품(보조 · 단 24) · 여기는 둘이 폭을 나눔만 */
|
||||
.cad-block-library__actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.cad-block-library__actions button {
|
||||
.cad-block-library__actions > .ui-btn {
|
||||
flex: 1;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 도각 자리표 패널 — 도각 편집으로 도면을 실었을 때만 뜬다 (2026-09-06 사용자 지시로
|
||||
부모 사이드바에서 캐드 안으로 옮김). 도면 오른쪽 위, 리본 아래에 붙는다. */
|
||||
부모 사이드바에서 캐드 안으로 옮김). 꼴 = 떠 있는 판 부품(`ui-float-panel`) · 여기는 자리 · 폭 · 줄 배치만. */
|
||||
.cad-frame-tokens {
|
||||
position: fixed;
|
||||
/* 오른쪽 위는 진행단계 패널이 쓴다 — 아래쪽(상태막대·명령행 위)에 붙인다. */
|
||||
@@ -399,36 +302,13 @@ body.cad-file-dragging::after {
|
||||
bottom: calc(var(--cad-status-height) + var(--cad-command-height) + 8px);
|
||||
z-index: 3;
|
||||
max-height: 46vh;
|
||||
overflow-y: auto;
|
||||
width: 236px;
|
||||
}
|
||||
|
||||
.cad-frame-tokens__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 236px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--cad-chrome-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
color: var(--cad-text);
|
||||
}
|
||||
|
||||
.cad-frame-tokens__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cad-frame-tokens__fold {
|
||||
flex: none;
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--cad-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cad-frame-tokens__group {
|
||||
@@ -440,41 +320,9 @@ body.cad-file-dragging::after {
|
||||
|
||||
.cad-frame-tokens__title {
|
||||
width: 100%;
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.cad-frame-tokens__button {
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: 4px;
|
||||
background: var(--cad-chrome);
|
||||
color: var(--cad-text);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cad-frame-tokens__button:hover {
|
||||
background: var(--cad-hover);
|
||||
}
|
||||
|
||||
/* 자리표 칸 크기 입력 (2026-09-06) */
|
||||
.cad-frame-tokens__size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.cad-frame-tokens__size input {
|
||||
/* 자리표 칸 크기 입력 (2026-09-06) — 칸 = 입력 부품(단 24) */
|
||||
.cad-frame-tokens__size > .ui-input {
|
||||
width: 64px;
|
||||
height: 24px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid var(--cad-line);
|
||||
border-radius: 6px;
|
||||
background: var(--cad-chrome);
|
||||
color: var(--cad-text);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** 삽입 탭 명령 (조사표 4절 중 구현분) */
|
||||
import { toast } from 'react-toastify';
|
||||
import { askText } from '../../../../ui_template/ui_template_modal';
|
||||
import { saveBlockFromEntities } from '../blocks/block-library';
|
||||
import { setBlockLibraryVisible } from '../components/ui-state';
|
||||
import { importImageFromFile } from '../helpers/import-export-handlers/import-image-from-file';
|
||||
import { pickFile } from '../helpers/pick-file';
|
||||
import { importDrawingFile } from '../integration/dxf-clip-paste';
|
||||
import { askText } from '../integration/ui-kit';
|
||||
import { getActiveToolActor, getSelectedEntities } from '../state';
|
||||
import { Tool } from '../tools';
|
||||
import { imageImportToolStateMachine } from '../tools/image-import-tool';
|
||||
@@ -40,14 +40,13 @@ export const INSERT_COMMANDS: CadCommand[] = [
|
||||
needsSelection: true,
|
||||
run: () => {
|
||||
const entities = getSelectedEntities();
|
||||
// 글 묻기 = 부품 확인창(553-6 · window.prompt 대신)
|
||||
void askText({ message: '블록 이름', okLabel: '저장' }).then((raw) => {
|
||||
const name = raw?.trim();
|
||||
// 글 묻기 = 확인창 부품(window.prompt 대신 · 553-6)
|
||||
void askText({ message: '블록 이름', okLabel: '저장' }).then(async (text) => {
|
||||
const name = text?.trim();
|
||||
if (!name) return;
|
||||
void saveBlockFromEntities(name, entities).then(() => {
|
||||
toast.success(`블록 '${name}' 저장 (객체 ${entities.length}개)`);
|
||||
setBlockLibraryVisible(true);
|
||||
});
|
||||
await saveBlockFromEntities(name, entities);
|
||||
toast.success(`블록 '${name}' 저장 (객체 ${entities.length}개)`);
|
||||
setBlockLibraryVisible(true);
|
||||
});
|
||||
return '블록 이름 입력';
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/** 블록 라이브러리 팔레트 — 저장한 블록을 고르면 삽입 도구가 켜진다 (INSERT/BLOCK) */
|
||||
/** 블록 라이브러리 팔레트 — 저장한 블록을 고르면 삽입 도구가 켜진다 (INSERT/BLOCK) · 떠 있는 판 부품 */
|
||||
import type { FC } from 'react';
|
||||
import { showConfirmDialog } from '../../../../ui_template/ui_template_modal';
|
||||
import {
|
||||
deleteBlock,
|
||||
exportBlocksToFile,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
importBlocksFromFile,
|
||||
} from '../blocks/block-library';
|
||||
import { pickFile } from '../helpers/pick-file';
|
||||
import { askConfirm } from '../integration/ui-kit';
|
||||
import { startBlockInsert } from '../tools/insert-block-tool';
|
||||
import { isBlockLibraryVisible, setBlockLibraryVisible } from './ui-state';
|
||||
|
||||
@@ -17,58 +17,83 @@ export const BlockLibraryPanel: FC = () => {
|
||||
const blocks = getBlocks();
|
||||
|
||||
return (
|
||||
<div className="cad-block-library controls">
|
||||
<div className="cad-quick-properties__header">
|
||||
<span>블록 라이브러리</span>
|
||||
<button type="button" onClick={() => setBlockLibraryVisible(false)} title="닫기">
|
||||
<div className="ui-float-panel cad-block-library controls">
|
||||
<div className="ui-float-panel__head">
|
||||
<span className="ui-float-panel__title">블록 라이브러리</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ui-icon-btn ui-icon-btn--ghost"
|
||||
data-size="24"
|
||||
onClick={() => setBlockLibraryVisible(false)}
|
||||
title="닫기"
|
||||
aria-label="닫기"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="ui-float-panel__body">
|
||||
{blocks.length === 0 ? (
|
||||
<div className="ui-empty ui-empty--small">
|
||||
<p className="ui-empty__text">객체를 선택하고 BLOCK 명령으로 저장하십시오.</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="cad-block-library__list">
|
||||
{blocks.map((block) => (
|
||||
<li key={block.name}>
|
||||
<button
|
||||
type="button"
|
||||
className="ui-sidebar-row cad-block-library__name"
|
||||
title={`객체 ${block.entities.length}개 — 클릭하면 삽입`}
|
||||
onClick={() => {
|
||||
setBlockLibraryVisible(false);
|
||||
void startBlockInsert(block);
|
||||
}}
|
||||
>
|
||||
{block.name}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ui-icon-btn ui-icon-btn--ghost"
|
||||
data-size="20"
|
||||
title="삭제"
|
||||
aria-label="삭제"
|
||||
onClick={async () => {
|
||||
if (
|
||||
await askConfirm({
|
||||
message: `블록 '${block.name}'을 삭제하시겠습니까?`,
|
||||
okLabel: '삭제',
|
||||
danger: true,
|
||||
})
|
||||
) {
|
||||
deleteBlock(block.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{blocks.length === 0 ? (
|
||||
<p className="cad-block-library__empty">객체를 선택하고 BLOCK 명령으로 저장하십시오.</p>
|
||||
) : (
|
||||
<ul className="cad-block-library__list">
|
||||
{blocks.map((block) => (
|
||||
<li key={block.name}>
|
||||
<button
|
||||
type="button"
|
||||
className="cad-block-library__name"
|
||||
title={`객체 ${block.entities.length}개 — 클릭하면 삽입`}
|
||||
onClick={() => {
|
||||
setBlockLibraryVisible(false);
|
||||
void startBlockInsert(block);
|
||||
}}
|
||||
>
|
||||
{block.name}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="삭제"
|
||||
onClick={() => {
|
||||
// 위험 확인 = 부품 확인창(553-6 · window.confirm 대신)
|
||||
void showConfirmDialog(`블록 '${block.name}'을 삭제하시겠습니까?`, '삭제').then(
|
||||
(ok) => ok && deleteBlock(block.name),
|
||||
);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<div className="cad-block-library__actions">
|
||||
<button type="button" onClick={() => exportBlocksToFile()}>
|
||||
파일로 내보내기
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => pickFile('application/json', (file) => importBlocksFromFile(file))}
|
||||
>
|
||||
파일에서 가져오기
|
||||
</button>
|
||||
<div className="cad-block-library__actions">
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn ui-btn--ghost"
|
||||
data-size="24"
|
||||
onClick={() => exportBlocksToFile()}
|
||||
>
|
||||
파일로 내보내기
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ui-btn ui-btn--ghost"
|
||||
data-size="24"
|
||||
onClick={() => pickFile('application/json', (file) => importBlocksFromFile(file))}
|
||||
>
|
||||
파일에서 가져오기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -37,9 +37,9 @@ export const Button: FC<ButtonProps> = ({
|
||||
left = null,
|
||||
right = null,
|
||||
}) => {
|
||||
// 색은 App.css의 테마 토큰이 data-type/data-active로 준다 — 여기서는 배치만.
|
||||
// 색은 App.css의 테마 토큰이 data-type/data-active로 준다 — 여기서는 배치만 · 높이 = 크기 단 42(글 14 · 둥글기 8)
|
||||
const classParts = [
|
||||
'font-semibold py-4 h-[42px] flex flex-row justify-start w-full items-center',
|
||||
'font-semibold h-[42px] text-sm rounded-lg flex flex-row justify-start w-full items-center',
|
||||
size === 'regular' ? 'pl-2 pr-2 gap-2' : '',
|
||||
size === 'small' ? 'pl-2 pr-2 gap-0' : '',
|
||||
className || '',
|
||||
|
||||
@@ -16,7 +16,10 @@ export const CommandLine: FC = () => {
|
||||
const history = getCommandHistory();
|
||||
const instruction = currentPrompt().line || '명령을 입력하거나 도구를 선택하십시오.';
|
||||
|
||||
const suggestions = useMemo(() => matchCommandPrefixes(pick?.base ?? text).slice(0, 8), [text, pick]);
|
||||
const suggestions = useMemo(
|
||||
() => matchCommandPrefixes(pick?.base ?? text).slice(0, 8),
|
||||
[text, pick]
|
||||
);
|
||||
|
||||
const submit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -102,10 +105,11 @@ export const CommandLine: FC = () => {
|
||||
<span className="cad-command-prompt">{instruction}</span>
|
||||
</form>
|
||||
{text.trim() && suggestions.length > 0 && (
|
||||
<div className="cad-command-suggestions">
|
||||
<div className="ui-ctx-menu cad-command-suggestions">
|
||||
{suggestions.map((command) => (
|
||||
<button
|
||||
type="button"
|
||||
className="ui-ctx-menu__item"
|
||||
key={command.id}
|
||||
onClick={() => {
|
||||
runCommandInput(command.id);
|
||||
|
||||
@@ -139,15 +139,15 @@ export const FramePlaceholderPanel: FC = () => {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<section className="cad-frame-tokens controls">
|
||||
<header className="cad-frame-tokens__header">
|
||||
<span>
|
||||
{folded ? '자리표 놓기' : '자리표 놓기 — 누르면 화면 가운데에 서고, 끌어서 자리를 잡습니다'}
|
||||
</span>
|
||||
<section className="ui-float-panel cad-frame-tokens controls">
|
||||
<header className="ui-float-panel__head cad-frame-tokens__header">
|
||||
<span className="ui-float-panel__title">자리표 놓기</span>
|
||||
<button
|
||||
type="button"
|
||||
className="cad-frame-tokens__fold"
|
||||
className="ui-icon-btn ui-icon-btn--ghost"
|
||||
data-size="24"
|
||||
aria-expanded={!folded}
|
||||
aria-label={folded ? '자리표 패널 펴기' : '자리표 패널 접기'}
|
||||
title={folded ? '자리표 패널 펴기' : '자리표 패널 접기'}
|
||||
onClick={() => setFolded(!folded)}
|
||||
>
|
||||
@@ -155,58 +155,71 @@ export const FramePlaceholderPanel: FC = () => {
|
||||
</button>
|
||||
</header>
|
||||
{!folded && (
|
||||
<>
|
||||
<div className="cad-frame-tokens__group">
|
||||
<span className="cad-frame-tokens__title">글자</span>
|
||||
{TEXT_TOKENS.map((token) => (
|
||||
<button
|
||||
key={token}
|
||||
type="button"
|
||||
className="cad-frame-tokens__button"
|
||||
onClick={() => addTextPlaceholder(token)}
|
||||
>
|
||||
{token}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="cad-frame-tokens__group">
|
||||
<span className="cad-frame-tokens__title">그림 (칸에 비율 그대로 들어감)</span>
|
||||
{IMAGE_TOKENS.map((token) => (
|
||||
<button
|
||||
key={token}
|
||||
type="button"
|
||||
className="cad-frame-tokens__button"
|
||||
onClick={() => void addImagePlaceholder(token)}
|
||||
>
|
||||
{token}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{picked && (
|
||||
<div className="cad-frame-tokens__group">
|
||||
<span className="cad-frame-tokens__title">고른 자리표 칸 크기 (mm)</span>
|
||||
<label className="cad-frame-tokens__size">
|
||||
가로
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={size.width}
|
||||
onChange={(event) => applySize(Number(event.target.value), size.height)}
|
||||
/>
|
||||
</label>
|
||||
<label className="cad-frame-tokens__size">
|
||||
세로
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={size.height}
|
||||
onChange={(event) => applySize(size.width, Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<div className="ui-float-panel__body cad-frame-tokens__body">
|
||||
<p className="ui-text ui-text--muted">
|
||||
누르면 화면 가운데에 서고, 끌어서 자리를 잡습니다
|
||||
</p>
|
||||
<div className="cad-frame-tokens__group">
|
||||
<span className="ui-text ui-text--muted cad-frame-tokens__title">글자</span>
|
||||
{TEXT_TOKENS.map((token) => (
|
||||
<button
|
||||
key={token}
|
||||
type="button"
|
||||
className="ui-btn ui-btn--ghost"
|
||||
data-size="22"
|
||||
onClick={() => addTextPlaceholder(token)}
|
||||
>
|
||||
{token}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="cad-frame-tokens__group">
|
||||
<span className="ui-text ui-text--muted cad-frame-tokens__title">
|
||||
그림 (칸에 비율 그대로 들어감)
|
||||
</span>
|
||||
{IMAGE_TOKENS.map((token) => (
|
||||
<button
|
||||
key={token}
|
||||
type="button"
|
||||
className="ui-btn ui-btn--ghost"
|
||||
data-size="22"
|
||||
onClick={() => void addImagePlaceholder(token)}
|
||||
>
|
||||
{token}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{picked && (
|
||||
<div className="cad-frame-tokens__group">
|
||||
<span className="ui-text ui-text--muted cad-frame-tokens__title">
|
||||
고른 자리표 칸 크기 (mm)
|
||||
</span>
|
||||
<label className="ui-text ui-text--muted cad-frame-tokens__size">
|
||||
가로
|
||||
<input
|
||||
type="number"
|
||||
className="ui-input"
|
||||
data-size="24"
|
||||
min={1}
|
||||
value={size.width}
|
||||
onChange={(event) => applySize(Number(event.target.value), size.height)}
|
||||
/>
|
||||
</label>
|
||||
<label className="ui-text ui-text--muted cad-frame-tokens__size">
|
||||
세로
|
||||
<input
|
||||
type="number"
|
||||
className="ui-input"
|
||||
data-size="24"
|
||||
min={1}
|
||||
value={size.height}
|
||||
onChange={(event) => applySize(size.width, Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,17 +27,24 @@ export const InspectorPanel: FC<InspectorPanelProps> = ({ collapsed, onToggleCol
|
||||
</button>
|
||||
{!collapsed && (
|
||||
<>
|
||||
<div className="cad-inspector-tabs">
|
||||
{/* 탭 부품(밑줄 탭 · 단 36) — 칸 둘이 폭을 나눠 가짐 */}
|
||||
<div className="ui-tabs cad-inspector-tabs" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
data-active={tab === 'properties'}
|
||||
role="tab"
|
||||
className={`ui-tabs__tab${tab === 'properties' ? ' is-active' : ''}`}
|
||||
data-size="36"
|
||||
aria-selected={tab === 'properties'}
|
||||
onClick={() => openInspector('properties')}
|
||||
>
|
||||
특성
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-active={tab === 'layers'}
|
||||
role="tab"
|
||||
className={`ui-tabs__tab${tab === 'layers' ? ' is-active' : ''}`}
|
||||
data-size="36"
|
||||
aria-selected={tab === 'layers'}
|
||||
onClick={() => openInspector('layers')}
|
||||
>
|
||||
레이어
|
||||
|
||||
@@ -18,7 +18,8 @@ interface PropertiesEditorProps {
|
||||
}
|
||||
|
||||
/** 고른 것을 사본으로 바꿈 — 되돌리기 한 칸(488) · 선 특성은 글 뺌 */
|
||||
const applyToSelection = (mutate: (entity: Entity) => void) => restyle(getSelectedEntities(), mutate);
|
||||
const applyToSelection = (mutate: (entity: Entity) => void) =>
|
||||
restyle(getSelectedEntities(), mutate);
|
||||
const applyToLines = (mutate: (entity: Entity) => void) =>
|
||||
restyle(styleTargets(getSelectedEntities()), mutate);
|
||||
|
||||
@@ -30,7 +31,11 @@ export const PropertiesEditor: FC<PropertiesEditorProps> = ({ compact = false })
|
||||
const view = selectionLineStyle();
|
||||
|
||||
if (!first) {
|
||||
return <p className="cad-properties__empty">선택된 객체가 없습니다.</p>;
|
||||
return (
|
||||
<div className="ui-empty ui-empty--small">
|
||||
<p className="ui-empty__text">선택된 객체가 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const points = sampleEntityPoints(first);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** 빠른 실행 단추(↶ ↷) — 리본 탭 줄 오른쪽(접기 단추 앞)에 그린다 · 숨김 목록은 DOM 만 둔다 */
|
||||
/** 빠른 실행 단추(↶ ↷) — 리본 탭 줄 오른쪽(접기 단추 앞)에 그린다 · 아이콘 단추 부품(단 20) · 숨김 목록은 DOM 만 둔다 */
|
||||
import type { FC } from 'react';
|
||||
import { getCommandById } from '../commands/registry';
|
||||
import { runCommand } from '../commands/run-command';
|
||||
@@ -16,6 +16,9 @@ export const QuickAccessBar: FC = () => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="ui-icon-btn ui-icon-btn--ghost"
|
||||
data-size="20"
|
||||
aria-label={command.label}
|
||||
key={command.id}
|
||||
hidden={QUICK_ACCESS_HIDDEN.includes(id)}
|
||||
title={`${command.label} (${command.id})`}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** 빠른 특성 — 선택이 있을 때만 화면 오른쪽 위에 뜨는 간이 특성 상자 (QUICKPROPERTIES) */
|
||||
/** 빠른 특성 — 선택이 있을 때만 화면 오른쪽 위에 뜨는 간이 특성 상자 (QUICKPROPERTIES) · 떠 있는 판 부품 */
|
||||
import type { FC } from 'react';
|
||||
import { PropertiesEditor } from './PropertiesEditor';
|
||||
import { getSelectedEntities } from '../state';
|
||||
@@ -8,14 +8,23 @@ export const QuickProperties: FC = () => {
|
||||
if (!isQuickPropertiesVisible() || getSelectedEntities().length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="cad-quick-properties controls">
|
||||
<div className="cad-quick-properties__header">
|
||||
<span>빠른 특성</span>
|
||||
<button type="button" onClick={() => setQuickPropertiesVisible(false)} title="닫기">
|
||||
<div className="ui-float-panel cad-quick-properties controls">
|
||||
<div className="ui-float-panel__head">
|
||||
<span className="ui-float-panel__title">빠른 특성</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ui-icon-btn ui-icon-btn--ghost"
|
||||
data-size="24"
|
||||
onClick={() => setQuickPropertiesVisible(false)}
|
||||
title="닫기"
|
||||
aria-label="닫기"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<PropertiesEditor compact />
|
||||
<div className="ui-float-panel__body">
|
||||
<PropertiesEditor compact />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** 상태막대 — AutoCAD 하단 제도 보조 토글 */
|
||||
/** 상태막대 — AutoCAD 하단 제도 보조 토글 · 켜기 칩 부품(`ui-toggle--chip` · 단 22) */
|
||||
import type { FC } from 'react';
|
||||
import {
|
||||
getAngleStep,
|
||||
@@ -27,7 +27,9 @@ export const StatusBar: FC<StatusBarProps> = ({ commandLineVisible, onToggleComm
|
||||
<footer className="cad-statusbar controls">
|
||||
<button
|
||||
type="button"
|
||||
data-active={snap}
|
||||
className="ui-toggle ui-toggle--chip"
|
||||
data-size="22"
|
||||
aria-pressed={snap}
|
||||
title="객체 스냅 (OSNAP)"
|
||||
onClick={() => setSnapEnabled(!snap)}
|
||||
>
|
||||
@@ -35,7 +37,9 @@ export const StatusBar: FC<StatusBarProps> = ({ commandLineVisible, onToggleComm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-active={ortho}
|
||||
className="ui-toggle ui-toggle--chip"
|
||||
data-size="22"
|
||||
aria-pressed={ortho}
|
||||
title="직교 모드 (F8) — 90° 간격 각도 가이드"
|
||||
onClick={() => setAngleStep(ortho ? 0 : 90)}
|
||||
>
|
||||
@@ -43,7 +47,9 @@ export const StatusBar: FC<StatusBarProps> = ({ commandLineVisible, onToggleComm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-active={polar}
|
||||
className="ui-toggle ui-toggle--chip"
|
||||
data-size="22"
|
||||
aria-pressed={polar}
|
||||
title="극좌표 추적 (F10) — 45° 간격 각도 가이드"
|
||||
onClick={() => setAngleStep(polar ? 0 : 45)}
|
||||
>
|
||||
@@ -51,7 +57,9 @@ export const StatusBar: FC<StatusBarProps> = ({ commandLineVisible, onToggleComm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-active={snapTracking}
|
||||
className="ui-toggle ui-toggle--chip"
|
||||
data-size="22"
|
||||
aria-pressed={snapTracking}
|
||||
title="객체 스냅 추적 — 스냅점에 잠시 머물면 그 점에서 정렬선이 뻗는다"
|
||||
onClick={() => setSnapTrackingEnabled(!snapTracking)}
|
||||
>
|
||||
@@ -59,7 +67,9 @@ export const StatusBar: FC<StatusBarProps> = ({ commandLineVisible, onToggleComm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-active={grid}
|
||||
className="ui-toggle ui-toggle--chip"
|
||||
data-size="22"
|
||||
aria-pressed={grid}
|
||||
title="그리드 표시"
|
||||
onClick={() => setGridEnabled(!grid)}
|
||||
>
|
||||
@@ -67,7 +77,9 @@ export const StatusBar: FC<StatusBarProps> = ({ commandLineVisible, onToggleComm
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-active={commandLineVisible}
|
||||
className="ui-toggle ui-toggle--chip"
|
||||
data-size="22"
|
||||
aria-pressed={commandLineVisible}
|
||||
title="명령행 표시/숨기기"
|
||||
onClick={onToggleCommandLine}
|
||||
>
|
||||
|
||||
@@ -3,12 +3,10 @@ import type { FC } from 'react';
|
||||
import { runCommandInput } from '../commands/run-command';
|
||||
import { redoCommand, undoCommand } from '../inputController/prompt-input';
|
||||
import { zoomExtents } from '../inputController/zoom-prompt';
|
||||
import {
|
||||
getScreenCanvasDrawController,
|
||||
isDrawingReadOnly,
|
||||
isHostSave,
|
||||
isViewOnly,
|
||||
} from '../state';
|
||||
import { getScreenCanvasDrawController, isDrawingReadOnly, isHostSave, isViewOnly } from '../state';
|
||||
|
||||
/** 아이콘 단추 부품(단 32) */
|
||||
const ICON_BUTTON = { className: 'ui-icon-btn ui-icon-btn--ghost', 'data-size': '32' } as const;
|
||||
|
||||
export const ViewControls: FC = () => {
|
||||
const zoom = getScreenCanvasDrawController()?.getScreenScale() ?? 1;
|
||||
@@ -19,27 +17,52 @@ export const ViewControls: FC = () => {
|
||||
<div className="cad-view-controls controls">
|
||||
{showUndo && (
|
||||
<>
|
||||
<button type="button" onClick={undoCommand} title="실행 취소 (Ctrl+Z)">
|
||||
<button
|
||||
type="button"
|
||||
{...ICON_BUTTON}
|
||||
onClick={undoCommand}
|
||||
title="실행 취소 (Ctrl+Z)"
|
||||
aria-label="실행 취소"
|
||||
>
|
||||
↶
|
||||
</button>
|
||||
<button type="button" onClick={redoCommand} title="다시 실행 (Ctrl+Y)">
|
||||
<button
|
||||
type="button"
|
||||
{...ICON_BUTTON}
|
||||
onClick={redoCommand}
|
||||
title="다시 실행 (Ctrl+Y)"
|
||||
aria-label="다시 실행"
|
||||
>
|
||||
↷
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="cad-view-controls__fit"
|
||||
{...ICON_BUTTON}
|
||||
onClick={() => zoomExtents()}
|
||||
title="범위 줌 (Z) — 도면 전체를 화면에 맞춘다"
|
||||
aria-label="범위 줌"
|
||||
>
|
||||
⛶
|
||||
</button>
|
||||
<button type="button" onClick={() => runCommandInput('ZOOMIN')} title="확대">
|
||||
<button
|
||||
type="button"
|
||||
{...ICON_BUTTON}
|
||||
onClick={() => runCommandInput('ZOOMIN')}
|
||||
title="확대"
|
||||
aria-label="확대"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<span>{Math.round(zoom * 100)}%</span>
|
||||
<button type="button" onClick={() => runCommandInput('ZOOMOUT')} title="축소">
|
||||
<button
|
||||
type="button"
|
||||
{...ICON_BUTTON}
|
||||
onClick={() => runCommandInput('ZOOMOUT')}
|
||||
title="축소"
|
||||
aria-label="축소"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,5 @@
|
||||
/* Shift+우클릭 한 번 스냅 메뉴(`snap-menu.ts`) — 커서 자리에 세로 목록 */
|
||||
/* Shift+우클릭 한 번 스냅 메뉴(`snap-menu.ts`) — 꼴 = 메뉴 부품(`ui-ctx-menu`) · 여기는 커서 자리 · 위 겹침만 */
|
||||
.cad-snap-menu {
|
||||
position: fixed;
|
||||
z-index: 60;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 160px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.cad-snap-menu > button {
|
||||
padding: 4px 10px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cad-snap-menu > button:hover,
|
||||
.cad-snap-menu > button:focus-visible {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
@@ -55,11 +55,12 @@ export function openSnapMenu(x: number, y: number, onPick: (name: string) => voi
|
||||
current = { onPick, element: null };
|
||||
if (typeof document === 'undefined') return;
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'cad-snap-menu controls';
|
||||
menu.className = 'ui-ctx-menu cad-snap-menu controls'; // 꼴 = 메뉴 부품
|
||||
menu.setAttribute('role', 'menu');
|
||||
for (const [name, label] of SNAP_MENU) {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'ui-ctx-menu__item';
|
||||
item.setAttribute('role', 'menuitem');
|
||||
item.textContent = `${label} (${name})`;
|
||||
item.addEventListener('click', () => pickSnapMenu(name));
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 부품 상자(`ui_template/`) CAD 쪽 한 벌(553-6) — CAD 겉도 앱 화면과 같은 부품 꼴을 씀.
|
||||
* 부품 CSS 를 싣고 단추 · 확인창 기본 꼴(`injectBaseStyles`)을 한 번 넣음 · 확인창 · 글 묻기를 다시 냄.
|
||||
* React 화면은 부품 클래스(`ui-btn` · `ui-icon-btn` · `ui-toggle--chip` · `ui-tabs` · `ui-float-panel` · `ui-ctx-menu` · `ui-empty` · `ui-text` · `ui-input`)와
|
||||
* `data-size` 단을 직접 붙임 · 자리 · 폭은 App.css 의 `cad-*` 가 맡음 · 도면 그림 색(`--draw-*`)은 안 건드림.
|
||||
*/
|
||||
import '../../../../ui_template/ui_template_tabs.css';
|
||||
import '../../../../ui_template/ui_template_toggle.css';
|
||||
import '../../../../ui_template/ui_template_float_panel.css';
|
||||
import '../../../../ui_template/ui_template_context_menu.css';
|
||||
import '../../../../ui_template/ui_template_empty.css';
|
||||
import '../../../../ui_template/ui_template_input.css';
|
||||
import '../../../../ui_template/ui_template_text.css';
|
||||
import { injectBaseStyles } from '../../../../ui_template/ui_template_elements_styles.ts';
|
||||
|
||||
export { askConfirm, askText } from '../../../../ui_template/ui_template_modal.ts';
|
||||
|
||||
injectBaseStyles();
|
||||
@@ -3,11 +3,12 @@ import { Point } from '@flatten-js/core';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { Actor, type MachineSnapshot } from 'xstate';
|
||||
// 부품 상자 CSS 를 앱 CSS 보다 먼저 실음 — 자리 · 폭은 뒤에 오는 App.css 의 `cad-*` 가 덮음
|
||||
import './integration/ui-kit.ts';
|
||||
import { SNAP_POINT_DISTANCE } from './App.consts';
|
||||
import App from './App.tsx';
|
||||
import { TOOL_STATE_MACHINES } from './commands/registry';
|
||||
import { ScreenCanvasDrawController } from './drawControllers/screenCanvas.drawController';
|
||||
import { injectBaseStyles } from '../../../ui_template/ui_template_elements_styles';
|
||||
import { registerAutoSave } from './helpers/autosave.ts';
|
||||
import { registerCadDebugHook } from './helpers/debug-hook.ts';
|
||||
import { draw } from './helpers/draw';
|
||||
@@ -48,7 +49,6 @@ import { ActorEvent, type DrawEvent } from './tools/tool.types';
|
||||
|
||||
// 호스트 앱의 화이트/블랙 모드를 먼저 붙인 뒤 렌더한다.
|
||||
syncThemeFromHost();
|
||||
injectBaseStyles(); // 부품 단추 · 확인창 꼴(553-6 · 앱과 같은 한 벌)
|
||||
// 부모가 `?bare` 로 띄우면 첫 그림부터 캔버스만 — 도면을 넘길 때 다시 정함(522-2)
|
||||
if (new URLSearchParams(window.location.search).has('bare')) setBare(true);
|
||||
|
||||
|
||||
@@ -129,10 +129,14 @@ const Panel: FC<{
|
||||
{overflowCommands.length ? ' ▾' : ''}
|
||||
</button>
|
||||
{overflowAt && overflowCommands.length > 0 && (
|
||||
<div className="cad-ribbon-overflow" style={{ top: overflowAt.top, left: overflowAt.left }}>
|
||||
<div
|
||||
className="ui-ctx-menu cad-ribbon-overflow"
|
||||
style={{ top: overflowAt.top, left: overflowAt.left }}
|
||||
>
|
||||
{overflowCommands.map((command) => (
|
||||
<button
|
||||
type="button"
|
||||
className="ui-ctx-menu__item"
|
||||
key={command.id}
|
||||
title={command.hint}
|
||||
onClick={() => {
|
||||
@@ -228,7 +232,9 @@ export const Ribbon: FC<RibbonProps> = ({ activeTool, widgets, onCommandRun }) =
|
||||
<QuickAccessBar />
|
||||
<button
|
||||
type="button"
|
||||
className="cad-ribbon-tabs__collapse"
|
||||
className="ui-icon-btn ui-icon-btn--ghost cad-ribbon-tabs__collapse"
|
||||
data-size="20"
|
||||
aria-label={collapsed ? '리본 펼치기' : '리본 접기'}
|
||||
onClick={() => {
|
||||
setPeek(false);
|
||||
setRibbonCollapsed(!collapsed);
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
/* M02 에 없는 조합 안내 — 빨간 글 */
|
||||
.b08-sheets__combo > .ui-general-block__empty {
|
||||
.b08-sheets__combo > .ui-text {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
@@ -75,31 +75,10 @@
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* 칩 = 부품 배지(알약) · 색은 상태마다 부품 색 · 없어진 원본 = 빨강 점선 */
|
||||
.b08-link__chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
max-width: 100%;
|
||||
padding: 2px var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-pills);
|
||||
}
|
||||
|
||||
.b08-link__chip.is-missing {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* 칩 테두리 상태 색(170-3) — 연결됨 · 마스터가 바뀜 · 고침 있음 · 원본 없어짐(빨강 점선) */
|
||||
.b08-link__chip.is-linked {
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.b08-link__chip.is-changed {
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.b08-link__chip.is-edited {
|
||||
border-color: var(--color-info);
|
||||
}
|
||||
|
||||
.b08-link__chip.is-gone {
|
||||
@@ -128,17 +107,3 @@
|
||||
.b08-link__badges:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 배지(163-4) — 고침 있음 · 마스터가 바뀜(경고) */
|
||||
.b08-link__badge {
|
||||
padding: 0 var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.b08-link__badge.is-warn {
|
||||
border-color: var(--color-warning);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import "./B08_Quantity_UI_Drawing.css";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { PART_OF, projectItems, withCopyValues } from "@ui/ui_template_item_picker_project";
|
||||
@@ -37,10 +38,7 @@ function L(key: keyof typeof ui_locales): string {
|
||||
}
|
||||
|
||||
function note(text: string): HTMLElement {
|
||||
const node = document.createElement("p");
|
||||
node.className = "ui-general-block__empty";
|
||||
node.textContent = text;
|
||||
return node;
|
||||
return createText(text, "muted", 14);
|
||||
}
|
||||
|
||||
export interface SheetsRequest {
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
* 자료 읽기 · 표 짜기 = `B08_Quantity_Earthwork_Tables`(화면 없음 · 인쇄 미리보기도 씀 · 302-3)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createCard } from "@ui/ui_template_elements";
|
||||
import { createCard } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import { footNote } from "./B08_Quantity_UI_Foot";
|
||||
import {
|
||||
EARTHWORK_SHEETS,
|
||||
@@ -29,10 +31,7 @@ import {
|
||||
import { onOptionsChange } from "./B08_Quantity_UI_Options";
|
||||
|
||||
export function note(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty";
|
||||
p.textContent = text;
|
||||
return p;
|
||||
return createText(text, "muted", 14);
|
||||
}
|
||||
|
||||
/** 표 한 장 — 공용 컨테이너(카드) 안에 공용 표 · 아래 줄 · 맨 아래 안내 · 경고 줄(202-1) */
|
||||
@@ -78,26 +77,18 @@ export async function renderEarthwork(host: HTMLElement, projectId: string): Pro
|
||||
const { table, interval, applyRock } = loaded;
|
||||
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs ui-subtabs b08-earthwork__tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
tabs.className = "b08-quantity__tabs b08-earthwork__tabs";
|
||||
const view = document.createElement("div");
|
||||
let current: SheetId = "summary";
|
||||
const select = (picked: SheetId): void => {
|
||||
current = picked;
|
||||
tabs.replaceChildren(
|
||||
...SHEETS.map(({ id, label }) => {
|
||||
const button = createButton({
|
||||
label,
|
||||
variant: id === picked ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => id !== picked && select(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === picked));
|
||||
button.dataset.b08EarthworkSheet = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
fillTabs(tabs, {
|
||||
tabs: SHEETS.map(({ id, label }) => ({ key: id, label })),
|
||||
value: picked,
|
||||
kind: "pill",
|
||||
data: "b08EarthworkSheet",
|
||||
onChange: (id) => select(id as SheetId),
|
||||
});
|
||||
if (picked === "sheet") renderSheet(view, table, interval);
|
||||
else if (picked === "summary") renderEarthworkSummary(view, table);
|
||||
else if (picked === "haul") renderHaulDistance(view, table);
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
|
||||
export function footNote(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty b08-quantity__foot";
|
||||
p.textContent = text;
|
||||
const p = createText(text, "muted");
|
||||
p.classList.add("b08-quantity__foot");
|
||||
attachHoverInfo(p, (el) => (el.scrollWidth > el.clientWidth ? el.textContent : null));
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
색은 테마 토큰만(밝음 · 어두움 같이) · 테두리는 입력 · 선택 칸(공용 ui-input · ui-select)만
|
||||
· 굵게 + 붉은 계열은 중요 결과(.is-key)만. */
|
||||
|
||||
/* 보조 탭 + [저장] 한 줄 = 부품 도구줄 · 여기는 아래 틈만 */
|
||||
.b08-hydro__toolbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-16);
|
||||
margin-bottom: var(--spacing-16);
|
||||
}
|
||||
|
||||
@@ -16,10 +13,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.b08-hydro__toolbar > .ui-btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.b08-hydro__sheet {
|
||||
container-type: inline-size; /* 2열 ↔ 1열 = 창이 아닌 이 칸 폭 기준 */
|
||||
color: var(--color-text-body);
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { clearState, writeState } from "../A00_Common/b_page_state";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
import {
|
||||
hydroCalc,
|
||||
@@ -36,10 +39,7 @@ import { buildHydroSheet, type HydroInputs, type HydroResult } from "./B08_Quant
|
||||
export { request };
|
||||
|
||||
function note(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty";
|
||||
p.textContent = text;
|
||||
return p;
|
||||
return createText(text, "muted", 14);
|
||||
}
|
||||
|
||||
/** `onSave` = 페이지 이동 전 저장 등록(481-2) — 고친 측점 없으면 요청 없이 참. */
|
||||
@@ -68,17 +68,15 @@ export async function renderHydro(
|
||||
const inputsOf = (station: HydroStation): HydroInputs => hydroInputs(sheet, station, drafts);
|
||||
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs ui-subtabs b08-hydro__tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
tabs.className = "b08-quantity__tabs b08-hydro__tabs";
|
||||
const save = createButton({
|
||||
label: "저장",
|
||||
variant: "filled",
|
||||
onClick: () => void saveAll(),
|
||||
});
|
||||
save.dataset.b08HydroSave = "";
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.className = "b08-hydro__toolbar";
|
||||
toolbar.append(tabs, save);
|
||||
const toolbar = createToolbar({ items: [tabs, save], size: 32 });
|
||||
toolbar.classList.add("b08-hydro__toolbar");
|
||||
const page = document.createElement("div");
|
||||
host.replaceChildren(toolbar, page);
|
||||
let seq = 0;
|
||||
@@ -87,22 +85,17 @@ export async function renderHydro(
|
||||
const open = (index: number): void => {
|
||||
openIndex = index;
|
||||
const station = sheet.stations[index];
|
||||
tabs.replaceChildren(
|
||||
...sheet.stations.map((s, i) => {
|
||||
fillTabs(tabs, {
|
||||
tabs: sheet.stations.map((s, i) => {
|
||||
const label = stationLabel(s.chainage_m, interval);
|
||||
const button = createButton({
|
||||
label: s.kind ? `${label} ${s.kind}` : label,
|
||||
variant: i === index ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => i !== index && open(i),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(i === index));
|
||||
button.dataset.b08HydroStation = String(s.chainage_m);
|
||||
button.title = s.kind || s.facility;
|
||||
return button;
|
||||
return { key: String(i), label: s.kind ? `${label} ${s.kind}` : label };
|
||||
}),
|
||||
);
|
||||
value: String(index),
|
||||
kind: "pill",
|
||||
data: "b08HydroStation",
|
||||
title: (key) => sheet.stations[Number(key)].kind || sheet.stations[Number(key)].facility,
|
||||
onChange: (key) => open(Number(key)),
|
||||
});
|
||||
const draw = (inputs: HydroInputs): void => {
|
||||
const view = buildHydroSheet(
|
||||
stationLabel(station.chainage_m, interval),
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
||||
import {
|
||||
projectItems,
|
||||
structureFolder,
|
||||
@@ -102,6 +103,22 @@ async function update(projectId: string, info: ProjectItemInfo): Promise<boolean
|
||||
}
|
||||
}
|
||||
|
||||
type ChipState = "is-linked" | "is-missing" | "is-changed" | "is-edited" | "is-gone";
|
||||
const CHIP_TONE: Record<ChipState, BadgeTone> = {
|
||||
"is-linked": "success",
|
||||
"is-missing": "danger",
|
||||
"is-changed": "warning",
|
||||
"is-edited": "info",
|
||||
"is-gone": "danger",
|
||||
};
|
||||
|
||||
/** 칩 = 부품 배지(알약 20) 꼴 · 색 = 상태(연결 초록 · 미연결 빨강 · 바뀜 노랑 · 고침 파랑 · 없어짐 빨강 점선) */
|
||||
function toneChip(chip: HTMLElement, state: ChipState): void {
|
||||
const look = createBadge("", CHIP_TONE[state], { size: 20 });
|
||||
chip.className = `${look.className} b08-link__chip ${state}`;
|
||||
chip.dataset.size = look.dataset.size ?? "20";
|
||||
}
|
||||
|
||||
/** 칩 배지 · 테두리 색 · [업데이트] — 사본 목록(원본판 대조)을 뒤에서 받아 칠함 · 못 받으면 단추 꺼진 채. */
|
||||
async function paintCopyState(
|
||||
projectId: string,
|
||||
@@ -116,12 +133,8 @@ async function paintCopyState(
|
||||
.then((rows) => rows.find((row) => row.폴더 === folder) ?? null)
|
||||
.catch(() => null);
|
||||
if (!info) return null;
|
||||
const badge = (key: keyof typeof ui_locales, warn: boolean): HTMLElement => {
|
||||
const node = document.createElement("span");
|
||||
node.className = warn ? "b08-link__badge is-warn" : "b08-link__badge";
|
||||
node.textContent = L(key);
|
||||
return node;
|
||||
};
|
||||
const badge = (key: keyof typeof ui_locales, warn: boolean): HTMLElement =>
|
||||
createBadge(L(key), warn ? "warning" : "neutral", { size: 18 });
|
||||
if (info.고침) badges.append(badge("B08_Quantity_Link_Edited", false));
|
||||
if (info.원본없음) badges.append(badge("B08_Quantity_Link_Gone", true));
|
||||
else if (info.마스터바뀜) badges.append(badge("B08_Quantity_Link_Changed", true));
|
||||
@@ -133,7 +146,7 @@ async function paintCopyState(
|
||||
: info.고침
|
||||
? "is-edited"
|
||||
: null;
|
||||
if (tone) chip.classList.replace("is-linked", tone);
|
||||
if (tone) toneChip(chip, tone);
|
||||
button.disabled = !info.마스터바뀜 || !!info.원본없음;
|
||||
button.title = button.disabled
|
||||
? L("B08_Quantity_Link_UpdateSame")
|
||||
@@ -173,7 +186,7 @@ export function linkSection(
|
||||
line.append(head);
|
||||
for (const slot of facility.slots) {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = slot.linked ? "b08-link__chip is-linked" : "b08-link__chip is-missing";
|
||||
toneChip(chip, slot.linked ? "is-linked" : "is-missing");
|
||||
chip.dataset.b08LinkSlot = slot.copy;
|
||||
const name = document.createElement("span");
|
||||
name.className = "b08-link__name";
|
||||
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import {
|
||||
blankUnitRows,
|
||||
@@ -53,10 +55,7 @@ const HINTS: Record<MaterialSheetId, string> = {
|
||||
const HIDDEN = "값 없는 줄은 숨김";
|
||||
|
||||
function note(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty";
|
||||
p.textContent = text;
|
||||
return p;
|
||||
return createText(text, "muted", 14);
|
||||
}
|
||||
|
||||
/** 글 → 값 · 빈칸 = null(일부러 비움) · 수가 아니면 undefined(무시) */
|
||||
@@ -138,8 +137,8 @@ export async function renderMaterialSheet(
|
||||
|
||||
const save = createButton({ label: "저장", variant: "filled", onClick: () => void saveAll() });
|
||||
save.dataset.b08MaterialSave = "";
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.className = "b08-material__toolbar";
|
||||
const toolbar = createToolbar({ items: ["spacer", save], size: 32 });
|
||||
toolbar.classList.add("b08-material__toolbar");
|
||||
const notes = document.createElement("div");
|
||||
notes.append(footNote(sheet === "concrete" ? HINTS[sheet] : `${HINTS[sheet]} · ${HIDDEN}`));
|
||||
if (sheet !== "quantity") {
|
||||
@@ -158,7 +157,6 @@ export async function renderMaterialSheet(
|
||||
const concreteNote = footNote(""); // 콘크리트 전용 빈 까닭(163-5) — 조작마다 다시
|
||||
concreteNote.dataset.b08MaterialConcreteWhy = "";
|
||||
if (sheet === "concrete") notes.append(concreteNote);
|
||||
toolbar.append(save);
|
||||
|
||||
let built = materialTable(sheet, sheets, overrides);
|
||||
const table = createDataTable({
|
||||
|
||||
@@ -41,12 +41,6 @@
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.b08-milestone__bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b08-milestone__image {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
@@ -61,8 +55,6 @@
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.b08-milestone__diagram {
|
||||
@@ -157,14 +149,13 @@
|
||||
.b08-milestone__leg-edit {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.b08-milestone__leg-edit > .ui-float-panel__body {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.b08-milestone__leg-edit .ui-input {
|
||||
@@ -251,14 +242,7 @@
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
|
||||
/* 157 경로 탭 머리 줄 — 거래처 이름 · 주소(왼) + [그림 변경] · [초기화](오른) · 글자 = 메인 탭 크기 */
|
||||
.b08-milestone__route-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
/* 157 경로 탭 머리 줄 = 부품 도구줄(이름 · 주소 왼 + 단추 오른) */
|
||||
.b08-milestone__route-title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
|
||||
import "./B08_Quantity_UI_Milestone.css";
|
||||
import { createDataTable, type DataColumn } from "@ui/ui_template_data_table";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import type { Row } from "../B09_Estimation/B09_Estimation_Calc_Base";
|
||||
import { openHaulModal } from "../B09_Estimation/B09_Estimation_UI_Haul_Modal";
|
||||
import { openPriceLink } from "../B09_Estimation/B09_Estimation_UI_Tabs_Bill_Link";
|
||||
@@ -146,10 +148,11 @@ function tableView(projectId: string): MilestoneView {
|
||||
};
|
||||
let mode = "false:false";
|
||||
let table = makeTable(milestoneColumns(false, false));
|
||||
const none = el("p", {
|
||||
className: "ui-general-block__empty",
|
||||
text: "경로가 없습니다 — 다이어그램 공사지 · 기준점 [+]로 항목을 넣으세요.",
|
||||
});
|
||||
const none = createText(
|
||||
"경로가 없습니다 — 다이어그램 공사지 · 기준점 [+]로 항목을 넣으세요.",
|
||||
"muted",
|
||||
14,
|
||||
);
|
||||
const refresh = (): void => {
|
||||
const data = liveMilestone(projectId);
|
||||
const { options, rows, mode: next } = milestoneTable(data, siteInfo()?.mid_default_m ?? null);
|
||||
@@ -179,8 +182,7 @@ function tableView(projectId: string): MilestoneView {
|
||||
|
||||
export function renderMilestone(host: HTMLElement, projectId: string): void {
|
||||
const tabs = el("div", {
|
||||
className: "b08-quantity__tabs ui-subtabs b08-structure__tabs",
|
||||
attrs: { role: "tablist" },
|
||||
className: "b08-quantity__tabs b08-structure__tabs",
|
||||
});
|
||||
const view = el("div");
|
||||
let current = "table";
|
||||
@@ -188,22 +190,19 @@ export function renderMilestone(host: HTMLElement, projectId: string): void {
|
||||
|
||||
const drawTabs = (): void => {
|
||||
const routes = liveMilestone(projectId).routes;
|
||||
const tab = (id: string, label: string): HTMLButtonElement => {
|
||||
const button = createButton({
|
||||
label,
|
||||
variant: id === current ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => id !== current && open(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === current));
|
||||
button.dataset.b08MilestoneTab = id;
|
||||
return button;
|
||||
};
|
||||
tabs.replaceChildren(
|
||||
tab("table", "거리이정표"),
|
||||
...routes.map((r, i) => tab(r.id, r.name.trim() || r.rows[0]?.item || `거래처 ${i + 1}`)),
|
||||
);
|
||||
fillTabs(tabs, {
|
||||
tabs: [
|
||||
{ key: "table", label: "거리이정표" },
|
||||
...routes.map((r, i) => ({
|
||||
key: r.id,
|
||||
label: r.name.trim() || r.rows[0]?.item || `거래처 ${i + 1}`,
|
||||
})),
|
||||
],
|
||||
value: current,
|
||||
kind: "pill",
|
||||
data: "b08MilestoneTab",
|
||||
onChange: open,
|
||||
});
|
||||
};
|
||||
|
||||
function open(id: string): void {
|
||||
@@ -213,10 +212,7 @@ export function renderMilestone(host: HTMLElement, projectId: string): void {
|
||||
view.replaceChildren(body.root);
|
||||
}
|
||||
|
||||
host.replaceChildren(
|
||||
tabs,
|
||||
el("p", { className: "ui-general-block__empty", text: "거리이정표를 불러오는 중…" }),
|
||||
);
|
||||
host.replaceChildren(tabs, createText("거리이정표를 불러오는 중…", "muted", 14));
|
||||
loadMilestone(projectId).then(
|
||||
() => {
|
||||
host.replaceChildren(tabs, view);
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan";
|
||||
import {
|
||||
midKm,
|
||||
@@ -94,7 +96,10 @@ function editLegAt(
|
||||
km.placeholder = fmtKm(leg.auto_km); // 빈칸 = 자동 값
|
||||
km.title = `자동 값 ${fmtKm(leg.auto_km)} km`;
|
||||
}
|
||||
const box = el("div", { className: "b08-milestone__leg-edit", children: [road.root, km] });
|
||||
const pop = createFloatPanel({ popover: true });
|
||||
pop.body.append(road.root, km);
|
||||
const box = pop.root;
|
||||
box.classList.add("b08-milestone__leg-edit");
|
||||
box.dataset.b08MilestoneLegEdit = `${from}:${i}`;
|
||||
const at = anchor.getBoundingClientRect(); // 팬 · 줌 뒤 글 자리 그대로
|
||||
box.style.left = `${at.left}px`;
|
||||
@@ -300,7 +305,8 @@ export function createDiagram(
|
||||
onClick: () => openPlaceModal(projectId, { add: true }, addRoute),
|
||||
});
|
||||
addElem.dataset.b08MilestoneAdd = "open";
|
||||
const tools = el("div", { className: "b08-milestone__tools", children: [arrange, addElem] });
|
||||
const tools = createToolbar({ items: [arrange, addElem], size: 32, glass: true });
|
||||
tools.classList.add("b08-milestone__tools");
|
||||
|
||||
/** 장소 그림(254-3 · 256-1) blob 주소 — 상자마다 키(이름#시각)가 바뀔 때만 다시 받음 */
|
||||
const picUrls = new Map<string, { key: string; url: Promise<string> }>();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import {
|
||||
ROADS,
|
||||
roadOf,
|
||||
@@ -113,7 +114,7 @@ export function legsEditor(
|
||||
};
|
||||
}
|
||||
|
||||
const hint = (text: string): HTMLElement => el("p", { className: "b08-options__hint", text });
|
||||
const hint = (text: string): HTMLElement => createText(text, "muted");
|
||||
|
||||
/** 시점 → 중점 — 도로 구분 · 거리 늘 손 입력(빈칸 = 노선 ÷ 2) */
|
||||
function openMidLeg(projectId: string): void {
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
type MapContextMenuItem,
|
||||
} from "@ui/ui_template_context_menu";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import type { MilestoneData, MilestoneRoute } from "./B08_Quantity_Milestone_Calc";
|
||||
import { chainOf, ownerOf, reachesSite } from "./B08_Quantity_Milestone_Layout";
|
||||
import { DEST_TEXT } from "./B08_Quantity_Milestone_Table";
|
||||
@@ -76,6 +78,13 @@ export interface MilestoneView {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/** 머리 줄 — 이름 · 주소(왼) + 단추(오른) = 부품 도구줄 */
|
||||
function routeHead(title: HTMLElement, buttons: HTMLElement[]): HTMLElement {
|
||||
const bar = createToolbar({ items: [title, "spacer", ...buttons], size: 32 });
|
||||
bar.classList.add("b08-milestone__route-head");
|
||||
return bar;
|
||||
}
|
||||
|
||||
export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
// 157 머리 줄 = 거래처 이름 · 주소(왼) + [그림 변경] · [초기화](오른) · 글자 = 메인 탭 크기
|
||||
const name = el("strong", { className: "b08-milestone__route-name" });
|
||||
@@ -155,11 +164,11 @@ export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
return [routeId, ...chain].map((id) => segmentPath(projectId, id)).filter((p) => p.length >= 2);
|
||||
};
|
||||
// 255-2 길찾기 실패 = 안내 한 줄(까닭 줄 = 「 — 」)
|
||||
const status = el("p", { className: "b08-options__hint" });
|
||||
const status = createText("", "muted");
|
||||
status.dataset.b08MilestoneRouteStatus = "";
|
||||
status.hidden = true;
|
||||
// 373 안 이은 기준점에 이음 = 시점까지 안 닿음 안내(이은 거래처만)
|
||||
const open = el("p", { className: "b08-options__hint" });
|
||||
const open = createText("", "muted");
|
||||
open.dataset.b08MilestoneRouteOpen = "";
|
||||
open.hidden = true;
|
||||
/** 길 선 여럿 다시 길찾기(그림 캡처 없음 · 캡처는 [저장] 때) → 선 다시 · 실패 안내 */
|
||||
@@ -207,13 +216,12 @@ export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
className: "b08-milestone__route ui-card",
|
||||
attrs: { "data-b08-milestone-route": routeId },
|
||||
children: [
|
||||
el("div", {
|
||||
className: "b08-milestone__route-head",
|
||||
children: [
|
||||
el("div", { className: "b08-milestone__route-title", children: [name, address] }),
|
||||
el("div", { className: "b08-milestone__bar", children: [place, change, reset, file] }),
|
||||
],
|
||||
}),
|
||||
routeHead(el("div", { className: "b08-milestone__route-title", children: [name, address] }), [
|
||||
place,
|
||||
change,
|
||||
reset,
|
||||
file,
|
||||
]),
|
||||
status,
|
||||
open,
|
||||
img,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { clampPct } from "./B08_Quantity_Options_Calc";
|
||||
@@ -97,10 +98,7 @@ function card(id: string, title: keyof typeof ui_locales, ...body: HTMLElement[]
|
||||
}
|
||||
|
||||
function hint(text: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "b08-options__hint";
|
||||
p.textContent = text;
|
||||
return p;
|
||||
return createText(text, "muted");
|
||||
}
|
||||
|
||||
/** 좌측 패널 — 맨 끝 `.ui-sidebar-actions` 줄([저장] · [?] 자리). `onSaved` = 열린 탭 다시 그림.
|
||||
|
||||
@@ -7,16 +7,22 @@
|
||||
padding: var(--spacing-16) var(--spacing-24); /* M02 본문(.m02-main)과 같은 값 */
|
||||
}
|
||||
|
||||
.b08-quantity .ui-general-block__empty {
|
||||
color: var(--color-text-secondary); /* muted 는 밝은 테마에서 대비 3:1 — 4.5:1 로 */
|
||||
/* 부품 보조 글은 display:flex 라 hidden 을 이김 — 숨김 켜고 끄는 글이 많아 한 번에 */
|
||||
.b08-quantity .ui-text[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 탭 줄 모양 = 부품 탭(밑줄 · 보조 알약) · 여기는 줄바꿈 · 아래 틈만 */
|
||||
.b08-quantity__tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b08-quantity > .b08-quantity__tabs {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b08-quantity__tabs.ui-subtabs {
|
||||
padding-bottom: var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* ☰ 들여쓰기(7-2) */
|
||||
@@ -31,6 +37,7 @@ html.is-bm-route .b08-quantity > .b08-quantity__tabs:first-child {
|
||||
|
||||
/* 236-1 표 아래 안내 · 경고 줄 — 늘 한 줄(줄임표 · 잘리면 공용 호버로 전문) */
|
||||
.b08-quantity__foot {
|
||||
display: block; /* 부품 보조 글은 flex — 줄임표는 블록에서 */
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -39,19 +46,11 @@ html.is-bm-route .b08-quantity > .b08-quantity__tabs:first-child {
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 자재 집계표 셋(121) — 안내 글 · [저장] 한 줄 */
|
||||
/* 자재 집계표 셋(121) — [저장] 한 줄 = 부품 도구줄 · 아래 틈만 */
|
||||
.b08-material__toolbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-16);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b08-material__toolbar > .ui-btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* 구조물 · 자재 집계표 머리 — 좁은 열(44~100)에서 말줄임 대신 줄바꿈(120-2 · 131 · 공용 표 CSS 는 그대로) */
|
||||
[data-b08-summary="table"] thead th,
|
||||
[data-b08-material] thead th {
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
import "./B08_Quantity_UI_Page.css";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import { moveStage, WORKFLOW_STEP_ROUTES, type StageStep } from "../A00_Common/b_workflow_nav";
|
||||
import { renderStructureSummary } from "./B08_Quantity_UI_Summary";
|
||||
import { renderMaterialSheet } from "./B08_Quantity_UI_Material";
|
||||
@@ -53,33 +54,22 @@ type SheetId = (typeof SHEETS)[number]["id"];
|
||||
type TabSave = (host: HTMLElement, save: () => Promise<boolean>) => void;
|
||||
|
||||
function pendingNote(): HTMLElement {
|
||||
const pending = document.createElement("p");
|
||||
pending.className = "ui-general-block__empty";
|
||||
pending.textContent = L("B08_Quantity_Tab_Pending");
|
||||
return pending;
|
||||
return createText(L("B08_Quantity_Tab_Pending"), "muted", 14);
|
||||
}
|
||||
|
||||
/** 자재 탭 — 보조 탭 줄 + 몸통(보조 탭마다 새 몸통 · 늦은 응답이 다른 보조 탭을 덮지 않게). */
|
||||
function renderMaterials(host: HTMLElement, projectId: string | null, onSave: TabSave): void {
|
||||
const tabs = document.createElement("div");
|
||||
tabs.className = "b08-quantity__tabs ui-subtabs b08-structure__tabs";
|
||||
tabs.setAttribute("role", "tablist");
|
||||
tabs.className = "b08-quantity__tabs b08-structure__tabs";
|
||||
const view = document.createElement("div");
|
||||
const select = (picked: SheetId): void => {
|
||||
tabs.replaceChildren(
|
||||
...SHEETS.map(({ id, label }) => {
|
||||
const button = createButton({
|
||||
label: L(label),
|
||||
variant: id === picked ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => id !== picked && select(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === picked));
|
||||
button.dataset.b08StructureSheet = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
fillTabs(tabs, {
|
||||
tabs: SHEETS.map(({ id, label }) => ({ key: id, label: L(label) })),
|
||||
value: picked,
|
||||
kind: "pill",
|
||||
data: "b08StructureSheet",
|
||||
onChange: (id) => select(id as SheetId),
|
||||
});
|
||||
const body = document.createElement("div");
|
||||
view.replaceChildren(body);
|
||||
if (!projectId) body.append(pendingNote());
|
||||
@@ -95,13 +85,21 @@ function renderTab(host: HTMLElement, tab: TabId, projectId: string | null, onSa
|
||||
host.replaceChildren(view);
|
||||
if (tab === "material") return renderMaterials(view, projectId, onSave);
|
||||
if (tab === "summary" && projectId) {
|
||||
// 보조 탭 줄 자리(170-4) — 보이지 않는 sm 단추 한 줄 · 자재 탭과 같은 높이라 탭 바꿀 때 안 튐
|
||||
// 보조 탭 줄 자리(170-4) — 보이지 않는 보조 탭 한 칸 · 자재 탭과 같은 높이라 탭 바꿀 때 안 튐
|
||||
const blank = document.createElement("div");
|
||||
blank.className = "b08-quantity__tabs ui-subtabs b08-quantity__tabs--blank";
|
||||
blank.className = "b08-quantity__tabs b08-quantity__tabs--blank";
|
||||
fillTabs(blank, {
|
||||
tabs: [{ key: SHEETS[0].id, label: L(SHEETS[0].label) }],
|
||||
value: SHEETS[0].id,
|
||||
kind: "pill",
|
||||
data: "b08StructureSheet",
|
||||
onChange: () => undefined,
|
||||
}).forEach((button) => {
|
||||
button.tabIndex = -1;
|
||||
delete button.dataset.b08StructureSheet;
|
||||
});
|
||||
blank.setAttribute("aria-hidden", "true");
|
||||
const ghost = createButton({ label: L(SHEETS[0].label), variant: "ghost", size: "sm" });
|
||||
ghost.tabIndex = -1;
|
||||
blank.append(ghost);
|
||||
blank.setAttribute("role", "presentation");
|
||||
const body = document.createElement("div");
|
||||
view.append(blank, body);
|
||||
return void renderStructureSummary(body, projectId);
|
||||
@@ -118,7 +116,6 @@ export function renderB08Quantity(root: HTMLElement): void {
|
||||
|
||||
const tabBar = document.createElement("div");
|
||||
tabBar.className = "b08-quantity__tabs";
|
||||
tabBar.setAttribute("role", "tablist");
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "b08-quantity__panel";
|
||||
panel.setAttribute("role", "tabpanel");
|
||||
@@ -128,19 +125,14 @@ export function renderB08Quantity(root: HTMLElement): void {
|
||||
const onTabSave: TabSave = (host, save) => (tabSave = { host, save });
|
||||
const select = (tab: TabId): void => {
|
||||
current = tab;
|
||||
tabBar.replaceChildren(
|
||||
...TABS.map(({ id, label }) => {
|
||||
const button = createButton({
|
||||
label: L(label),
|
||||
variant: id === tab ? "filled" : "ghost",
|
||||
onClick: () => id !== tab && select(id),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(id === tab));
|
||||
button.dataset.b08Tab = id;
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
fillTabs(tabBar, {
|
||||
tabs: TABS.map(({ id, label }) => ({ key: id, label: L(label) })),
|
||||
value: tab,
|
||||
kind: "line",
|
||||
size: 32,
|
||||
data: "b08Tab",
|
||||
onChange: (id) => select(id as TabId),
|
||||
});
|
||||
renderTab(panel, tab, projectId, onTabSave);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createCard, showToast } from "@ui/ui_template_elements";
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
@@ -39,10 +40,7 @@ function L(key: keyof typeof ui_locales): string {
|
||||
}
|
||||
|
||||
function note(text: string): HTMLElement {
|
||||
const node = document.createElement("p");
|
||||
node.className = "ui-general-block__empty";
|
||||
node.textContent = text;
|
||||
return node;
|
||||
return createText(text, "muted", 14);
|
||||
}
|
||||
|
||||
function buildTable(
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
import { tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
|
||||
|
||||
@@ -60,9 +62,8 @@ export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => voi
|
||||
refresh.classList.toggle("is-pending", Boolean(next.stale));
|
||||
const made = checkItems(sheets, next);
|
||||
items = made.items;
|
||||
const head = document.createElement("p");
|
||||
head.className = "b09-check__head";
|
||||
head.textContent = `연결 없음 ${items.length}곳`;
|
||||
const head = createText(`연결 없음 ${items.length}곳`, "muted");
|
||||
head.classList.add("b09-check__head");
|
||||
const rows = document.createElement("div");
|
||||
rows.className = "ui-sidebar-list";
|
||||
rows.append(
|
||||
@@ -73,18 +74,16 @@ export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => voi
|
||||
row.dataset.i = String(i);
|
||||
const name = document.createElement("span");
|
||||
name.textContent = item.name;
|
||||
const tab = document.createElement("span");
|
||||
tab.className = "b09-check__tab";
|
||||
tab.textContent = item.tab;
|
||||
const tab = createBadge(item.tab, "neutral", { size: 18 });
|
||||
tab.classList.add("b09-check__tab");
|
||||
row.append(name, tab);
|
||||
row.addEventListener("click", () => goto(item.link));
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
const notes = made.notes.map((text) => {
|
||||
const p = document.createElement("p");
|
||||
p.className = "b09-check__note";
|
||||
p.textContent = text;
|
||||
const p = createText(text, "muted");
|
||||
p.classList.add("b09-check__note");
|
||||
return p;
|
||||
});
|
||||
list.replaceChildren(head, ...(items.length ? [rows] : []), ...notes);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* 스위치 값 = pref global(계정 공용 · [저장] 필요 없음) · 바닥 고정 = 공용 `.ui-sidebar-actions` 한 벌
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { DRAFT } from "./B09_Estimation_UI_Tabs";
|
||||
import {
|
||||
@@ -40,7 +40,7 @@ function setLinkMode(mode: LinkMode): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** 단추 둘 스위치 — 켜진 쪽 filled · aria-pressed */
|
||||
/** 단추 둘 스위치 — 부품 「분할 고르기」(32) · 고른 쪽 aria-checked */
|
||||
function switchRow<T extends string>(
|
||||
name: string,
|
||||
values: T[],
|
||||
@@ -48,32 +48,14 @@ function switchRow<T extends string>(
|
||||
current: () => T,
|
||||
pick: (value: T) => void,
|
||||
): HTMLElement {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ui-btn-group b09-dock__row";
|
||||
row.dataset.b09Switch = name;
|
||||
const buttons = values.map((value) => {
|
||||
const button = createButton({
|
||||
label: label(value),
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
if (value === current()) return;
|
||||
pick(value);
|
||||
sync();
|
||||
},
|
||||
});
|
||||
button.dataset.value = value;
|
||||
return button;
|
||||
const segment = createSegment({
|
||||
options: values.map((value) => ({ value, label: label(value) })),
|
||||
value: current(),
|
||||
size: 32,
|
||||
onChange: (value) => pick(value as T),
|
||||
});
|
||||
const sync = (): void =>
|
||||
buttons.forEach((button, k) => {
|
||||
const on = values[k] === current();
|
||||
button.classList.toggle("ui-btn--filled", on);
|
||||
button.classList.toggle("ui-btn--ghost", !on);
|
||||
button.setAttribute("aria-pressed", String(on));
|
||||
});
|
||||
sync();
|
||||
row.append(...buttons);
|
||||
return row;
|
||||
segment.root.dataset.b09Switch = name;
|
||||
return segment.root;
|
||||
}
|
||||
|
||||
/** 하단 고정 줄 — onView = 보기 바뀜(묶음 탭이면 다시 그림) */
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
}
|
||||
.b09-haulstep {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface-raised);
|
||||
.b09-haulstep.ui-card {
|
||||
padding: 0; /* 부품 카드 · 머리 · 바닥 줄이 가장자리까지 */
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ const group = (label: string, rows: StepRow[], ats?: number[]): HTMLElement[] =>
|
||||
/** 칸 하나 — 제목 줄(제목 · 곁글) · 몸 · 바닥(굵게) */
|
||||
function box(title: string, sub: string, body: HTMLElement[], foot: HTMLElement[]): HTMLElement {
|
||||
return el("section", {
|
||||
className: "b09-haulstep",
|
||||
className: "b09-haulstep ui-card",
|
||||
children: [
|
||||
el("div", {
|
||||
className: "b09-haulstep__head",
|
||||
|
||||
@@ -8,22 +8,17 @@
|
||||
padding: var(--spacing-16) var(--spacing-24); /* B08 본문과 같은 값 */
|
||||
}
|
||||
|
||||
.b09-estimation .ui-general-block__empty,
|
||||
.b09-estimation__hint {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.b09-estimation__hint {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 탭 줄 모양 = 부품 탭(밑줄 · 보조 알약) · 여기는 줄바꿈 · 아래 틈만(B08 과 같은 값) */
|
||||
.b09-estimation__tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8); /* B08 위 · 보조 탭 줄과 같은 값(181-1) */
|
||||
}
|
||||
|
||||
.b09-estimation > .b09-estimation__tabs:first-child {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b09-estimation__tabs.ui-subtabs {
|
||||
padding-bottom: var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* ☰ 들여쓰기(7-2) */
|
||||
@@ -59,12 +54,13 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
|
||||
}
|
||||
|
||||
/* 202-1 안내 줄 = 표 · 카드 판 아래 · 203 줄 수 변수만큼 보임 · 넘치면 영역 안 스크롤 */
|
||||
.b09-estimation .ui-data-table + .ui-general-block__empty,
|
||||
.b09-estimation .ui-group-cards + .ui-general-block__empty {
|
||||
.b09-estimation .ui-data-table + .ui-text,
|
||||
.b09-estimation .ui-group-cards + .ui-text {
|
||||
margin: var(--spacing-8) 0 0;
|
||||
}
|
||||
|
||||
.b09-estimation__line-hint {
|
||||
display: block; /* 부품 보조 글은 flex — 줄 수 · 스크롤은 블록에서 */
|
||||
min-width: 0;
|
||||
max-height: calc(var(--b09-hint-lines) * 1lh);
|
||||
overflow-y: auto;
|
||||
@@ -192,15 +188,11 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
|
||||
/* 연결 점검 목록 — 이름 · 탭(작게) · 알림 글 */
|
||||
.b09-check__head,
|
||||
.b09-check__note {
|
||||
margin: var(--spacing-8) 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
margin-top: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b09-check__tab {
|
||||
flex: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 줄 끌기(204-1) — 놓을 자리 · 레벨 미리 보기 선 · 못 놓는 자리 = 빨간 점선 */
|
||||
@@ -232,15 +224,11 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
|
||||
background: var(--color-canvas);
|
||||
}
|
||||
|
||||
.b09-estimation__drop-tag {
|
||||
.b09-estimation__drop > .b09-estimation__drop-tag.ui-badge {
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
padding: 1px var(--spacing-8);
|
||||
border: 1px solid;
|
||||
border-radius: var(--radius-pills);
|
||||
border: 1px solid; /* 놓을 자리 색(--drop-c)을 따름 */
|
||||
background: var(--color-surface-raised);
|
||||
font-size: var(--text-caption);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b09-estimation [data-b09-table="bill"] tbody tr.is-drop-into td {
|
||||
@@ -266,44 +254,43 @@ html.is-bm-route .b09-estimation > .b09-estimation__tabs:first-child {
|
||||
.b09-dock .b09-dock__row {
|
||||
display: flex;
|
||||
gap: var(--btn-group-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 보기 · 행 누름 스위치 = 부품 분할 고르기 · 줄 폭을 나눠 가짐 */
|
||||
.b09-dock > .ui-segment {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.b09-dock > .ui-segment > .ui-segment__item {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 줄 폭을 나눠 가짐 · 가로 넘침 없음(min-width 0 = 공용 단추 줄의 auto 보다 앞섬 · 글 줄임표) — 인쇄 미리보기 좌측 줄도 같은 규칙 */
|
||||
.b09-dock .b09-dock__row > .ui-btn:not(.ui-help-btn, .ui-step-nav-btn) {
|
||||
flex: 1 1 0;
|
||||
min-width: auto;
|
||||
min-width: 0;
|
||||
padding-inline: var(--spacing-4);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 303-2 행 고름 팝업 — 누른 줄 곁 표 조각 창(겹쳐 쌓임 · 창 안 b09 표 꼴 그대로 · 빨강 테두리) */
|
||||
.b09-dock .b09-dock__row > .ui-btn[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 303-2 행 고름 팝업 — 누른 줄 곁 표 조각 창(겹쳐 쌓임) = 부품 떠 있는 판 · 여기는 자리 · 크기만 */
|
||||
.b09-estimation.b09-popup {
|
||||
position: fixed;
|
||||
z-index: var(--z-modal);
|
||||
gap: var(--spacing-4);
|
||||
width: max-content;
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.b09-popup__head {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.b09-popup__title {
|
||||
margin-right: auto;
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.b09-popup .ui-data-table {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { createDataTable, type DataTableOptions } from "@ui/ui_template_data_table";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { placeHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { tableOptionsOf } from "./B09_Estimation_UI_Builders";
|
||||
import { rowIndex, tabName, type Link, type TabId, type TabTable } from "./B09_Estimation_UI_Tabs";
|
||||
@@ -135,35 +136,28 @@ export function openRowPopup(o: PopupOpen): void {
|
||||
if (!spec || !range) return o.go(o.link); // 보일 줄 없음 = 옮김
|
||||
listen();
|
||||
const level = stack.length;
|
||||
const box = document.createElement("div");
|
||||
box.className = "b09-estimation b09-popup ui-popup";
|
||||
// 떠 있는 판(부품) — 머리 = 제목 + [탭 이동] + 닫기 ✕ · 몸 = 표 조각
|
||||
const panel = createFloatPanel({ title: tabName(tab), onClose: () => closePopups(level) });
|
||||
const box = panel.root;
|
||||
box.classList.add("b09-estimation", "b09-popup", "ui-popup");
|
||||
box.setAttribute("role", "dialog");
|
||||
box.setAttribute("aria-label", tabName(tab));
|
||||
box.dataset.b09Popup = tab;
|
||||
const title = document.createElement("span");
|
||||
title.className = "b09-popup__title";
|
||||
title.textContent = tabName(tab);
|
||||
const head = box.querySelector<HTMLElement>(".ui-float-panel__head") as HTMLElement;
|
||||
head.classList.add("b09-popup__head");
|
||||
const closeButton = head.querySelector<HTMLElement>(".ui-icon-btn") as HTMLElement;
|
||||
closeButton.dataset.b09PopupClose = "";
|
||||
const goButton = createButton({
|
||||
label: "탭 이동",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
size: 24,
|
||||
onClick: () => {
|
||||
closePopups();
|
||||
o.go(o.link);
|
||||
},
|
||||
});
|
||||
goButton.dataset.b09PopupGo = "";
|
||||
const closeButton = createButton({
|
||||
label: "×",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => closePopups(level),
|
||||
});
|
||||
closeButton.dataset.b09PopupClose = "";
|
||||
closeButton.setAttribute("aria-label", "닫기");
|
||||
const head = document.createElement("div");
|
||||
head.className = "b09-popup__head";
|
||||
head.append(title, goButton, closeButton);
|
||||
closeButton.before(goButton);
|
||||
const from = range[0];
|
||||
const table = createDataTable({
|
||||
...popupOptions(spec, tab, range),
|
||||
@@ -181,7 +175,7 @@ export function openRowPopup(o: PopupOpen): void {
|
||||
},
|
||||
});
|
||||
table.root.dataset.b09Table = tab;
|
||||
box.append(head, table.root);
|
||||
panel.body.append(table.root);
|
||||
document.body.append(box);
|
||||
stack.push(box);
|
||||
const view = { width: window.innerWidth, height: window.innerHeight };
|
||||
|
||||
@@ -14,6 +14,7 @@ import "@ui/ui_template_general_layout.css";
|
||||
import "./B09_Estimation_UI_Page.css";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import {
|
||||
createButton,
|
||||
hideLoadingOverlay,
|
||||
@@ -79,10 +80,7 @@ async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
}
|
||||
|
||||
function note(textValue: string): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-general-block__empty";
|
||||
p.textContent = textValue;
|
||||
return p;
|
||||
return createText(textValue, "muted", 14);
|
||||
}
|
||||
|
||||
/** 산근(145-1) = 서버 단독 — 받은 그대로 두는 칸(+ sg_hand = 손 줄 로직 → 산근 키 · 166-2) */
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_TabBar.ts
|
||||
* B09 탭 줄 두 층(PLAN 181-1) — 위 탭(md) · 아래 탭(sm) · B08 수량산출 페이지 꼴.
|
||||
* B09 탭 줄 두 층(PLAN 181-1) — 위 탭(밑줄 · 32) · 아래 탭(보조 알약) · B08 수량산출 페이지 꼴(부품 「탭」).
|
||||
*
|
||||
* 위 탭 = `TAB_GROUPS`(`…_UI_Tabs`) · 아래 탭 하나뿐인 위 탭 = 보이지 않는 sm 단추 한 줄(높이만 채움)
|
||||
* 위 탭 = `TAB_GROUPS`(`…_UI_Tabs`) · 아래 탭 하나뿐인 위 탭 = 보이지 않는 보조 탭 한 칸(높이만 채움)
|
||||
* 위 탭 누름 = 그 묶음에서 마지막으로 본 아래 탭(이 화면 안 Map · 저장 안 함) · 없으면 첫 아래 탭
|
||||
* 탭 이동은 `draw(탭)` — 이동이 묶음을 넘어도 위 탭이 따라 켜짐
|
||||
* 보기 단추 · 가로 열 수(202-1) = 아래 줄 오른쪽 맞춤 자리(`setTools`) · 빈 줄에서도 보임
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createToolbar } from "@ui/ui_template_toolbar";
|
||||
import { fillTabs } from "../A00_Common/b_page_tabs";
|
||||
import { groupOf, tabName, TAB_GROUPS, type TabId } from "./B09_Estimation_UI_Tabs";
|
||||
|
||||
export interface TabBars {
|
||||
@@ -20,58 +21,49 @@ export interface TabBars {
|
||||
setTools: (tools: HTMLElement[]) => void;
|
||||
}
|
||||
|
||||
function tabButton(label: string, on: boolean, size: "sm" | undefined, pick: () => void) {
|
||||
const button = createButton({
|
||||
label,
|
||||
variant: on ? "filled" : "ghost",
|
||||
...(size ? { size } : {}),
|
||||
onClick: () => !on && pick(),
|
||||
});
|
||||
button.setAttribute("role", "tab");
|
||||
button.setAttribute("aria-selected", String(on));
|
||||
return button;
|
||||
}
|
||||
|
||||
export function createTabBars(onPick: (tab: TabId) => void): TabBars {
|
||||
const top = document.createElement("div");
|
||||
top.className = "b09-estimation__tabs";
|
||||
top.setAttribute("role", "tablist");
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "b09-estimation__tabs ui-subtabs";
|
||||
const tools = document.createElement("div");
|
||||
tools.className = "b09-estimation__tools ui-subtabs__tools";
|
||||
sub.className = "b09-estimation__tabs";
|
||||
const tools = createToolbar({ items: [], size: 24 });
|
||||
tools.classList.add("b09-estimation__tools", "ui-subtabs__tools");
|
||||
const lastSeen = new Map<TabId, TabId>();
|
||||
|
||||
const draw = (tab: TabId): void => {
|
||||
const [, picked] = groupOf(tab);
|
||||
lastSeen.set(picked[0], tab);
|
||||
top.replaceChildren(
|
||||
...TAB_GROUPS.map(([label, tabs]) => {
|
||||
const button = tabButton(label, tabs === picked, undefined, () =>
|
||||
onPick(lastSeen.get(tabs[0]) ?? tabs[0]),
|
||||
);
|
||||
button.dataset.b09Group = tabs[0];
|
||||
if (tabs.length === 1) button.dataset.b09Tab = tabs[0];
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
const groupTab = new Map(TAB_GROUPS.map(([, tabs]) => [tabs[0], tabs]));
|
||||
fillTabs(top, {
|
||||
tabs: TAB_GROUPS.map(([label, tabs]) => ({ key: tabs[0], label })),
|
||||
value: picked[0],
|
||||
kind: "line",
|
||||
size: 32,
|
||||
data: "b09Group",
|
||||
onChange: (key) => onPick(lastSeen.get(key as TabId) ?? (key as TabId)),
|
||||
}).forEach((button) => {
|
||||
const tabs = groupTab.get(button.dataset.tab as TabId);
|
||||
if (tabs?.length === 1) button.dataset.b09Tab = tabs[0];
|
||||
});
|
||||
const single = picked.length === 1;
|
||||
sub.classList.toggle("b09-estimation__tabs--blank", single);
|
||||
const buttons = fillTabs(sub, {
|
||||
tabs: single
|
||||
? [{ key: picked[0], label: tabName(picked[0]) }]
|
||||
: picked.map((id) => ({ key: id, label: tabName(id) })),
|
||||
value: single ? picked[0] : tab,
|
||||
kind: "pill",
|
||||
data: "b09Tab",
|
||||
onChange: (id) => onPick(id as TabId),
|
||||
});
|
||||
sub.setAttribute("role", single ? "presentation" : "tablist");
|
||||
if (single) {
|
||||
const ghost = createButton({ label: tabName(picked[0]), variant: "ghost", size: "sm" });
|
||||
ghost.tabIndex = -1;
|
||||
ghost.setAttribute("aria-hidden", "true"); // 도구는 보이는 줄이라 줄 전체를 숨기지 않음
|
||||
return void sub.replaceChildren(ghost, tools);
|
||||
}
|
||||
sub.replaceChildren(
|
||||
...picked.map((id) => {
|
||||
const button = tabButton(tabName(id), id === tab, "sm", () => onPick(id));
|
||||
button.dataset.b09Tab = id;
|
||||
return button;
|
||||
}),
|
||||
tools,
|
||||
);
|
||||
if (single)
|
||||
for (const button of buttons) {
|
||||
button.tabIndex = -1;
|
||||
button.setAttribute("aria-hidden", "true"); // 도구는 보이는 줄이라 줄 전체를 숨기지 않음
|
||||
delete button.dataset.b09Tab;
|
||||
}
|
||||
sub.append(tools);
|
||||
};
|
||||
const setTools = (items: HTMLElement[]): void => tools.replaceChildren(...items);
|
||||
return { top, sub, draw, setTools };
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* 잡기 = 글 칸 · 단추 밖 아무 칸 · 6px 넘게 움직여야 끌기(그냥 누름 = 줄 고름)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
|
||||
@@ -68,8 +69,8 @@ export function attachDrag(root: HTMLElement, table: BillTable, done: (key: stri
|
||||
const step = parseFloat(getComputedStyle(tr).getPropertyValue("--dt-indent")) || 14;
|
||||
const line = document.createElement("div");
|
||||
line.className = "b09-estimation__drop";
|
||||
const tag = document.createElement("span");
|
||||
tag.className = "b09-estimation__drop-tag";
|
||||
const tag = createBadge("", "neutral", { size: 18 });
|
||||
tag.classList.add("b09-estimation__drop-tag");
|
||||
line.append(tag);
|
||||
const rowEl = (i: number): HTMLElement | null => root.querySelector(`tr[data-i="${i}"]`);
|
||||
let into: HTMLElement | null = null;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { attachDrag } from "./B09_Estimation_UI_Tabs_Bill_Drag";
|
||||
import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
@@ -149,9 +150,8 @@ export function levelBar(
|
||||
button.dataset.b09Level = label;
|
||||
return button;
|
||||
});
|
||||
const guide = document.createElement("p");
|
||||
guide.className = "ui-general-block__empty b09-estimation__line-hint";
|
||||
guide.textContent = GUIDE;
|
||||
const guide = createText(GUIDE, "muted");
|
||||
guide.classList.add("b09-estimation__line-hint");
|
||||
attachHoverInfo(guide, (el) => (el.scrollWidth > el.clientWidth ? el.textContent : null)); // 236-2 잘릴 때만
|
||||
const sync = (): void => {
|
||||
const acts = menuActs(table, index());
|
||||
|
||||
@@ -10,14 +10,7 @@
|
||||
}
|
||||
|
||||
.b09-print__status {
|
||||
margin: 0;
|
||||
min-height: 1.4em;
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.b09-print__status.is-error {
|
||||
color: var(--color-danger);
|
||||
min-height: 1.4em; /* 글이 비어도 줄 높이 유지 */
|
||||
}
|
||||
|
||||
.b09-print__frame {
|
||||
@@ -30,9 +23,7 @@
|
||||
}
|
||||
|
||||
.b09-print__areas {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
flex-wrap: wrap; /* 부품 보조 탭 줄 */
|
||||
}
|
||||
|
||||
.b09-print__list {
|
||||
@@ -42,39 +33,3 @@
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.b09-print__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
font-size: var(--text-body-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 좌측 하단 두 줄(302-5 · B09 Dock 꼴) — 단추는 줄 폭을 나눠 가짐 · 가로 넘침 없음 */
|
||||
.b09-print__actions.ui-sidebar-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.b09-print__actions.ui-sidebar-actions > * {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.b09-print__actions .b09-print__row {
|
||||
display: flex;
|
||||
gap: var(--btn-group-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-sidebar-actions.b09-print__actions .b09-print__row > .ui-btn:not(.ui-help-btn) {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding-inline: var(--spacing-4);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b09-print__actions .b09-print__row > .ui-btn[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -11,14 +11,18 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import "@ui/ui_template_general_layout.css";
|
||||
import "../B09_Estimation_UI_Page.css"; // 좌측 하단 단추 줄(b09-dock) 규칙 한 벌
|
||||
import "./B09_Draft_Print_Page.css";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import type { MountedPage } from "../../A00_Common/router";
|
||||
import { navigateTo } from "../../A00_Common/router";
|
||||
import { workflowSteps } from "../../A00_Common/b_page_scaffold";
|
||||
import { fillTabs } from "../../A00_Common/b_page_tabs";
|
||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../../A00_Common/b_workflow_nav";
|
||||
import { inlineImages, pdfBody, xlsxBody } from "./B09_Draft_Print_Table";
|
||||
import type {
|
||||
@@ -114,8 +118,8 @@ function save(blob: Blob, name: string): void {
|
||||
export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
|
||||
const status = document.createElement("p");
|
||||
status.className = "b09-print__status";
|
||||
const status = createText("", "muted");
|
||||
status.className += " b09-print__status";
|
||||
status.dataset.b09PrintStatus = "";
|
||||
const frame = document.createElement("iframe");
|
||||
frame.className = "b09-print__frame";
|
||||
@@ -144,6 +148,8 @@ export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
const say = (text: string, error = false) => {
|
||||
status.textContent = text;
|
||||
status.classList.toggle("is-error", error);
|
||||
status.classList.toggle("ui-text--error", error);
|
||||
status.classList.toggle("ui-text--muted", !error);
|
||||
};
|
||||
const picked = () => (area?.items ?? []).map((it) => it.id).filter((id) => !off.has(id));
|
||||
const clearFrame = () => {
|
||||
@@ -213,21 +219,16 @@ export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
const drawList = () => {
|
||||
list.replaceChildren(
|
||||
...(area?.items ?? []).map((it) => {
|
||||
const row = document.createElement("label");
|
||||
row.className = "b09-print__item";
|
||||
const box = document.createElement("input");
|
||||
box.type = "checkbox";
|
||||
box.checked = !off.has(it.id);
|
||||
box.dataset.b09PrintItem = it.id;
|
||||
box.addEventListener("change", () => {
|
||||
if (box.checked) off.delete(it.id);
|
||||
else off.add(it.id);
|
||||
later();
|
||||
const row = createCheckbox(it.name, {
|
||||
checked: !off.has(it.id),
|
||||
onChange: (on) => {
|
||||
if (on) off.delete(it.id);
|
||||
else off.add(it.id);
|
||||
later();
|
||||
},
|
||||
});
|
||||
const name = document.createElement("span");
|
||||
name.textContent = it.name;
|
||||
row.append(box, name);
|
||||
return row;
|
||||
row.input.dataset.b09PrintItem = it.id;
|
||||
return row.root;
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -238,18 +239,13 @@ export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
off = new Set();
|
||||
const my = ++seq;
|
||||
clearFrame();
|
||||
areaRow.replaceChildren(
|
||||
...AREAS.map((a) => {
|
||||
const b = createButton({
|
||||
label: a.label,
|
||||
size: "sm",
|
||||
variant: a.id === id ? "filled" : "ghost",
|
||||
onClick: () => a.id !== areaId && pickArea(a.id),
|
||||
});
|
||||
b.dataset.b09PrintArea = a.id;
|
||||
return b;
|
||||
}),
|
||||
);
|
||||
fillTabs(areaRow, {
|
||||
tabs: AREAS.map((a) => ({ key: a.id, label: a.label })),
|
||||
value: id,
|
||||
kind: "pill",
|
||||
data: "b09PrintArea",
|
||||
onChange: (key) => pickArea(key as AreaId),
|
||||
});
|
||||
xlsxBtn.hidden = id === "drawings";
|
||||
drawList();
|
||||
if (!projectId) return say("현재 프로젝트가 없습니다.", true);
|
||||
@@ -270,14 +266,14 @@ export function renderB09Print(root: HTMLElement): MountedPage {
|
||||
|
||||
// 두 줄(302-5) — 위 = 내려받기 · 아래 = [?](도움말 줄) · 돌아가기 — 패널 폭 넘침 없음
|
||||
const files = document.createElement("div");
|
||||
files.className = "b09-print__row";
|
||||
files.className = "b09-dock__row b09-print__row";
|
||||
files.append(pdfBtn, xlsxBtn);
|
||||
const nav = document.createElement("div");
|
||||
nav.className = "b09-print__row";
|
||||
nav.className = "b09-dock__row b09-print__row";
|
||||
nav.dataset.helpRow = "";
|
||||
nav.append(back);
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "ui-sidebar-actions b09-print__actions";
|
||||
actions.className = "ui-sidebar-actions b09-dock b09-print__actions";
|
||||
actions.append(files, nav);
|
||||
const left = document.createElement("div");
|
||||
left.className = "ui-sidebar-panel b09-print__panel";
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -87,6 +87,19 @@ let layoutOpts = null;
|
||||
let confirmAnswer = false;
|
||||
const store = new Map(); // 가짜 세션 캐시
|
||||
const common = {
|
||||
"../A00_Common/b_page_tabs": {
|
||||
// 탭 줄 부품 흉내 — 칸마다 dataset[data] = 열쇠 · 지금 칸 누름은 안 부름
|
||||
fillTabs: (host, o) => {
|
||||
const kids = o.tabs.map((t) =>
|
||||
Object.assign(new Node("button"), {
|
||||
onClick: () => t.key !== o.value && o.onChange(t.key),
|
||||
dataset: { [o.data]: t.key },
|
||||
}),
|
||||
);
|
||||
host.replaceChildren(...kids);
|
||||
return kids;
|
||||
},
|
||||
},
|
||||
"@ui/ui_template_workflow_layout": {
|
||||
createWorkflowLayout: (opts) => {
|
||||
layoutOpts = opts;
|
||||
|
||||
@@ -445,11 +445,16 @@ const out = {};
|
||||
|
||||
// ── 3. 단추 줄(318-1) — 빈자리 누름 = 카드 고름 · 조작 누름 = 안 넘김 ───────────
|
||||
{
|
||||
// 단추 줄 = 부품 단추 · 분할 · 켜기(553-2) — 진짜 부품 파일을 흉내 노드로 돌림
|
||||
const UI = (name) => path.join(ROOT, "ui_template", `${name}.ts`);
|
||||
const parts = ["ui_template_button", "ui_template_segment", "ui_template_toggle"];
|
||||
const loadUi = makeLoader([...parts, "ui_template_elements_base"].map(UI));
|
||||
const load = makeLoader([B06("B06_Section_UI_Cross_Design")], {
|
||||
"@ui/ui_template_locale": {
|
||||
currentLanguageIndex: 0,
|
||||
ui_locales: new Proxy({}, { get: (_t, key) => [String(key)] }),
|
||||
},
|
||||
...Object.fromEntries(parts.map((name) => [`@ui/${name}`, loadUi(UI(name))])),
|
||||
});
|
||||
const { buildDesignControls } = load(B06("B06_Section_UI_Cross_Design"));
|
||||
const { bar } = buildDesignControls(
|
||||
|
||||
@@ -155,10 +155,13 @@ const walk = (node) => {
|
||||
walk(groundSegment);
|
||||
walk(bar);
|
||||
const press = (pick) => buttons.find(pick)?.listeners.click?.forEach((fn) => fn({}));
|
||||
// 부품 켜기 칩(553-2) — 글은 안쪽 span 에 · 자식 글을 이어 읽음
|
||||
const text = (node) =>
|
||||
typeof node === "string"
|
||||
? node
|
||||
: (node?.textContent ?? "") + (node?.children ?? []).map(text).join("");
|
||||
out.soilPressed = buttons[0]?.getAttribute("aria-pressed");
|
||||
press(
|
||||
(b) => b.textContent === "측구" || b.textContent === "측구 없음" || /측구/.test(b.textContent),
|
||||
);
|
||||
press((b) => /측구/.test(text(b)));
|
||||
out.ditchChange = sent.at(-1) ?? null;
|
||||
// 새 카드 · 토사 단추 — 지반만(측구형식은 이미 일반이라 안 실림)
|
||||
buttons.length = 0;
|
||||
|
||||
@@ -81,6 +81,19 @@ const calls = [];
|
||||
let layoutOpts = null;
|
||||
const store = new Map(); // 가짜 세션 캐시 — 이름 → 값
|
||||
const common = {
|
||||
"../A00_Common/b_page_tabs": {
|
||||
// 탭 줄 부품 흉내 — 칸마다 dataset[data] = 열쇠 · 지금 칸 누름은 안 부름
|
||||
fillTabs: (host, o) => {
|
||||
const kids = o.tabs.map((t) =>
|
||||
Object.assign(new Node("button"), {
|
||||
onClick: () => t.key !== o.value && o.onChange(t.key),
|
||||
dataset: { [o.data]: t.key },
|
||||
}),
|
||||
);
|
||||
host.replaceChildren(...kids);
|
||||
return kids;
|
||||
},
|
||||
},
|
||||
"@ui/ui_template_workflow_layout": {
|
||||
createWorkflowLayout: (opts) => {
|
||||
layoutOpts = opts;
|
||||
|
||||
@@ -15,7 +15,7 @@ COMMON = "ui_template/ui_template_group_cards.ts"
|
||||
TABS = "B09_Estimation/B09_Estimation_UI_Tabs.ts"
|
||||
GROUPS = "B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts"
|
||||
|
||||
# 기억(b_page_state) 대신 Map · 단추(createButton) 대신 가짜 button · 호버(attachHoverInfo) = 부른 자리 기록 · 157-3 원문번호 풍선(raw) 짝
|
||||
# 기억(b_page_state) 대신 Map · 탭 줄(fillTabs) 대신 가짜 button 줄 · 단추(createButton) 대신 가짜 button · 호버(attachHoverInfo) = 부른 자리 기록 · 157-3 원문번호 풍선(raw) 짝
|
||||
STUBS = r"""
|
||||
const kept = new Map();
|
||||
const readState = (name) => (kept.has(name) ? JSON.parse(kept.get(name)) : null);
|
||||
@@ -27,6 +27,50 @@ const createButton = (o) => {
|
||||
if (o.onClick) b.addEventListener("click", o.onClick);
|
||||
return b;
|
||||
};
|
||||
const fillTabs = (host, o) => {
|
||||
const kids = o.tabs.map((t) => {
|
||||
const b = document.createElement("button");
|
||||
b.textContent = t.label;
|
||||
b.dataset[o.data] = t.key;
|
||||
b.addEventListener("click", () => t.key !== o.value && o.onChange(t.key));
|
||||
return b;
|
||||
});
|
||||
host.replaceChildren(...kids);
|
||||
return kids;
|
||||
};
|
||||
const createSegment = (o) => {
|
||||
const root = document.createElement("div");
|
||||
let current = o.value;
|
||||
const items = o.options.map((x) => {
|
||||
const b = document.createElement("button");
|
||||
b.textContent = x.label;
|
||||
b.dataset.value = x.value;
|
||||
b.addEventListener("click", () => {
|
||||
if (current === x.value) return;
|
||||
current = x.value;
|
||||
mark();
|
||||
o.onChange(x.value);
|
||||
});
|
||||
return b;
|
||||
});
|
||||
const mark = () => items.forEach((b) => b.setAttribute("aria-checked", String(b.dataset.value === current)));
|
||||
mark();
|
||||
root.append(...items);
|
||||
return { root, get: () => current, set: (v) => { current = v; mark(); } };
|
||||
};
|
||||
const createText = (text, kind) => {
|
||||
const p = document.createElement("p");
|
||||
p.className = `ui-text ui-text--${kind}`;
|
||||
p.textContent = text;
|
||||
return p;
|
||||
};
|
||||
const createBadge = (text, tone) => {
|
||||
const span = document.createElement("span");
|
||||
span.className = `ui-badge ui-badge--${tone}`;
|
||||
span.textContent = text;
|
||||
return span;
|
||||
};
|
||||
const createToolbar = () => document.createElement("div");
|
||||
const hoverCalls = [];
|
||||
const attachHoverInfo = (root, text, selector) => { hoverCalls.push({ root, text, selector }); return () => {}; };
|
||||
"""
|
||||
|
||||
@@ -48,6 +48,7 @@ function createButton(o) {
|
||||
b.addEventListener("click", o.onClick);
|
||||
return b;
|
||||
}
|
||||
function createText(text) { const p = new FakeEl("p"); p.textContent = text; return p; }
|
||||
function attachHoverInfo(el, text) { hovers.push([el.textContent, text]); }
|
||||
|
||||
let user = {};
|
||||
|
||||
@@ -150,11 +150,13 @@ def test_B08_표_카드_차례_표_아래줄_안내():
|
||||
def test_B08_자재_구조물_안내_줄_표_아래():
|
||||
material = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Material.ts").read_text("utf-8")
|
||||
assert "createCard({ body: [toolbar, table.root, excel, notes] })" in material
|
||||
assert "toolbar.append(save);" in material # 저장 단추만 위 줄(안내는 아래)
|
||||
assert (
|
||||
'createToolbar({ items: ["spacer", save], size: 32 })' in material
|
||||
) # 저장 단추만 위 줄(안내는 아래)
|
||||
summary = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Summary.ts").read_text("utf-8")
|
||||
assert "createCard({ body: [table, ...parts] })" in summary
|
||||
css = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Page.css").read_text("utf-8")
|
||||
assert "justify-content: flex-end" in _block(css, ".b08-material__toolbar")
|
||||
assert "margin-bottom" in _block(css, ".b08-material__toolbar") # 줄 모양 = 부품 도구줄(spacer)
|
||||
|
||||
|
||||
# ── 창 끝까지 셈(199-1)에 아래 줄 몫 ────────────────────────────────────────
|
||||
|
||||
@@ -161,7 +161,7 @@ def test_메인_탭_하위_탭_추가_삭제():
|
||||
"ui_template/ui_template_locale_b4.ts"
|
||||
)
|
||||
ui = _read("B08_Quantity/B08_Quantity_UI_Milestone.ts")
|
||||
assert 'tab("table", "거리이정표")' in ui and "routes.map((r, i) => tab(r.id," in ui
|
||||
assert '{ key: "table", label: "거리이정표" }' in ui and "...routes.map((r, i) => ({" in ui
|
||||
# 244-4 — 하위 탭 줄 [+ 추가] · [삭제] 없음(탭은 [+] 등록 · 항목 정보 [삭제]로만)
|
||||
assert 'label: "+ 추가"' not in ui and "removeCurrent" not in ui
|
||||
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
|
||||
|
||||
@@ -172,6 +172,8 @@ def test_157_도구_왼쪽_아래_경로_탭_지도_그림_변경_초기화():
|
||||
assert 'label: "그림 변경"' in route and 'label: "초기화"' in route
|
||||
assert "그림 지우기" not in route and "자동 그림으로 되돌리기" not in route.split("*/", 1)[1]
|
||||
assert "map.root.hidden = user;" in route and "editMap({" in route # 그림 없으면 길찾기 지도
|
||||
assert ".b08-milestone__route-head {" in css and "justify-content: space-between;" in css
|
||||
assert (
|
||||
'createToolbar({ items: [title, "spacer", ...buttons], size: 32 })' in route
|
||||
) # 머리 줄 = 부품 도구줄
|
||||
state = _read("B08_Quantity/B08_Quantity_UI_Milestone_State.ts")
|
||||
assert "export const hasUserImage" in state and "canRevertImage" not in state
|
||||
|
||||
@@ -434,9 +434,13 @@ def test_좌측_하단_두_줄_가로_넘침_없음():
|
||||
assert "files.append(pdfBtn, xlsxBtn)" in page
|
||||
assert "nav.dataset.helpRow" in page and "nav.append(back)" in page
|
||||
assert "actions.append(files, nav)" in page
|
||||
css = re.sub(r"\s+", " ", _text(DRAFT / "B09_Draft_Print_Page.css"))
|
||||
assert ".b09-print__actions.ui-sidebar-actions { flex-direction: column; }" in css
|
||||
btn = css[css.index(".ui-sidebar-actions.b09-print__actions .b09-print__row > .ui-btn") :]
|
||||
# 553-3 — 좌측 하단 줄 = B09 dock 한 벌(복사 없음) · 줄 폭을 나눠 가짐 · 글 줄임표
|
||||
assert 'actions.className = "ui-sidebar-actions b09-dock b09-print__actions"' in page
|
||||
assert 'className = "b09-dock__row b09-print__row"' in page
|
||||
css = re.sub(r"\s+", " ", _text(ROOT / "B09_Estimation/B09_Estimation_UI_Page.css"))
|
||||
assert ".b09-dock.ui-sidebar-actions { flex-direction: column; }" in css
|
||||
btn = css[css.index(".b09-dock .b09-dock__row > .ui-btn:not(") :]
|
||||
btn = btn[: btn.index("}")]
|
||||
# 공용 단추 줄(min-width: auto = 글보다 안 좁아짐)보다 앞서 이기는 규칙 · 글 줄임표
|
||||
assert "flex: 1 1 0" in btn and "min-width: 0" in btn and "text-overflow: ellipsis" in btn
|
||||
assert ".b09-print__actions" not in _text(DRAFT / "B09_Draft_Print_Page.css")
|
||||
|
||||
@@ -78,7 +78,7 @@ def test_좌측_스위치_누름():
|
||||
"\nlet redraws = 0;"
|
||||
"\nconst dock = createDock([document.createElement('button')], () => redraws++);"
|
||||
"\nconst row = dock.children[0];"
|
||||
"\nconst pressed = () => row.children.map((b) => b.attrs['aria-pressed']);"
|
||||
"\nconst pressed = () => row.children.map((b) => b.attrs['aria-checked']);"
|
||||
"\nconst labels = row.children.map((b) => b.textContent);"
|
||||
"\nconst a = pressed();"
|
||||
"\nrow.children[0].click();" # 이미 박스 = 그대로
|
||||
|
||||
@@ -145,3 +145,60 @@ 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"]
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
/* 단추 = 단추 부품(단 24) · 여기는 넷이 폭을 나눔만 */
|
||||
.cad-frame-edit__buttons > button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* ui_template/cad_host/cad_host_tabs — CAD 를 감싸는 공용 탭 줄(82-1 · M02 구조물 도면 · B07). */
|
||||
/* ui_template/cad_host/cad_host_tabs — CAD 를 감싸는 공용 탭 줄(82-1 · M02 구조물 도면 · B07).
|
||||
문서 탭 = 꼴 그대로 · 공통 값 · 크기 단만(탭 24 · × 18 · + 22 = 아이콘 단추 부품 · 553-6). */
|
||||
|
||||
.cad-tabs {
|
||||
display: flex;
|
||||
@@ -18,8 +19,10 @@
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
box-sizing: border-box;
|
||||
max-width: 220px;
|
||||
padding: 4px 8px;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-bottom: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
|
||||
Reference in New Issue
Block a user