Merge remote-tracking branches 'origin/sub_laptop_5' and 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -109,7 +109,7 @@
|
||||
bottom: 40px;
|
||||
z-index: 20;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-buttons);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
@@ -126,6 +126,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ss-brush:hover {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
.ss-brush.is-on {
|
||||
background: color-mix(in srgb, var(--color-focus-ring) 20%, var(--color-surface-raised));
|
||||
box-shadow: inset 0 0 0 2px var(--color-focus-ring);
|
||||
@@ -135,19 +139,17 @@
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
.ss-find {
|
||||
position: absolute;
|
||||
/* 찾기 = 떠 있는 판 부품(머리 없음) · 칸 · 단추 = 부품 — 여기는 자리 · 줄 배치만(553-6) */
|
||||
.ss-find.ui-float-panel {
|
||||
top: 4px;
|
||||
right: 16px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.ss-find > .ui-float-panel__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.ss-find[hidden],
|
||||
@@ -166,32 +168,17 @@
|
||||
|
||||
.ss-find__input {
|
||||
width: 180px;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text-body);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.ss-find__btn {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-body);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ss-find__opt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 12px;
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ss-find__status {
|
||||
font-size: 12px;
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@@ -223,19 +210,11 @@
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.ss-pick > button {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-body);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 함수 자동 완성 · 인자 도움말 */
|
||||
.ss-hint {
|
||||
position: absolute;
|
||||
@@ -265,22 +244,17 @@
|
||||
}
|
||||
|
||||
.ss-hint__item.is-on {
|
||||
background: color-mix(in srgb, var(--color-accent) 18%, var(--color-surface-raised));
|
||||
background: var(--color-accent-soft);
|
||||
}
|
||||
|
||||
/* 데이터 메뉴 — 패널 창 · 유효성 목록 ▼ */
|
||||
.ss-dialog {
|
||||
position: absolute;
|
||||
/* 패널 창 = 떠 있는 판 부품 — 여기는 자리 · 크기만(553-6) */
|
||||
.ss-dialog.ui-float-panel {
|
||||
top: 4px;
|
||||
right: 16px;
|
||||
z-index: 21;
|
||||
max-width: min(720px, calc(100% - 32px));
|
||||
max-height: calc(100% - 8px);
|
||||
overflow: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.ss-dialog[hidden],
|
||||
@@ -288,20 +262,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ss-dialog__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ss-dialog__body {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.ss-list-arrow {
|
||||
position: absolute;
|
||||
z-index: 8;
|
||||
|
||||
@@ -118,10 +118,15 @@ export function dialogKeys(overlay: HTMLElement, close: () => void, run?: () =>
|
||||
});
|
||||
}
|
||||
|
||||
/** body 에 뜬 창(.ui-confirm)이 다 닫히면 초점을 격자로 — 창을 연 바로 뒤에 부름 */
|
||||
/** body 에 뜬 창(.ui-confirm · 모달)이 다 닫히면 초점을 격자로 — 창을 연 바로 뒤에 부름 */
|
||||
export function watchClose(back: () => void): void {
|
||||
const w = new MutationObserver(() => {
|
||||
if (document.querySelector("body > .ui-confirm, body > .ui-confirm__panel")) return;
|
||||
if (
|
||||
document.querySelector(
|
||||
"body > .ui-confirm, body > .ui-confirm__panel, body > .ui-modal__backdrop",
|
||||
)
|
||||
)
|
||||
return;
|
||||
w.disconnect();
|
||||
back();
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgb(0 0 0 / 18%);
|
||||
background: var(--color-scrim);
|
||||
}
|
||||
|
||||
.ss-steps__box {
|
||||
@@ -115,18 +115,10 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ss-errtip__btn {
|
||||
/* ⚠ = 아이콘 단추 부품(선 · 18) — 여기는 자리 · 경고 색만(553-6) */
|
||||
.ss-errtip__btn.ui-icon-btn {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
cursor: pointer;
|
||||
color: var(--color-warning);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.ss-errtip__balloon {
|
||||
@@ -183,5 +175,5 @@
|
||||
}
|
||||
|
||||
.ss-errtip__item:hover {
|
||||
background: var(--color-mist-violet);
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import "./spreadsheet_stage3.css"; // sub4 패널 스타일(잇는 쪽이 붙임)
|
||||
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { parseRange, rangeToA1, toA1 } from "./spreadsheet_address";
|
||||
import { autoSumCells, autoSumRange } from "./spreadsheet_autosum";
|
||||
import { isError } from "./spreadsheet_eval";
|
||||
@@ -40,7 +41,6 @@ const TEXT = {
|
||||
validation: "데이터 유효성…",
|
||||
names: "이름 관리…",
|
||||
grid: "눈금선 보이기 · 숨기기",
|
||||
close: "닫기",
|
||||
list: "값 목록(Alt+↓)",
|
||||
sheets: "시트 숨기기 · 탭 색…",
|
||||
audit: "수식 분석",
|
||||
@@ -121,18 +121,14 @@ export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMe
|
||||
};
|
||||
|
||||
// ── 패널 창(조건부 서식 · 유효성 · 이름) — 한 번에 하나 ─────────────────────
|
||||
const title = el("span", { className: "ss-dialog__title" });
|
||||
const body = el("div", { className: "ss-dialog__body" });
|
||||
const shut = el("button", {
|
||||
className: "ss-find__btn",
|
||||
text: "×",
|
||||
attrs: { type: "button", title: TEXT.close },
|
||||
});
|
||||
const dialog = el("div", {
|
||||
className: "ss-dialog ui-popup",
|
||||
attrs: { hidden: "" },
|
||||
children: [el("div", { className: "ss-dialog__head", children: [title, shut] }), body],
|
||||
// 떠 있는 판 부품(머리 · 닫기 ✕ · 몸 · 553-6) — 자리는 `.ss-dialog`
|
||||
const { root: dialog, body } = createFloatPanel({
|
||||
title: " ",
|
||||
onClose: () => (closePanel(), back()),
|
||||
});
|
||||
dialog.classList.add("ss-dialog");
|
||||
dialog.setAttribute("hidden", "");
|
||||
const title = dialog.querySelector(".ui-float-panel__title") as HTMLElement;
|
||||
ctx.root.append(dialog);
|
||||
let panel: PartHandle | null = null;
|
||||
const closePanel = (): void => {
|
||||
@@ -141,7 +137,6 @@ export function mountDataMenu(ctx: SpreadsheetContext, back: () => void): DataMe
|
||||
body.replaceChildren();
|
||||
dialog.setAttribute("hidden", "");
|
||||
};
|
||||
shut.addEventListener("click", () => (closePanel(), back()));
|
||||
dialog.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Escape") return;
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* 시트 전체를 훑지 말고 그리는 칸만 물음(값 하나 · 칸 하나 읽는 값싼 부름).
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createIconButton, el } from "@ui/ui_template_elements";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { toA1 } from "./spreadsheet_address";
|
||||
import { isError } from "./spreadsheet_eval";
|
||||
@@ -137,11 +137,9 @@ export function attachErrorTip(ctx: SpreadsheetContext, trace?: TraceHandle): Er
|
||||
const g = ctx.grid.root.getBoundingClientRect();
|
||||
layer.style.left = `${box.x - (a.left - g.left) - 20}px`;
|
||||
layer.style.top = `${box.y - (a.top - g.top)}px`;
|
||||
const btn = el("button", {
|
||||
className: "ss-errtip__btn",
|
||||
text: "⚠",
|
||||
attrs: { type: "button", "aria-label": w.까닭 },
|
||||
});
|
||||
const btn = createIconButton({ glyph: "⚠", title: w.까닭, variant: "outline", size: 18 });
|
||||
btn.classList.add("ss-errtip__btn");
|
||||
btn.removeAttribute("title"); // 까닭은 옆 풍선이 보임
|
||||
const balloon = el("div", { className: "ss-errtip__balloon", text: w.까닭 });
|
||||
btn.addEventListener("mousedown", (ev) => ev.stopPropagation());
|
||||
btn.addEventListener("click", () => (menu ? closeMenu() : openMenu(w, r, c)));
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 빨간 세모는 C 격자 · 우클릭 [메모 …] 는 E 메뉴 몫(브레인 배정).
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { watchClose } from "./spreadsheet_actions";
|
||||
import { parseClipboard } from "./spreadsheet_clipboard";
|
||||
import { attachComments, getComment, setComment } from "./spreadsheet_comments";
|
||||
@@ -138,10 +138,8 @@ export function attachExtras(
|
||||
: undefined;
|
||||
picker.replaceChildren(
|
||||
...(Object.keys(TEXT.modes) as PasteSpecialMode[]).map((mode) => {
|
||||
const b = el("button", {
|
||||
text: TEXT.modes[mode],
|
||||
attrs: { type: "button", "data-mode": mode },
|
||||
});
|
||||
const b = createButton({ label: TEXT.modes[mode], variant: "ghost", size: 22 });
|
||||
b.dataset.mode = mode;
|
||||
b.addEventListener("click", () => {
|
||||
picker.setAttribute("hidden", "");
|
||||
// 늦게 — 크기 예산
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* Enter = 다음 · Esc = 닫고 격자로. 바꾸기 = 활성 칸이 맞으면 바꾸고 다음으로(엑셀).
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createButton, createIconButton, el, setUiSize } from "@ui/ui_template_elements";
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { findAll, findNext, replaceAll, replaceOne, type FindOptions } from "./spreadsheet_find";
|
||||
import { selectCell } from "./spreadsheet_selection";
|
||||
import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types";
|
||||
@@ -29,12 +30,14 @@ export interface FindPanelHandle extends PartHandle {
|
||||
}
|
||||
|
||||
export function mountFindPanel(ctx: SpreadsheetContext, back: () => void): FindPanelHandle {
|
||||
const query = el("input", { className: "ss-find__input", attrs: { placeholder: TEXT.find } });
|
||||
const repl = el("input", { className: "ss-find__input", attrs: { placeholder: TEXT.replace } });
|
||||
const field = (placeholder: string): HTMLInputElement =>
|
||||
setUiSize(el("input", { className: "ui-input ss-find__input", attrs: { placeholder } }), 24);
|
||||
const query = field(TEXT.find);
|
||||
const repl = field(TEXT.replace);
|
||||
const status = el("span", { className: "ss-find__status" });
|
||||
const button = (text: string, onClick: () => void): HTMLButtonElement => {
|
||||
const b = el("button", { className: "ss-find__btn", text, attrs: { type: "button" } });
|
||||
b.addEventListener("click", onClick);
|
||||
const b = createButton({ label: text, variant: "ghost", size: 22, onClick });
|
||||
b.classList.add("ss-find__btn");
|
||||
return b;
|
||||
};
|
||||
const check = (text: string): HTMLInputElement => {
|
||||
@@ -42,8 +45,8 @@ export function mountFindPanel(ctx: SpreadsheetContext, back: () => void): FindP
|
||||
options.append(el("label", { className: "ss-find__opt", children: [box, text] }));
|
||||
return box;
|
||||
};
|
||||
const closeBtn = button("×", close);
|
||||
closeBtn.title = TEXT.close;
|
||||
const closeBtn = createIconButton({ glyph: "×", title: TEXT.close, size: 22, onClick: close });
|
||||
closeBtn.classList.add("ss-find__btn");
|
||||
const options = el("div", { className: "ss-find__row" });
|
||||
const caseBox = check(TEXT.case);
|
||||
const wholeBox = check(TEXT.whole);
|
||||
@@ -52,19 +55,19 @@ export function mountFindPanel(ctx: SpreadsheetContext, back: () => void): FindP
|
||||
className: "ss-find__row",
|
||||
children: [repl, button(TEXT.one, one), button(TEXT.all, all)],
|
||||
});
|
||||
const root = el("div", {
|
||||
className: "ss-find",
|
||||
attrs: { hidden: "" },
|
||||
children: [
|
||||
el("div", {
|
||||
className: "ss-find__row",
|
||||
children: [query, button(TEXT.next, next), closeBtn],
|
||||
}),
|
||||
replaceRow,
|
||||
options,
|
||||
status,
|
||||
],
|
||||
});
|
||||
// 떠 있는 판 부품(머리 없음 · 553-6) — 자리는 `.ss-find`
|
||||
const { root, body } = createFloatPanel({ popover: true });
|
||||
root.classList.add("ss-find");
|
||||
root.setAttribute("hidden", "");
|
||||
body.append(
|
||||
el("div", {
|
||||
className: "ss-find__row",
|
||||
children: [query, button(TEXT.next, next), closeBtn],
|
||||
}),
|
||||
replaceRow,
|
||||
options,
|
||||
status,
|
||||
);
|
||||
ctx.root.append(root);
|
||||
|
||||
const opts = (): FindOptions => ({
|
||||
|
||||
@@ -1,50 +1,9 @@
|
||||
/* =============================================================================
|
||||
* spreadsheet_format_dialog.css (2단계 · 주인 sub_laptop_1)
|
||||
* 셀 서식 대화상자 — 뿌리 배경(`.ui-confirm`)은 공용 확인 모달과 같은 것을 그대로 씀.
|
||||
* 셀 서식 대화상자 — 창 · 탭 · 칸 = 부품 모달(40rem) · 탭(32) · 입력 · 고르기(28) · 여기는 안 배치만(553-6).
|
||||
* 색 · 간격은 테마 토큰만(`ui_template_theme.css`).
|
||||
* ========================================================================== */
|
||||
|
||||
.ss-fmtdlg__panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(560px, 92vw);
|
||||
max-height: 84vh;
|
||||
padding: var(--spacing-16);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.ss-fmtdlg__title {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-body);
|
||||
}
|
||||
|
||||
.ss-fmtdlg__tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ss-fmtdlg__tabbtn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-bottom: 0;
|
||||
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||
background: var(--color-paper);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ss-fmtdlg__tabbtn.is-active {
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-text-body);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ss-fmtdlg__body {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
@@ -68,21 +27,7 @@
|
||||
width: 140px;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.ss-fmtdlg__row select,
|
||||
.ss-fmtdlg__row input[type="text"],
|
||||
.ss-fmtdlg__row input:not([type]),
|
||||
.ss-fmtdlg__row input[type="number"],
|
||||
.ss-fmtdlg__code {
|
||||
height: 28px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-text-body);
|
||||
font-size: 0.82rem;
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.ss-fmtdlg__code {
|
||||
@@ -135,7 +80,7 @@
|
||||
.ss-fmtdlg__note {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ss-fmtdlg__actions {
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
* 우클릭 메뉴 [셀 서식] 도 같은 함수를 부르면 됨(`spreadsheet_menu.ts`).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { createButton, el, setUiSize } from "@ui/ui_template_elements";
|
||||
import { createTabs } from "@ui/ui_template_tabs";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { toFrac } from "@ui/sheet/ui_template_sheet_frac";
|
||||
import { colName, rangeToA1, toA1 } from "./spreadsheet_address";
|
||||
import {
|
||||
@@ -39,6 +41,7 @@ const TEXT = {
|
||||
tabProtect: "보호",
|
||||
ok: "확인",
|
||||
cancel: "취소",
|
||||
close: "닫기",
|
||||
category: "범주",
|
||||
decimals: "소수 자릿수",
|
||||
thousands: "천 단위 구분 기호 사용",
|
||||
@@ -105,17 +108,9 @@ export function openFormatDialog(ctx: SpreadsheetContext): void {
|
||||
const spansMany = range.r0 !== range.r1 || range.c0 !== range.c1;
|
||||
let mergeWanted = (ctx.sheet().병합 ?? []).includes(rangeToA1(range));
|
||||
|
||||
const overlay = el("div", { className: "ui-confirm ss-fmtdlg" });
|
||||
const panel = el("div", { className: "ss-fmtdlg__panel" });
|
||||
const tabsBar = el("div", { className: "ss-fmtdlg__tabs" });
|
||||
const body = el("div", { className: "ss-fmtdlg__body" });
|
||||
overlay.append(panel);
|
||||
panel.append(el("h2", { className: "ss-fmtdlg__title", text: TEXT.title }), tabsBar, body);
|
||||
|
||||
const close = (): void => overlay.remove();
|
||||
overlay.addEventListener("click", (ev) => {
|
||||
if (ev.target === overlay) close();
|
||||
});
|
||||
let close = (): void => {};
|
||||
|
||||
// ── 표시 형식 ────────────────────────────────────────────────────────
|
||||
let category: Category = guessCategory(draft.형식);
|
||||
@@ -464,22 +459,25 @@ export function openFormatDialog(ctx: SpreadsheetContext): void {
|
||||
[TEXT.tabProtect, protectTab],
|
||||
];
|
||||
|
||||
function showTab(index: number): void {
|
||||
body.replaceChildren(tabs[index][1]());
|
||||
tabsBar.querySelectorAll(".ss-fmtdlg__tabbtn").forEach((btn, i) => {
|
||||
btn.classList.toggle("is-active", i === index);
|
||||
});
|
||||
}
|
||||
|
||||
tabs.forEach(([label], i) => {
|
||||
const btn = el("button", {
|
||||
className: "ss-fmtdlg__tabbtn",
|
||||
text: label,
|
||||
attrs: { type: "button" },
|
||||
});
|
||||
btn.addEventListener("click", () => showTab(i));
|
||||
tabsBar.append(btn);
|
||||
const tabBar = createTabs({
|
||||
tabs: tabs.map(([label], i) => ({ key: String(i), label })),
|
||||
value: "0",
|
||||
size: 32,
|
||||
onChange: (key) => showTab(Number(key)),
|
||||
});
|
||||
tabsBar.append(tabBar.root);
|
||||
|
||||
function showTab(index: number): void {
|
||||
const tab = tabs[index][1]();
|
||||
// 칸 = 부품 입력 · 고르기 28(553-6)
|
||||
const fields = "select, input:not([type]), input[type=text], input[type=number]";
|
||||
for (const f of tab.querySelectorAll<HTMLElement>(fields)) {
|
||||
f.classList.add(f.tagName === "SELECT" ? "ui-select" : "ui-input");
|
||||
setUiSize(f, 28);
|
||||
}
|
||||
body.replaceChildren(tab);
|
||||
tabBar.select(String(index));
|
||||
}
|
||||
|
||||
function apply(): void {
|
||||
ctx.dispatch({ 종류: "서식", 시트: ctx.selection.시트, 범위: ctx.selection.범위, 바꿀: draft });
|
||||
@@ -497,13 +495,21 @@ export function openFormatDialog(ctx: SpreadsheetContext): void {
|
||||
const actions = el("div", {
|
||||
className: "ss-fmtdlg__actions",
|
||||
children: [
|
||||
createButton({ label: TEXT.cancel, variant: "ghost", onClick: close }),
|
||||
createButton({ label: TEXT.cancel, variant: "ghost", onClick: () => close() }),
|
||||
createButton({ label: TEXT.ok, variant: "filled", onClick: apply }),
|
||||
],
|
||||
});
|
||||
panel.append(actions);
|
||||
|
||||
showTab(0);
|
||||
document.body.append(overlay);
|
||||
dialogKeys(overlay, close, apply);
|
||||
openModal({
|
||||
title: TEXT.title,
|
||||
closeLabel: TEXT.close,
|
||||
width: "40rem",
|
||||
dialogClass: "ss-fmtdlg",
|
||||
mount: (box, shut) => {
|
||||
close = shut;
|
||||
box.append(tabsBar, body, actions);
|
||||
dialogKeys(box.parentElement ?? box, shut, apply);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,20 +120,11 @@
|
||||
width: 320px;
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.ss-links__search {
|
||||
padding: 4px 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-text-body);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.ss-links__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -155,7 +146,7 @@
|
||||
}
|
||||
|
||||
.ss-links__row:hover {
|
||||
background: var(--color-mist-violet);
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
.ss-links__note {
|
||||
|
||||
@@ -585,8 +585,8 @@ export function mountLinks(
|
||||
function openPick(anchor: HTMLElement): void {
|
||||
closePick();
|
||||
const input = el("input", {
|
||||
className: "ss-links__search",
|
||||
attrs: { type: "search", placeholder: TEXT.search },
|
||||
className: "ui-input ss-links__search",
|
||||
attrs: { type: "search", placeholder: TEXT.search, "data-size": "28" },
|
||||
}) as HTMLInputElement;
|
||||
const list = el("div", { className: "ss-links__list" });
|
||||
const note = (text: string) => el("div", { className: "ss-links__note", text });
|
||||
|
||||
@@ -59,15 +59,15 @@ export function attachMenu(ctx: SpreadsheetContext): PartHandle {
|
||||
/** 메모 글 입력 — 작은 오버레이(native prompt 안 씀, 자동화 창이 native 창을 자동 취소하는
|
||||
* 문제는 `showConfirmDialog` 머리 주석과 같은 까닭). 저장을 누르면 `onSave`. */
|
||||
function openCommentEditor(initial: string, onSave: (text: string) => void): void {
|
||||
const overlay = el("div", { className: "ss-comment-editor" });
|
||||
const panel = el("div", { className: "ss-comment-editor__panel" });
|
||||
const overlay = el("div", { className: "ui-confirm ss-comment-editor" });
|
||||
const panel = el("div", { className: "ui-confirm__panel" });
|
||||
const textarea = el("textarea", {
|
||||
className: "ss-comment-editor__input",
|
||||
className: "ui-input ss-comment-editor__input",
|
||||
}) as HTMLTextAreaElement;
|
||||
textarea.value = initial;
|
||||
const done = (): void => overlay.remove();
|
||||
const actions = el("div", {
|
||||
className: "ss-comment-editor__actions",
|
||||
className: "ui-confirm__actions",
|
||||
children: [
|
||||
createButton({ label: "취소", variant: "ghost", onClick: done }),
|
||||
createButton({
|
||||
|
||||
@@ -100,12 +100,17 @@ export function openSortDialog(ctx: SpreadsheetContext): () => void {
|
||||
function drawKeys() {
|
||||
list.replaceChildren(
|
||||
...keys.map((k, i) => {
|
||||
const col = el("select", { attrs: { "aria-label": t("Column") } });
|
||||
const col = el("select", {
|
||||
className: "ui-select",
|
||||
attrs: { "aria-label": t("Column"), "data-size": "28" },
|
||||
});
|
||||
for (let c = rg.c0; c <= rg.c1; c++)
|
||||
col.append(el("option", { text: columnLabel(c), attrs: { value: String(c) } }));
|
||||
col.value = String(k.열);
|
||||
col.addEventListener("change", () => (k.열 = Number(col.value)));
|
||||
const dir = el("select", {
|
||||
className: "ui-select",
|
||||
attrs: { "data-size": "28" },
|
||||
children: [
|
||||
el("option", { text: t("Asc"), attrs: { value: "asc" } }),
|
||||
el("option", { text: t("Desc"), attrs: { value: "desc" } }),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgb(0 0 0 / 18%);
|
||||
background: var(--color-scrim);
|
||||
}
|
||||
|
||||
.ss-sort__box,
|
||||
@@ -48,11 +48,6 @@
|
||||
|
||||
.ss-sort__row select {
|
||||
flex: 1;
|
||||
padding: var(--spacing-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-canvas);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.ss-sort__row option {
|
||||
@@ -92,8 +87,8 @@
|
||||
}
|
||||
|
||||
.ss-filter__btn.is-on {
|
||||
color: var(--color-royal-amethyst);
|
||||
border-color: var(--color-royal-amethyst);
|
||||
color: var(--color-accent);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.ss-filter__panel {
|
||||
@@ -130,5 +125,5 @@
|
||||
}
|
||||
|
||||
.ss-filter__link:hover {
|
||||
background: var(--color-mist-violet);
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
padding: var(--spacing-8);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-cards);
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
}
|
||||
|
||||
.ss-validation__dropdown-item:hover {
|
||||
background: var(--color-mist-violet);
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
.ss-statusbar {
|
||||
|
||||
@@ -215,42 +215,13 @@
|
||||
|
||||
/* ── 메모 편집 창(칸 우클릭 [메모 삽입 · 편집]) ──────────────────────────────── */
|
||||
|
||||
.ss-comment-editor {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: var(--z-modal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-scrim);
|
||||
}
|
||||
|
||||
.ss-comment-editor__panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
width: 280px;
|
||||
padding: var(--spacing-16);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
/* 창 = 공용 확인창(.ui-confirm) · 칸 = 부품 입력(여러 줄) — 여기는 칸 크기만(553-6) */
|
||||
.ss-comment-editor__input {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-text-body);
|
||||
font: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.ss-comment-editor__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-8);
|
||||
margin-bottom: var(--spacing-16);
|
||||
}
|
||||
|
||||
/* ⋯ 칸 메뉴(기기대응 3-5) — 터치 모드에서만 */
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
* 대표 단면 별(자동 = 채움 · 사용자 켬/끔 = 밑줄 · 누르면 바꿈). 정렬 · 거름은 logic `listRows`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
||||
import { createIconButton } from "@ui/ui_template_button";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { e03Table } from "./E03_StabSection_UI_Table";
|
||||
import {
|
||||
listRows,
|
||||
roleText,
|
||||
@@ -30,91 +34,86 @@ export interface ListDraw {
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
const VERDICT_TONE: Record<string, BadgeTone> = { ok: "success", ng: "danger", none: "neutral" };
|
||||
|
||||
export function buildList(
|
||||
draw: ListDraw,
|
||||
onSelect: (key: string) => void,
|
||||
onToggle: (key: string) => void,
|
||||
): HTMLElement {
|
||||
const box = el("div", { className: "e03-list" });
|
||||
const table = el("table", { attrs: { "data-role": "station-list" } });
|
||||
const head = el("tr");
|
||||
head.append(
|
||||
el("th", { text: t("E03_Col_Station") }),
|
||||
el("th", { text: t("E03_Col_Role") }),
|
||||
...draw.conditions.map((c) =>
|
||||
el("th", {
|
||||
className: "is-num",
|
||||
text: t(`E03_Cond_${c}` as LocaleKey),
|
||||
attrs: c === draw.condition ? { "aria-sort": "other" } : {},
|
||||
}),
|
||||
),
|
||||
el("th", { text: t("E03_Verdict") }),
|
||||
el("th", { text: t("E03_Col_Rep"), attrs: { title: t("E03_Rep_Hint") } }),
|
||||
);
|
||||
table.append(el("thead", { children: [head] }));
|
||||
const body = el("tbody");
|
||||
const rows = listRows(draw.rows, draw.condition, draw.sort, draw.ngOnly);
|
||||
for (const row of rows) {
|
||||
const tr = el("tr", {
|
||||
className: row.key === draw.active ? "is-active" : "",
|
||||
attrs: { "data-key": row.key, "data-verdict": verdictOf(row, draw.condition) },
|
||||
});
|
||||
const columns = [
|
||||
{ label: t("E03_Col_Station") },
|
||||
{ label: t("E03_Col_Role") },
|
||||
...draw.conditions.map((c) => ({ label: t(`E03_Cond_${c}` as LocaleKey), num: true })),
|
||||
{ label: t("E03_Verdict") },
|
||||
{ label: t("E03_Col_Rep"), title: t("E03_Rep_Hint") },
|
||||
];
|
||||
const cells = rows.map((row) => {
|
||||
const v = verdictOf(row, draw.condition);
|
||||
const rep = draw.representative(row);
|
||||
const star = el("button", {
|
||||
className: `e03-star${rep.on ? " is-on" : ""}${rep.user !== null ? " is-user" : ""}`,
|
||||
text: rep.on ? "★" : "☆",
|
||||
attrs: {
|
||||
type: "button",
|
||||
title:
|
||||
[
|
||||
...(row.representative?.auto ?? []).map((r) => t(`E03_Rep_${r}` as LocaleKey)),
|
||||
rep.user !== null ? t("E03_Rep_User") : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ") || t("E03_Rep_Hint"),
|
||||
"aria-pressed": String(rep.on),
|
||||
const title =
|
||||
[
|
||||
...(row.representative?.auto ?? []).map((r) => t(`E03_Rep_${r}` as LocaleKey)),
|
||||
rep.user !== null ? t("E03_Rep_User") : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ") || t("E03_Rep_Hint");
|
||||
const star = createIconButton({
|
||||
glyph: rep.on ? "★" : "☆",
|
||||
title,
|
||||
size: 20,
|
||||
pressed: rep.on,
|
||||
disabled: !draw.editable,
|
||||
onClick: (ev) => {
|
||||
ev.stopPropagation();
|
||||
onToggle(row.key);
|
||||
},
|
||||
});
|
||||
star.disabled = !draw.editable;
|
||||
star.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation();
|
||||
onToggle(row.key);
|
||||
});
|
||||
tr.append(
|
||||
el("td", {
|
||||
text: row.label?.replace(/^STA\./, "").replace(/\.000$/, "") ?? row.key,
|
||||
children: row.walls
|
||||
? [el("span", { className: "e03-wall-tag", text: "▦", attrs: { title: t("E03_Walls") } })]
|
||||
: [],
|
||||
}),
|
||||
el("td", {
|
||||
text: row.sides.length
|
||||
? roleText(row, { cut: t("E03_CutShort"), fill: t("E03_FillShort") })
|
||||
: "-",
|
||||
}),
|
||||
...draw.conditions.map((c) => el("td", { className: "is-num", text: fmt(row.min[c]) })),
|
||||
el("td", {
|
||||
children: [
|
||||
el("span", {
|
||||
className: `e03-verdict e03-verdict--${v}`,
|
||||
text: row.error
|
||||
? t("E03_Error")
|
||||
: v === "none"
|
||||
? "-"
|
||||
: t(`E03_Verdict_${v}` as LocaleKey),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("td", { children: [star] }),
|
||||
star.classList.add("e03-star");
|
||||
star.classList.toggle("is-on", rep.on);
|
||||
star.classList.toggle("is-user", rep.user !== null);
|
||||
const verdict = createBadge(
|
||||
row.error ? t("E03_Error") : v === "none" ? "-" : t(`E03_Verdict_${v}` as LocaleKey),
|
||||
VERDICT_TONE[v],
|
||||
{ kind: "lamp" },
|
||||
);
|
||||
verdict.classList.add("e03-verdict", `e03-verdict--${v}`);
|
||||
const station = el("span", {
|
||||
text: row.label?.replace(/^STA\./, "").replace(/\.000$/, "") ?? row.key,
|
||||
children: row.walls
|
||||
? [el("span", { className: "e03-wall-tag", text: "▦", attrs: { title: t("E03_Walls") } })]
|
||||
: [],
|
||||
});
|
||||
return [
|
||||
station,
|
||||
row.sides.length ? roleText(row, { cut: t("E03_CutShort"), fill: t("E03_FillShort") }) : "-",
|
||||
...draw.conditions.map((c) => fmt(row.min[c])),
|
||||
verdict,
|
||||
star,
|
||||
];
|
||||
});
|
||||
const list = e03Table(columns, cells, "station-list");
|
||||
list.table.tHead?.rows[0].cells
|
||||
.item(2 + draw.conditions.indexOf(draw.condition))
|
||||
?.setAttribute("aria-sort", "other");
|
||||
list.rows.forEach((tr, i) => {
|
||||
const row = rows[i];
|
||||
tr.dataset.key = row.key;
|
||||
tr.dataset.verdict = verdictOf(row, draw.condition);
|
||||
tr.classList.toggle("is-active", row.key === draw.active);
|
||||
if (row.error) tr.title = row.error;
|
||||
tr.addEventListener("click", () => onSelect(row.key));
|
||||
body.append(tr);
|
||||
}
|
||||
table.append(body);
|
||||
box.append(table);
|
||||
if (!rows.length) box.append(el("p", { className: "e03-empty", text: t("E03_ListEmpty") }));
|
||||
});
|
||||
box.append(list.root);
|
||||
if (!rows.length) box.append(hookEmpty(t("E03_ListEmpty")));
|
||||
queueMicrotask(() => box.querySelector("tr.is-active")?.scrollIntoView({ block: "nearest" }));
|
||||
return box;
|
||||
}
|
||||
|
||||
function hookEmpty(text: string): HTMLElement {
|
||||
const empty = createEmpty({ text, small: true });
|
||||
empty.classList.add("e03-empty");
|
||||
return empty;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import { buildMissingBanner } from "../E01_StabRegister/E01_StabRegister_UI_Missing";
|
||||
import { buildList } from "./E03_StabSection_UI_List";
|
||||
import { buildPlan } from "./E03_StabSection_UI_Plan";
|
||||
import { card, check, emptyBox, muted } from "./E03_StabSection_UI_Parts";
|
||||
import { buildResult } from "./E03_StabSection_UI_Result";
|
||||
import {
|
||||
buildLegends,
|
||||
@@ -86,20 +87,12 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const repKey = (projectId: string): string => `e03_representative_cache:${projectId}`;
|
||||
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
function checkbox(label: string, checked: boolean, onChange: (v: boolean) => void): HTMLElement {
|
||||
const input = el("input", { attrs: { type: "checkbox" } });
|
||||
input.checked = checked;
|
||||
input.addEventListener("change", () => onChange(input.checked));
|
||||
return el("label", { className: "e03-check", children: [input, label] });
|
||||
}
|
||||
|
||||
export function renderE03StabSection(root: HTMLElement): void {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
const main = el("div", { className: "e03-page" });
|
||||
const panel = el("div", { className: "ui-sidebar-panel e03-panel" });
|
||||
mountGroupStep(root, ROUTES.E03_STAB_SECTION, { mainContent: main, leftPanel: panel });
|
||||
if (!projectId)
|
||||
return void main.append(el("p", { className: "e03-empty", text: t("E03_NoProject") }));
|
||||
if (!projectId) return void main.append(emptyBox(t("E03_NoProject")));
|
||||
const pid: string = projectId;
|
||||
const base = `/projects/${pid}/stab`;
|
||||
const prob = createProbPanel(pid, base, () => paintSection());
|
||||
@@ -146,11 +139,11 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
paintList();
|
||||
},
|
||||
});
|
||||
const ngBox = checkbox(t("E03_NgOnly"), false, (v) => {
|
||||
const ngBox = check(t("E03_NgOnly"), false, (v) => {
|
||||
ngOnly = v;
|
||||
paintList();
|
||||
});
|
||||
const status = el("p", { className: "e03-muted", attrs: { "data-role": "status" } });
|
||||
const status = muted("", "status");
|
||||
const listHost = el("div", {
|
||||
className: "e03-panel",
|
||||
attrs: { style: "flex: 1 1 auto; min-height: 0" },
|
||||
@@ -180,7 +173,7 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
const missingHost = el("div");
|
||||
const planHost = el("div");
|
||||
const toolbar = el("div", { className: "e03-card__head" });
|
||||
const sectionCard = el("div", { className: "e03-card" });
|
||||
const sectionCard = card();
|
||||
const legendHost = el("div");
|
||||
const canvas = createSectionCanvas((i) => {
|
||||
picked = i;
|
||||
@@ -270,9 +263,9 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
if (!view) {
|
||||
canvas.render(null);
|
||||
const text = result?.status === "ok" ? t("E03_PickStation") : t("E03_NotYet");
|
||||
resultHost.append(
|
||||
el("div", { className: "e03-card", children: [el("p", { className: "e03-empty", text })] }),
|
||||
);
|
||||
const none = card();
|
||||
none.append(emptyBox(text));
|
||||
resultHost.append(none);
|
||||
return;
|
||||
}
|
||||
const analysis = currentAnalysis(view);
|
||||
@@ -297,13 +290,9 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
});
|
||||
const flags = el("div", { className: "e03-panel__row" });
|
||||
flags.append(
|
||||
checkbox(t("E03_Show_Map"), toggles.map, (v) => ((toggles.map = v), paintSection())),
|
||||
checkbox(
|
||||
t("E03_Show_Centers"),
|
||||
toggles.centers,
|
||||
(v) => ((toggles.centers = v), paintSection()),
|
||||
),
|
||||
checkbox(
|
||||
check(t("E03_Show_Map"), toggles.map, (v) => ((toggles.map = v), paintSection())),
|
||||
check(t("E03_Show_Centers"), toggles.centers, (v) => ((toggles.centers = v), paintSection())),
|
||||
check(
|
||||
t("E03_Show_Candidates"),
|
||||
toggles.candidates,
|
||||
(v) => ((toggles.candidates = v), paintSection()),
|
||||
@@ -330,15 +319,9 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
legendHost.append(buildLegends(draw));
|
||||
const rule = view.water_rule?.[condition];
|
||||
if (rule?.mode === "ru")
|
||||
legendHost.append(
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: t("E03_Water_Ru").replace("{v}", String(rule.ru)),
|
||||
attrs: { "data-role": "water-ru" },
|
||||
}),
|
||||
);
|
||||
legendHost.append(muted(t("E03_Water_Ru").replace("{v}", String(rule.ru)), "water-ru"));
|
||||
if (view.regions.some((r) => r.material_id === WALL_ID))
|
||||
legendHost.append(el("p", { className: "e03-muted", text: t("E03_Walls_Hint") }));
|
||||
legendHost.append(muted(t("E03_Walls_Hint")));
|
||||
const row = currentRow();
|
||||
if (mode === "prob" && row) {
|
||||
resultHost.append(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/* =============================================================================
|
||||
* E03_StabSection_UI_Parts.ts
|
||||
* E03 안 부품 걸이(553-4) — 부품(`ui_template_text` · `_empty` · `_card` · `_checkbox` · `_flow_legend`)을 부르고
|
||||
* 촬영 장면 · 시험이 잡는 이름(`e03-muted` · `e03-empty` · `e03-card` · `e03-check` · `e03-legend`)만 얹음.
|
||||
* 모양은 부품 · 이 파일에 CSS 없음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createCard } from "@ui/ui_template_card";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createLegend, type LegendItem } from "@ui/ui_template_flow_legend";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
|
||||
/** 보조 글 한 줄(`data-role` 이 있으면 같이) */
|
||||
export function muted(text: string, role?: string): HTMLElement {
|
||||
const node = createText(text, "muted");
|
||||
node.classList.add("e03-muted");
|
||||
if (role) node.dataset.role = role;
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 빈 자리 안내 */
|
||||
export function emptyBox(text: string): HTMLElement {
|
||||
const node = createEmpty({ text, small: true });
|
||||
node.classList.add("e03-empty");
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 카드 판 — 속은 부르는 쪽이 채움 */
|
||||
export function card(extra = "", role?: string): HTMLDivElement {
|
||||
const node = createCard({ pad: 12 });
|
||||
node.classList.add("e03-card", ...extra.split(" ").filter(Boolean));
|
||||
if (role) node.dataset.role = role;
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 체크 한 줄 */
|
||||
export function check(
|
||||
label: string,
|
||||
checked: boolean,
|
||||
onChange: (v: boolean) => void,
|
||||
): HTMLElement {
|
||||
const box = createCheckbox(label, { checked, onChange });
|
||||
box.root.classList.add("e03-check");
|
||||
return box.root;
|
||||
}
|
||||
|
||||
/** 범례 — 색은 `var(--e03-…)`(그림 색 · 이 화면 자체 토큰) */
|
||||
export function legend(items: LegendItem[], role?: string): HTMLElement {
|
||||
const node = createLegend(items);
|
||||
node.classList.add("e03-legend");
|
||||
if (role) node.dataset.role = role;
|
||||
return node;
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
* 범례 = 판정마다 아이콘 + 글 + 측점 수. 계산은 logic `bandSegments` · `planFit`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
type Point,
|
||||
type StationRow,
|
||||
} from "./logic/E03_StabSection_Logic";
|
||||
import { card } from "./E03_StabSection_UI_Parts";
|
||||
import { svgEl } from "./E03_StabSection_UI_Section";
|
||||
|
||||
const H = 96;
|
||||
@@ -26,7 +28,7 @@ export function buildPlan(
|
||||
active: string | null,
|
||||
onSelect: (key: string) => void,
|
||||
): HTMLElement {
|
||||
const box = el("div", { className: "e03-plan e03-card" });
|
||||
const box = card("e03-plan");
|
||||
const head = el("div", { className: "e03-card__head" });
|
||||
head.append(el("h3", { className: "e03-card__title", text: t("E03_Plan") }));
|
||||
const legend = el("div", { className: "e03-legend", attrs: { "data-role": "plan-legend" } });
|
||||
@@ -34,8 +36,12 @@ export function buildPlan(
|
||||
rows.forEach((r) => (counts[verdictOf(r, condition)] += 1));
|
||||
for (const v of ["ng", "ok", "none"] as const) {
|
||||
if (!counts[v]) continue;
|
||||
const item = el("span", { className: `e03-verdict e03-verdict--${v}` });
|
||||
item.append(`${t(`E03_Verdict_${v}` as LocaleKey)} ${counts[v]}`);
|
||||
const item = createBadge(
|
||||
`${t(`E03_Verdict_${v}` as LocaleKey)} ${counts[v]}`,
|
||||
v === "ok" ? "success" : v === "ng" ? "danger" : "neutral",
|
||||
{ kind: "lamp" },
|
||||
);
|
||||
item.classList.add("e03-verdict", `e03-verdict--${v}`);
|
||||
legend.append(item);
|
||||
}
|
||||
head.append(legend);
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
width: 6.2em;
|
||||
}
|
||||
|
||||
.e03-prob-spread {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.e03-prob-run {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
|
||||
@@ -17,7 +17,12 @@ import {
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createKv } from "@ui/ui_template_kv";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { card, emptyBox, muted } from "./E03_StabSection_UI_Parts";
|
||||
import { e03Table } from "./E03_StabSection_UI_Table";
|
||||
import {
|
||||
changedCount,
|
||||
fillCache,
|
||||
@@ -125,10 +130,6 @@ export function createProbPanel(projectId: string, base: string, repaint: () =>
|
||||
}
|
||||
|
||||
function inputTable(setup: ProbSetup): HTMLElement {
|
||||
const table = el("table", {
|
||||
className: "e03-table e03-prob-vars",
|
||||
attrs: { "data-role": "prob-vars" },
|
||||
});
|
||||
const heads = [
|
||||
"E03_Pb_Use",
|
||||
"E03_Pb_Var",
|
||||
@@ -137,36 +138,36 @@ export function createProbPanel(projectId: string, base: string, repaint: () =>
|
||||
"E03_Pb_Spread",
|
||||
"E03_Pb_Trunc",
|
||||
];
|
||||
table.append(
|
||||
el("thead", {
|
||||
children: [el("tr", { children: heads.map((k) => el("th", { text: t(k as LocaleKey) })) })],
|
||||
}),
|
||||
const rows = probRows(setup);
|
||||
const list = e03Table(
|
||||
heads.map((k, i) => ({ label: t(k as LocaleKey), num: i === 2 })),
|
||||
rows.map(inputCells),
|
||||
"prob-vars",
|
||||
"e03-prob-vars",
|
||||
);
|
||||
const body = el("tbody");
|
||||
for (const row of probRows(setup)) body.append(inputRow(row));
|
||||
table.append(body);
|
||||
return table;
|
||||
list.rows.forEach((tr, i) => (tr.dataset.key = rows[i].key));
|
||||
return list.root;
|
||||
}
|
||||
|
||||
function inputRow(row: ProbRow): HTMLElement {
|
||||
function inputCells(row: ProbRow): HTMLElement[] {
|
||||
const v = cache!.vars[row.key];
|
||||
const tr = el("tr", { attrs: { "data-key": row.key } });
|
||||
const use = el("input", { attrs: { type: "checkbox", "data-field": "use" } });
|
||||
use.checked = v.use;
|
||||
use.addEventListener("change", () => ((v.use = use.checked), save()));
|
||||
const dist = el("select", {
|
||||
className: "ui-input ui-input--sm",
|
||||
attrs: { "data-field": "dist" },
|
||||
});
|
||||
for (const d of row.prop === "water" ? ["normal"] : ["normal", "lognormal"])
|
||||
dist.append(el("option", { text: t(`E03_Pb_Dist_${d}` as LocaleKey), attrs: { value: d } }));
|
||||
dist.value = v.dist;
|
||||
const use = createCheckbox("", { checked: v.use });
|
||||
use.input.dataset.field = "use";
|
||||
use.input.addEventListener("change", () => ((v.use = use.input.checked), save()));
|
||||
const dist = createSelectField({
|
||||
compact: true,
|
||||
options: (row.prop === "water" ? ["normal"] : ["normal", "lognormal"]).map((d) => ({
|
||||
value: d,
|
||||
text: t(`E03_Pb_Dist_${d}` as LocaleKey),
|
||||
})),
|
||||
value: v.dist,
|
||||
}).select;
|
||||
dist.dataset.field = "dist";
|
||||
dist.addEventListener("change", () => ((v.dist = dist.value as typeof v.dist), save()));
|
||||
const num = (value: number | null, field: string, apply: (n: number | null) => void) => {
|
||||
const input = el("input", {
|
||||
className: "ui-input ui-input--sm",
|
||||
attrs: { type: "number", step: "0.01", min: "0", "data-field": field },
|
||||
});
|
||||
const input = createInputField({ type: "number", compact: true, min: 0 }).input;
|
||||
input.step = "0.01";
|
||||
input.dataset.field = field;
|
||||
input.value = value === null ? "" : String(value);
|
||||
input.addEventListener("input", () => {
|
||||
const n = Number(input.value);
|
||||
@@ -181,20 +182,18 @@ export function createProbPanel(projectId: string, base: string, repaint: () =>
|
||||
row.prop === "water" || row.prop === "ru"
|
||||
? t(`E03_Pb_Prop_${row.prop}` as LocaleKey)
|
||||
: `${row.name} ${t(`E03_Pb_Prop_${row.prop}` as LocaleKey)}`;
|
||||
tr.append(
|
||||
el("td", { children: [use] }),
|
||||
el("td", { text: label }),
|
||||
el("td", { className: "is-num", text: row.prop === "water" ? "±0" : fmt(row.mean, 2) }),
|
||||
el("td", { children: [dist] }),
|
||||
el("td", {
|
||||
children: [
|
||||
num(v.value, "value", (n) => (v.value = n ?? 0)),
|
||||
el("span", { className: "e03-muted", text: ` ${unit}` }),
|
||||
],
|
||||
}),
|
||||
el("td", { children: [num(v.trunc, "trunc", (n) => (v.trunc = n))] }),
|
||||
);
|
||||
return tr;
|
||||
const spread = el("div", {
|
||||
className: "e03-prob-spread",
|
||||
children: [num(v.value, "value", (n) => (v.value = n ?? 0)), createText(` ${unit}`, "muted")],
|
||||
});
|
||||
return [
|
||||
use.root,
|
||||
el("span", { text: label }),
|
||||
el("span", { text: row.prop === "water" ? "±0" : fmt(row.mean, 2) }),
|
||||
dist,
|
||||
spread,
|
||||
num(v.trunc, "trunc", (n) => (v.trunc = n)),
|
||||
];
|
||||
}
|
||||
|
||||
function runFields(setup: ProbSetup, slot: string, editable: boolean): HTMLElement {
|
||||
@@ -304,12 +303,13 @@ export function createProbPanel(projectId: string, base: string, repaint: () =>
|
||||
["E03_Pb_Valid", `${s.n_valid} / ${s.n}`],
|
||||
["E03_Pb_Time", `${fmt(rec.result.elapsed_s.total, 1)} s`],
|
||||
];
|
||||
const dl = el("dl", { className: "e03-kpi", attrs: { "data-role": "prob-summary" } });
|
||||
for (const [k, v] of rows)
|
||||
dl.append(el("dt", { text: t(k) }), el("dd", { text: v, attrs: { "data-k": k } }));
|
||||
const dl = createKv(rows.map(([k, v]) => ({ label: t(k), value: v })));
|
||||
dl.classList.add("e03-kpi");
|
||||
dl.dataset.role = "prob-summary";
|
||||
dl.querySelectorAll("dd").forEach((dd, i) => dd.setAttribute("data-k", rows[i][0]));
|
||||
const r = rec.result;
|
||||
const note = `${t(`E03_Pb_Mode_${r.mode}` as LocaleKey)} · ${methodName(r.method)} · ${r.sampling.toUpperCase()} ${r.n} · seed ${r.seed} · ${rec.computed_at}`;
|
||||
return el("div", { children: [dl, el("p", { className: "e03-muted", text: note })] });
|
||||
return el("div", { children: [dl, muted(note)] });
|
||||
}
|
||||
|
||||
function sources(setup: ProbSetup, changed: number): HTMLElement {
|
||||
@@ -318,18 +318,17 @@ export function createProbPanel(projectId: string, base: string, repaint: () =>
|
||||
? t("E03_Pb_UserInput").replace("{n}", String(changed))
|
||||
: t("E03_Pb_Unconfirmed");
|
||||
d.append(el("summary", { text: tag }));
|
||||
for (const [k, v] of Object.entries(setup.defaults.sources))
|
||||
d.append(el("p", { className: "e03-muted", text: `${k} — ${v}` }));
|
||||
for (const [k, v] of Object.entries(setup.defaults.sources)) d.append(muted(`${k} — ${v}`));
|
||||
return d;
|
||||
}
|
||||
|
||||
function render(v: ProbView): HTMLElement {
|
||||
const host = el("div", { className: "e03-card e03-prob", attrs: { "data-role": "prob" } });
|
||||
const host = card("e03-prob", "prob");
|
||||
const head = (key: LocaleKey): HTMLElement =>
|
||||
el("h3", { className: "e03-card__title", text: t(key) });
|
||||
host.append(head("E03_Tab_Prob"));
|
||||
if (!v.row.representative?.on) {
|
||||
host.append(el("p", { className: "e03-empty", text: t("E03_Pb_RepOnly") }));
|
||||
host.append(emptyBox(t("E03_Pb_RepOnly")));
|
||||
return host;
|
||||
}
|
||||
const side = v.side ?? v.row.sides[0]?.side;
|
||||
@@ -339,30 +338,20 @@ export function createProbPanel(projectId: string, base: string, repaint: () =>
|
||||
if (got === undefined) queueMicrotask(() => void load(slot, v.row.key, side, v.condition));
|
||||
if (typeof got !== "object") {
|
||||
const text = typeof got === "string" && got !== "loading" ? got : t("E03_Loading");
|
||||
host.append(el("p", { className: "e03-empty", text }));
|
||||
host.append(emptyBox(text));
|
||||
return host;
|
||||
}
|
||||
cache = fillCache(got, cache);
|
||||
const sideText = `${t(side === "left" ? "E03_Left" : "E03_Right")} · ${t(got.role === "cut" ? "E03_Cut" : "E03_Fill")}`;
|
||||
host.append(
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: `${sideText} · ${t(`E03_Cond_${v.condition}` as LocaleKey)} · ${t("E03_Pb_MeanNote")}`,
|
||||
}),
|
||||
muted(`${sideText} · ${t(`E03_Cond_${v.condition}` as LocaleKey)} · ${t("E03_Pb_MeanNote")}`),
|
||||
);
|
||||
host.append(
|
||||
inputTable(got),
|
||||
runFields(got, slot, v.editable),
|
||||
sources(got, changedCount(got, cache)),
|
||||
);
|
||||
if (busy)
|
||||
host.append(
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: `${busy.percent}% · ${busy.message}`,
|
||||
attrs: { "data-role": "prob-progress" },
|
||||
}),
|
||||
);
|
||||
if (busy) host.append(muted(`${busy.percent}% · ${busy.message}`, "prob-progress"));
|
||||
const rec = records.get(slot);
|
||||
if (!rec) return host;
|
||||
const res = rec.result;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { el } from "@ui/ui_template_elements";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { axisTicks, type ProbCurve, type ProbResult } from "./logic/E03_StabSection_Prob";
|
||||
import { pointsText, type Point } from "./logic/E03_StabSection_Logic";
|
||||
import { legend } from "./E03_StabSection_UI_Parts";
|
||||
import { svgEl } from "./E03_StabSection_UI_Section";
|
||||
|
||||
const W = 380;
|
||||
@@ -128,23 +129,9 @@ export function cdfChart(res: ProbResult, required: number | null): HTMLElement
|
||||
}
|
||||
|
||||
function legendOf(curves: ProbCurve[]): HTMLElement {
|
||||
const legend = el("div", { className: "e03-legend" });
|
||||
curves.forEach((c, i) => {
|
||||
const item = el("span", { className: "e03-legend__item" });
|
||||
const sw = svgEl("svg", { width: 18, height: 8 });
|
||||
sw.append(
|
||||
svgEl("line", {
|
||||
x1: 0,
|
||||
x2: 18,
|
||||
y1: 4,
|
||||
y2: 4,
|
||||
class: `e03-prob-line e03-prob-line--${i % 8}`,
|
||||
}),
|
||||
);
|
||||
item.append(sw as unknown as HTMLElement, c.name);
|
||||
legend.append(item);
|
||||
});
|
||||
return legend;
|
||||
return legend(
|
||||
curves.map((c, i) => ({ label: c.name, color: `var(--e03-mat-${i % 8})`, kind: "line" })),
|
||||
);
|
||||
}
|
||||
|
||||
/** 민감도 — 가로 = 범위 %(0 = 아래 끝 · 100 = 위 끝) · 나머지 변수 평균 · 임계면 고정 */
|
||||
|
||||
@@ -10,6 +10,8 @@ import { createButton, createInputField, createSelectField, el } from "@ui/ui_te
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { createSectionCanvas } from "./E03_StabSection_UI_Section";
|
||||
import { verdictChip } from "./E03_StabSection_UI_Result";
|
||||
import { card, muted } from "./E03_StabSection_UI_Parts";
|
||||
import { e03Table } from "./E03_StabSection_UI_Table";
|
||||
import type { Condition, SectionView, SideCase, StationRow } from "./logic/E03_StabSection_Logic";
|
||||
|
||||
export interface WhatIfRecord {
|
||||
@@ -89,39 +91,33 @@ function num(input: HTMLInputElement): number | null {
|
||||
|
||||
/** 전후 안전율 · 판정 표 — 비탈 가정 · 네일 · 앵커 가정 같이 씀 */
|
||||
export function slopeTable(rec: Pick<WhatIfRecord, "before" | "after">): HTMLElement {
|
||||
const table = el("table", { className: "e03-table", attrs: { "data-role": "whatif-table" } });
|
||||
const head = ["E03_Condition", "E03_Rf_Before", "E03_Rf_After", "E03_Required"] as LocaleKey[];
|
||||
table.append(
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: [
|
||||
...head.map((k, i) => el("th", { className: i ? "is-num" : "", text: t(k) })),
|
||||
el("th", { text: t("E03_Rf_VerdictBefore") }),
|
||||
el("th", { text: t("E03_Rf_VerdictAfter") }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
const conds = CONDS.filter((cond) => rec.after[cond]);
|
||||
const list = e03Table(
|
||||
[
|
||||
...head.map((k, i) => ({ label: t(k), num: i > 0 })),
|
||||
{ label: t("E03_Rf_VerdictBefore") },
|
||||
{ label: t("E03_Rf_VerdictAfter") },
|
||||
],
|
||||
conds.map((cond) => {
|
||||
const a = rec.after[cond]!;
|
||||
const b = rec.before[cond];
|
||||
return [
|
||||
t(`E03_Cond_${cond}` as LocaleKey),
|
||||
fmt(b?.fs, 3),
|
||||
fmt(a.fs, 3),
|
||||
fmt(a.required),
|
||||
verdictChip(b?.ok ?? null),
|
||||
verdictChip(a.ok),
|
||||
];
|
||||
}),
|
||||
"whatif-table",
|
||||
);
|
||||
const body = el("tbody");
|
||||
for (const cond of CONDS) {
|
||||
const a = rec.after[cond];
|
||||
if (!a) continue;
|
||||
const b = rec.before[cond];
|
||||
const tr = el("tr", { attrs: { "data-cond": cond } });
|
||||
tr.append(
|
||||
el("td", { text: t(`E03_Cond_${cond}` as LocaleKey) }),
|
||||
el("td", { className: "is-num", text: fmt(b?.fs, 3) }),
|
||||
el("td", { className: "is-num", text: fmt(a.fs, 3), attrs: { "data-role": "fs-after" } }),
|
||||
el("td", { className: "is-num", text: fmt(a.required) }),
|
||||
el("td", { children: [verdictChip(b?.ok ?? null)] }),
|
||||
el("td", { children: [verdictChip(a.ok)] }),
|
||||
);
|
||||
body.append(tr);
|
||||
}
|
||||
table.append(body);
|
||||
return table;
|
||||
list.rows.forEach((tr, i) => {
|
||||
tr.dataset.cond = conds[i];
|
||||
tr.cells[2].dataset.role = "fs-after";
|
||||
});
|
||||
return list.root;
|
||||
}
|
||||
|
||||
function assumeText(rec: WhatIfRecord): string {
|
||||
@@ -140,52 +136,39 @@ function wallTable(rec: WallRecord): HTMLElement {
|
||||
for (const wall of rec.walls) {
|
||||
for (const c of wall.cases) {
|
||||
box.append(
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text:
|
||||
`${wall.name ?? t("E03_Rf_Wall")} · ${t(`E03_Rf_Case_${c.case}` as LocaleKey)} · ` +
|
||||
muted(
|
||||
`${wall.name ?? t("E03_Rf_Wall")} · ${t(`E03_Rf_Case_${c.case}` as LocaleKey)} · ` +
|
||||
`H ${fmt(c.geometry.H)} m · B ${fmt(c.geometry.B)} m · Ka ${fmt(c.coefficients.ka, 3)}` +
|
||||
(c.coefficients.kae ? ` · KAE ${fmt(c.coefficients.kae, 3)}` : "") +
|
||||
` · e ${fmt(c.base.e)} m · q_max ${fmt(c.base.q_max, 1)} kPa`,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const table = el("table", { className: "e03-table" });
|
||||
table.append(
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: [
|
||||
el("th", { text: t("E03_Rf_Check") }),
|
||||
el("th", { className: "is-num", text: t("E03_Rf_Fs") }),
|
||||
el("th", { className: "is-num", text: t("E03_Required") }),
|
||||
el("th", { text: t("E03_Verdict") }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
const checks = e03Table(
|
||||
[
|
||||
{ label: t("E03_Rf_Check") },
|
||||
{ label: t("E03_Rf_Fs"), num: true },
|
||||
{ label: t("E03_Required"), num: true },
|
||||
{ label: t("E03_Verdict") },
|
||||
],
|
||||
CHECKS.map((k) => [
|
||||
t(`E03_Rf_${k}` as LocaleKey),
|
||||
fmt(c.fs[k]),
|
||||
fmt(c.required[k], 1),
|
||||
verdictChip(c.ok[k]),
|
||||
]),
|
||||
);
|
||||
const body = el("tbody");
|
||||
for (const k of CHECKS)
|
||||
body.append(
|
||||
el("tr", {
|
||||
attrs: { "data-check": k, "data-case": c.case },
|
||||
children: [
|
||||
el("td", { text: t(`E03_Rf_${k}` as LocaleKey) }),
|
||||
el("td", { className: "is-num", text: fmt(c.fs[k]) }),
|
||||
el("td", { className: "is-num", text: fmt(c.required[k], 1) }),
|
||||
el("td", { children: [verdictChip(c.ok[k])] }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
table.append(body);
|
||||
box.append(table);
|
||||
checks.rows.forEach((tr, i) => {
|
||||
tr.dataset.check = CHECKS[i];
|
||||
tr.dataset.case = c.case;
|
||||
});
|
||||
box.append(checks.root);
|
||||
}
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
export function buildReinforce(o: ReinforceOptions): HTMLElement {
|
||||
const host = el("div", { className: "e03-card", attrs: { "data-role": "reinforce" } });
|
||||
const host = card("", "reinforce");
|
||||
const sides = o.row.sides;
|
||||
const sideField = createSelectField({
|
||||
label: t("E03_Side"),
|
||||
@@ -247,21 +230,14 @@ export function buildReinforce(o: ReinforceOptions): HTMLElement {
|
||||
buttons.append(find, run);
|
||||
host.append(
|
||||
el("h3", { className: "e03-card__title", text: t("E03_Rf_Title") }),
|
||||
el("p", { className: "e03-muted", text: t("E03_Rf_Hint") }),
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: t("E03_Rf_Goal").replace("{c}", t(`E03_Cond_${o.condition}` as LocaleKey)),
|
||||
}),
|
||||
muted(t("E03_Rf_Hint")),
|
||||
muted(t("E03_Rf_Goal").replace("{c}", t(`E03_Cond_${o.condition}` as LocaleKey))),
|
||||
fields,
|
||||
buttons,
|
||||
);
|
||||
if (o.slope) {
|
||||
host.append(
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: assumeText(o.slope),
|
||||
attrs: { "data-role": "assume" },
|
||||
}),
|
||||
muted(assumeText(o.slope), "assume"),
|
||||
slopeTable(o.slope),
|
||||
el("h4", { className: "e03-card__title", text: t("E03_Rf_AfterSection") }),
|
||||
);
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
* 간극수압 · 동원 전단응력 — kPa 한 축). 계산은 logic.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createKv, type KvRow } from "@ui/ui_template_kv";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import {
|
||||
forceArrows,
|
||||
@@ -18,6 +20,8 @@ import {
|
||||
type SectionView,
|
||||
} from "./logic/E03_StabSection_Logic";
|
||||
import { WALL_ID, materialClass, svgEl } from "./E03_StabSection_UI_Section";
|
||||
import { card, emptyBox, legend, muted } from "./E03_StabSection_UI_Parts";
|
||||
import { e03Table } from "./E03_StabSection_UI_Table";
|
||||
|
||||
const fmt = (v: number | null | undefined, d = 2): string =>
|
||||
v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(d);
|
||||
@@ -26,57 +30,59 @@ const METHODS = ["ordinary", "bishop", "janbu", "spencer"] as const;
|
||||
|
||||
export function verdictChip(ok: boolean | null): HTMLElement {
|
||||
const v = ok === null ? "none" : ok ? "ok" : "ng";
|
||||
return el("span", {
|
||||
className: `e03-verdict e03-verdict--${v}`,
|
||||
text: t(`E03_Verdict_${v}` as LocaleKey),
|
||||
});
|
||||
const chip = createBadge(
|
||||
t(`E03_Verdict_${v}` as LocaleKey),
|
||||
v === "ok" ? "success" : v === "ng" ? "danger" : "neutral",
|
||||
{ kind: "lamp" },
|
||||
);
|
||||
chip.classList.add("e03-verdict", `e03-verdict--${v}`);
|
||||
return chip;
|
||||
}
|
||||
|
||||
function kpi(analysis: Analysis, walls: number): HTMLElement {
|
||||
const c = analysis.critical;
|
||||
const dl = el("dl", { className: "e03-kpi" });
|
||||
const row = (label: string, value: HTMLElement | string, big = false): void => {
|
||||
const dd = el("dd", { className: big ? "is-big" : "" });
|
||||
dd.append(value);
|
||||
dl.append(el("dt", { text: label }), dd);
|
||||
};
|
||||
row(t("E03_Fs"), fmt(c?.fs, 3), true);
|
||||
row(t("E03_Verdict"), verdictChip(analysis.ok));
|
||||
row(t("E03_Required"), fmt(analysis.required));
|
||||
row(
|
||||
t("E03_Method"),
|
||||
`${t("E03_Method_bishop")} · ${c?.kind === "circle" ? t("E03_Circle") : t("E03_NonCircle")}`,
|
||||
);
|
||||
row(
|
||||
t("E03_Converged"),
|
||||
c
|
||||
? `${c.converged ? t("E03_Yes") : t("E03_No")} · ${c.iterations ?? "-"}${t("E03_Iter")}`
|
||||
: "-",
|
||||
);
|
||||
row(t("E03_Height"), `${fmt(analysis.height_m)} m`);
|
||||
const rows: KvRow[] = [
|
||||
{ label: t("E03_Fs"), value: fmt(c?.fs, 3), big: true },
|
||||
{ label: t("E03_Verdict"), value: verdictChip(analysis.ok) },
|
||||
{ label: t("E03_Required"), value: fmt(analysis.required) },
|
||||
{
|
||||
label: t("E03_Method"),
|
||||
value: `${t("E03_Method_bishop")} · ${c?.kind === "circle" ? t("E03_Circle") : t("E03_NonCircle")}`,
|
||||
},
|
||||
{
|
||||
label: t("E03_Converged"),
|
||||
value: c
|
||||
? `${c.converged ? t("E03_Yes") : t("E03_No")} · ${c.iterations ?? "-"}${t("E03_Iter")}`
|
||||
: "-",
|
||||
},
|
||||
{ label: t("E03_Height"), value: `${fmt(analysis.height_m)} m` },
|
||||
];
|
||||
if (walls)
|
||||
row(t("E03_Walls"), el("span", { text: String(walls), attrs: { "data-role": "walls" } }));
|
||||
rows.push({
|
||||
label: t("E03_Walls"),
|
||||
value: el("span", { text: String(walls), attrs: { "data-role": "walls" } }),
|
||||
});
|
||||
if (c?.center) {
|
||||
row(t("E03_Center"), `${fmt(c.center[0])}, ${fmt(c.center[1])}`);
|
||||
row(t("E03_Radius"), `${fmt(c.radius)} m`);
|
||||
rows.push({ label: t("E03_Center"), value: `${fmt(c.center[0])}, ${fmt(c.center[1])}` });
|
||||
rows.push({ label: t("E03_Radius"), value: `${fmt(c.radius)} m` });
|
||||
}
|
||||
const dl = createKv(rows);
|
||||
dl.classList.add("e03-kpi");
|
||||
return dl;
|
||||
}
|
||||
|
||||
function methodTable(analysis: Analysis): HTMLElement {
|
||||
const table = el("table", { className: "e03-table", attrs: { "data-role": "methods" } });
|
||||
const head = el("tr");
|
||||
const body = el("tr");
|
||||
for (const m of METHODS) {
|
||||
head.append(el("th", { className: "is-num", text: t(`E03_Method_${m}` as LocaleKey) }));
|
||||
const v = analysis.min_by_method[m];
|
||||
const unsolved = (v === null || v === undefined) && analysis.no_solution?.includes(m);
|
||||
body.append(
|
||||
el("td", { className: "is-num", text: unsolved ? t("E03_NoSolution") : fmt(v, 3) }),
|
||||
);
|
||||
}
|
||||
table.append(el("thead", { children: [head] }), el("tbody", { children: [body] }));
|
||||
return table;
|
||||
return e03Table(
|
||||
METHODS.map((m) => ({ label: t(`E03_Method_${m}` as LocaleKey), num: true })),
|
||||
[
|
||||
METHODS.map((m) => {
|
||||
const v = analysis.min_by_method[m];
|
||||
const unsolved = (v === null || v === undefined) && analysis.no_solution?.includes(m);
|
||||
return unsolved ? t("E03_NoSolution") : fmt(v, 3);
|
||||
}),
|
||||
],
|
||||
"methods",
|
||||
).root;
|
||||
}
|
||||
|
||||
function sliceTable(
|
||||
@@ -85,7 +91,6 @@ function sliceTable(
|
||||
onPick: (i: number | null) => void,
|
||||
): HTMLElement {
|
||||
const wrap = el("div", { className: "e03-slices" });
|
||||
const table = el("table", { className: "e03-table", attrs: { "data-role": "slice-table" } });
|
||||
const cols: [LocaleKey, (s: Analysis["slices"][number]) => string][] = [
|
||||
["E03_Col_No", (s) => String(s.i)],
|
||||
["E03_Col_Width", (s) => fmt(s.width)],
|
||||
@@ -98,27 +103,20 @@ function sliceTable(
|
||||
["E03_Col_C", (s) => fmt(s.c, 0)],
|
||||
["E03_Col_Phi", (s) => fmt(s.phi_deg, 0)],
|
||||
];
|
||||
table.append(
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", { children: cols.map(([k]) => el("th", { className: "is-num", text: t(k) })) }),
|
||||
],
|
||||
}),
|
||||
const slices = e03Table(
|
||||
cols.map(([k]) => ({ label: t(k), num: true })),
|
||||
analysis.slices.map((s) => cols.map(([, f]) => f(s))),
|
||||
"slice-table",
|
||||
);
|
||||
const body = el("tbody");
|
||||
analysis.slices.forEach((s, i) => {
|
||||
const tr = el("tr", {
|
||||
className: picked === i ? "is-picked" : "",
|
||||
attrs: { "data-i": String(i) },
|
||||
});
|
||||
for (const [, f] of cols) tr.append(el("td", { className: "is-num", text: f(s) }));
|
||||
slices.rows.forEach((tr, i) => {
|
||||
tr.dataset.i = String(i);
|
||||
tr.classList.toggle("is-picked", picked === i);
|
||||
tr.classList.toggle("is-active", picked === i);
|
||||
tr.addEventListener("click", () => onPick(picked === i ? null : i));
|
||||
body.append(tr);
|
||||
});
|
||||
table.append(body);
|
||||
wrap.append(table);
|
||||
wrap.append(slices.root);
|
||||
if (picked !== null)
|
||||
queueMicrotask(() => body.children[picked]?.scrollIntoView({ block: "nearest" }));
|
||||
queueMicrotask(() => slices.rows[picked]?.scrollIntoView({ block: "nearest" }));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
@@ -127,7 +125,7 @@ function freeBody(view: SectionView, analysis: Analysis, picked: number | null):
|
||||
const box = el("div", { className: "e03-fbd" });
|
||||
const slice = picked !== null ? analysis.slices[picked] : null;
|
||||
if (!slice || !analysis.critical) {
|
||||
box.append(el("p", { className: "e03-muted", text: t("E03_PickSlice") }));
|
||||
box.append(muted(t("E03_PickSlice")));
|
||||
return box;
|
||||
}
|
||||
const W = 380;
|
||||
@@ -198,10 +196,9 @@ function freeBody(view: SectionView, analysis: Analysis, picked: number | null):
|
||||
}
|
||||
box.append(svg);
|
||||
box.append(
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: `#${slice.i} · α ${fmt(slice.alpha_deg, 1)}° · b ${fmt(slice.width)} m · ${t("E03_FbdUnit")}`,
|
||||
}),
|
||||
muted(
|
||||
`#${slice.i} · α ${fmt(slice.alpha_deg, 1)}° · b ${fmt(slice.width)} m · ${t("E03_FbdUnit")}`,
|
||||
),
|
||||
);
|
||||
return box;
|
||||
}
|
||||
@@ -261,19 +258,13 @@ function stressGraph(analysis: Analysis, picked: number | null): HTMLElement {
|
||||
svg.append(hit);
|
||||
});
|
||||
box.append(svg);
|
||||
const legend = el("div", { className: "e03-legend" });
|
||||
for (const [key, label] of [
|
||||
["sigma", "E03_Graph_Sigma"],
|
||||
["u", "E03_Graph_U"],
|
||||
["tau", "E03_Graph_Tau"],
|
||||
] as const) {
|
||||
const item = el("span", { className: "e03-legend__item" });
|
||||
const sw = svgEl("svg", { width: 18, height: 8 });
|
||||
sw.append(svgEl("line", { x1: 0, x2: 18, y1: 4, y2: 4, class: `e03-line e03-line--${key}` }));
|
||||
item.append(sw as unknown as HTMLElement, t(label as LocaleKey));
|
||||
legend.append(item);
|
||||
}
|
||||
box.append(legend);
|
||||
box.append(
|
||||
legend([
|
||||
{ label: t("E03_Graph_Sigma"), color: "var(--e03-mat-0)", kind: "line" },
|
||||
{ label: t("E03_Graph_U"), color: "var(--e03-water)", kind: "dash" },
|
||||
{ label: t("E03_Graph_Tau"), color: "var(--e03-mat-1)", kind: "line" },
|
||||
]),
|
||||
);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -283,9 +274,9 @@ export function buildResult(
|
||||
picked: number | null,
|
||||
onPick: (i: number | null) => void,
|
||||
): HTMLElement {
|
||||
const root = el("div", { className: "e03-result e03-card" });
|
||||
const root = card("e03-result");
|
||||
if (!analysis || !analysis.critical) {
|
||||
root.append(el("p", { className: "e03-empty", text: t("E03_NoSurface") }));
|
||||
root.append(emptyBox(t("E03_NoSurface")));
|
||||
return root;
|
||||
}
|
||||
const head = (key: LocaleKey): HTMLElement =>
|
||||
|
||||
@@ -11,6 +11,7 @@ import "./E03_StabSection_UI_Walls.css";
|
||||
import "./E03_StabSection_UI_Support.css";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { legend, muted } from "./E03_StabSection_UI_Parts";
|
||||
import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan";
|
||||
import {
|
||||
bandIndex,
|
||||
@@ -74,34 +75,34 @@ export function buildLegends(draw: SectionDraw): HTMLElement {
|
||||
const box = el("div", { className: "e03-legend" });
|
||||
const required = draw.analysis?.required ?? null;
|
||||
if (draw.toggles.map && draw.analysis?.grid) {
|
||||
box.append(el("span", { className: "e03-muted", text: t("E03_Legend_Map") }));
|
||||
for (const band of bandLegend(required)) {
|
||||
const text =
|
||||
band.from === null
|
||||
? `< ${band.to}`
|
||||
: band.to === null
|
||||
? `≥ ${band.from}`
|
||||
: `${band.from}–${band.to}`;
|
||||
const item = el("span", {
|
||||
className: "e03-legend__item",
|
||||
attrs: { "data-band": String(band.index) },
|
||||
});
|
||||
item.append(el("span", { className: `e03-legend__swatch e03-band-${band.index}` }), text);
|
||||
box.append(item);
|
||||
}
|
||||
box.append(muted(t("E03_Legend_Map")));
|
||||
const bands = bandLegend(required);
|
||||
const row = legend(
|
||||
bands.map((band) => ({
|
||||
label:
|
||||
band.from === null
|
||||
? `< ${band.to}`
|
||||
: band.to === null
|
||||
? `≥ ${band.from}`
|
||||
: `${band.from}–${band.to}`,
|
||||
color: `var(--e03-band-${band.index})`,
|
||||
})),
|
||||
);
|
||||
row.querySelectorAll(".ui-legend__item").forEach((item, k) => {
|
||||
item.setAttribute("data-band", String(bands[k].index));
|
||||
});
|
||||
box.append(row);
|
||||
}
|
||||
const used = new Set(draw.view.regions.map((r) => r.material_id));
|
||||
box.append(el("span", { className: "e03-muted", text: t("E03_Legend_Layers") }));
|
||||
for (const m of draw.view.materials.filter((x) => used.has(x.id))) {
|
||||
const item = el("span", { className: "e03-legend__item" });
|
||||
item.append(
|
||||
el("span", {
|
||||
className: `e03-legend__swatch ${m.id === WALL_ID ? "e03-wall-swatch" : materialClass(draw.view, m.id)}`,
|
||||
}),
|
||||
m.name,
|
||||
);
|
||||
box.append(item);
|
||||
}
|
||||
box.append(muted(t("E03_Legend_Layers")));
|
||||
const layers = draw.view.materials.filter((x) => used.has(x.id));
|
||||
const row = legend(
|
||||
layers.map((m) => ({ label: m.name, color: `var(--${materialClass(draw.view, m.id)})` })),
|
||||
);
|
||||
row.querySelectorAll(".ui-legend__mark").forEach((mark, k) => {
|
||||
if (layers[k].id === WALL_ID) mark.classList.add("e03-wall-swatch");
|
||||
});
|
||||
box.append(row);
|
||||
return box;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,15 +94,13 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 카드 · 보조 글 · 빈 자리 · 체크 · 표 · 범례 · 값 목록 · 모달은 부품 — 여기는 자리만 */
|
||||
.e03-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.e03-card__head {
|
||||
@@ -121,17 +119,11 @@
|
||||
}
|
||||
|
||||
.e03-muted {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body-sm);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.e03-empty {
|
||||
.e03-card > .e03-empty {
|
||||
margin: auto;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body);
|
||||
text-align: center;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* ---- 좌측 측점 목록 ---- */
|
||||
@@ -155,16 +147,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.e03-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-body-sm);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.e03-list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -172,89 +154,37 @@
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.e03-list table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-body);
|
||||
.e03-list .ui-table-wrap,
|
||||
.e03-slices .ui-table-wrap {
|
||||
overflow: visible;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.e03-list th {
|
||||
.e03-list th,
|
||||
.e03-slices th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
padding: var(--spacing-4);
|
||||
background: var(--color-surface-raised);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.e03-list td {
|
||||
padding: 3px var(--spacing-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.e03-list td.is-num,
|
||||
.e03-list th.is-num,
|
||||
.e03-table td.is-num,
|
||||
.e03-table th.is-num {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.e03-list tbody tr {
|
||||
.e03-list tbody tr,
|
||||
.e03-slices tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.e03-list tbody tr:hover {
|
||||
background: var(--color-surface-sunken);
|
||||
.e03-list .ui-table th,
|
||||
.e03-list .ui-table td,
|
||||
.e03-slices .ui-table th,
|
||||
.e03-slices .ui-table td {
|
||||
padding: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.e03-list tbody tr.is-active {
|
||||
background: var(--color-surface-sunken);
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: -2px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.e03-verdict {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.e03-verdict::before {
|
||||
content: "";
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--e03-none);
|
||||
}
|
||||
|
||||
.e03-verdict--ok::before {
|
||||
background: var(--e03-ok);
|
||||
}
|
||||
|
||||
.e03-verdict--ng::before {
|
||||
background: var(--e03-ng);
|
||||
border-radius: 2px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.e03-star {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0 2px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
.e03-check {
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.e03-star.is-on {
|
||||
@@ -265,10 +195,6 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.e03-star:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* ---- 평면 띠 ---- */
|
||||
.e03-plan svg {
|
||||
display: block;
|
||||
@@ -309,22 +235,6 @@
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4) var(--spacing-12);
|
||||
align-items: center;
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-caption, 12px);
|
||||
}
|
||||
|
||||
.e03-legend__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.e03-legend__swatch {
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* ---- 횡단 ---- */
|
||||
@@ -458,63 +368,6 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.e03-kpi {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--spacing-4) var(--spacing-12);
|
||||
margin: 0;
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.e03-kpi dt {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.e03-kpi dd {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.e03-kpi dd.is-big {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.e03-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-body);
|
||||
}
|
||||
|
||||
.e03-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--color-surface-raised);
|
||||
padding: 2px var(--spacing-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.e03-table td {
|
||||
padding: 2px var(--spacing-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.e03-table tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.e03-table tbody tr.is-picked {
|
||||
background: var(--color-surface-sunken);
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.e03-slices {
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
@@ -577,26 +430,6 @@
|
||||
}
|
||||
|
||||
/* ---- 3D 보기 ---- */
|
||||
.e03-view3d {
|
||||
position: fixed;
|
||||
inset: 5vh 5vw;
|
||||
z-index: 800;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.e03-view3d__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
}
|
||||
|
||||
.e03-view3d__canvas {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -612,79 +445,60 @@
|
||||
/* ---- 색 칸(지층 · 등안전율 띠) ---- */
|
||||
.e03-mat-0 {
|
||||
fill: var(--e03-mat-0);
|
||||
background: var(--e03-mat-0);
|
||||
}
|
||||
|
||||
.e03-mat-1 {
|
||||
fill: var(--e03-mat-1);
|
||||
background: var(--e03-mat-1);
|
||||
}
|
||||
|
||||
.e03-mat-2 {
|
||||
fill: var(--e03-mat-2);
|
||||
background: var(--e03-mat-2);
|
||||
}
|
||||
|
||||
.e03-mat-3 {
|
||||
fill: var(--e03-mat-3);
|
||||
background: var(--e03-mat-3);
|
||||
}
|
||||
|
||||
.e03-mat-4 {
|
||||
fill: var(--e03-mat-4);
|
||||
background: var(--e03-mat-4);
|
||||
}
|
||||
|
||||
.e03-mat-5 {
|
||||
fill: var(--e03-mat-5);
|
||||
background: var(--e03-mat-5);
|
||||
}
|
||||
|
||||
.e03-mat-6 {
|
||||
fill: var(--e03-mat-6);
|
||||
background: var(--e03-mat-6);
|
||||
}
|
||||
|
||||
.e03-mat-7 {
|
||||
fill: var(--e03-mat-7);
|
||||
background: var(--e03-mat-7);
|
||||
}
|
||||
|
||||
.e03-band-0 {
|
||||
fill: var(--e03-band-0);
|
||||
background: var(--e03-band-0);
|
||||
}
|
||||
|
||||
.e03-band-1 {
|
||||
fill: var(--e03-band-1);
|
||||
background: var(--e03-band-1);
|
||||
}
|
||||
|
||||
.e03-band-2 {
|
||||
fill: var(--e03-band-2);
|
||||
background: var(--e03-band-2);
|
||||
}
|
||||
|
||||
.e03-band-3 {
|
||||
fill: var(--e03-band-3);
|
||||
background: var(--e03-band-3);
|
||||
}
|
||||
|
||||
.e03-band-4 {
|
||||
fill: var(--e03-band-4);
|
||||
background: var(--e03-band-4);
|
||||
}
|
||||
|
||||
.e03-band-5 {
|
||||
fill: var(--e03-band-5);
|
||||
background: var(--e03-band-5);
|
||||
}
|
||||
|
||||
.e03-band-6 {
|
||||
fill: var(--e03-band-6);
|
||||
background: var(--e03-band-6);
|
||||
}
|
||||
|
||||
.e03-swatch-none {
|
||||
background: var(--e03-none);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import { createButton, createInputField, createSelectField, el } from "@ui/ui_te
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { slopeTable } from "./E03_StabSection_UI_Reinforce";
|
||||
import { createSectionCanvas } from "./E03_StabSection_UI_Section";
|
||||
import { card, muted } from "./E03_StabSection_UI_Parts";
|
||||
import { e03Table } from "./E03_StabSection_UI_Table";
|
||||
import type { Condition, SectionView, SideCase, StationRow } from "./logic/E03_StabSection_Logic";
|
||||
import {
|
||||
layoutSupports,
|
||||
@@ -100,50 +102,37 @@ function assumeText(rec: SupportRecord): string {
|
||||
|
||||
function forceTable(rec: SupportRecord, condition: Condition): HTMLElement {
|
||||
const forces = rec.support_forces?.[condition] ?? null;
|
||||
const table = el("table", { className: "e03-table", attrs: { "data-role": "support-table" } });
|
||||
const head = ["E03_Sp_No", "E03_Sp_Type", "E03_Sp_Head", "E03_Sp_Length", "E03_Sp_Force"];
|
||||
table.append(
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: [
|
||||
...head.map((k, i) =>
|
||||
el("th", { className: i > 1 ? "is-num" : "", text: t(k as LocaleKey) }),
|
||||
),
|
||||
el("th", { text: t("E03_Sp_Mode") }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
const found = rec.assume.supports.map((s, i) => ({ s, f: forces?.find((v) => v.index === i) }));
|
||||
const list = e03Table(
|
||||
[
|
||||
...head.map((k, i) => ({ label: t(k as LocaleKey), num: i > 1 })),
|
||||
{ label: t("E03_Sp_Mode") },
|
||||
],
|
||||
found.map(({ s, f }, i) => {
|
||||
const mode = f?.mode ?? "pending";
|
||||
const swatch = el("span", {
|
||||
className: `e03-support e03-support--${mode}`,
|
||||
children: [el("span", { className: "e03-support-swatch" })],
|
||||
});
|
||||
const cell = document.createDocumentFragment();
|
||||
cell.append(swatch, el("span", { text: t(`E03_Sp_Mode_${mode}` as LocaleKey) }));
|
||||
return [
|
||||
String(i + 1),
|
||||
t(`E03_Sp_${s.type}` as LocaleKey),
|
||||
`${fmt(s.x)} / ${fmt(s.z)}`,
|
||||
fmt(s.length_m, 1),
|
||||
fmt(f?.mobilized_kN_per_m, 1),
|
||||
cell,
|
||||
];
|
||||
}),
|
||||
"support-table",
|
||||
);
|
||||
const body = el("tbody");
|
||||
rec.assume.supports.forEach((s, i) => {
|
||||
const f = forces?.find((v) => v.index === i);
|
||||
const mode = f?.mode ?? "pending";
|
||||
body.append(
|
||||
el("tr", {
|
||||
attrs: { "data-i": String(i), "data-mode": mode },
|
||||
children: [
|
||||
el("td", { text: String(i + 1) }),
|
||||
el("td", { text: t(`E03_Sp_${s.type}` as LocaleKey) }),
|
||||
el("td", { className: "is-num", text: `${fmt(s.x)} / ${fmt(s.z)}` }),
|
||||
el("td", { className: "is-num", text: fmt(s.length_m, 1) }),
|
||||
el("td", { className: "is-num", text: fmt(f?.mobilized_kN_per_m, 1) }),
|
||||
el("td", {
|
||||
children: [
|
||||
el("span", {
|
||||
className: `e03-support e03-support--${mode}`,
|
||||
children: [el("span", { className: "e03-support-swatch" })],
|
||||
}),
|
||||
el("span", { text: t(`E03_Sp_Mode_${mode}` as LocaleKey) }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
list.rows.forEach((tr, i) => {
|
||||
tr.dataset.i = String(i);
|
||||
tr.dataset.mode = found[i].f?.mode ?? "pending";
|
||||
});
|
||||
table.append(body);
|
||||
return table;
|
||||
return list.root;
|
||||
}
|
||||
|
||||
function legend(): HTMLElement {
|
||||
@@ -160,7 +149,7 @@ function legend(): HTMLElement {
|
||||
}
|
||||
|
||||
export function buildSupport(o: SupportOptions): HTMLElement {
|
||||
const host = el("div", { className: "e03-card", attrs: { "data-role": "support" } });
|
||||
const host = card("", "support");
|
||||
const sides = o.row.sides;
|
||||
const sideField = createSelectField({
|
||||
label: t("E03_Side"),
|
||||
@@ -246,32 +235,15 @@ export function buildSupport(o: SupportOptions): HTMLElement {
|
||||
buttons.append(find, run);
|
||||
host.append(
|
||||
el("h3", { className: "e03-card__title", text: t("E03_Sp_Title") }),
|
||||
el("p", { className: "e03-muted", text: t("E03_Sp_Hint") }),
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: t("E03_Sp_Source"),
|
||||
attrs: { "data-role": "support-source" },
|
||||
}),
|
||||
muted(t("E03_Sp_Hint")),
|
||||
muted(t("E03_Sp_Source"), "support-source"),
|
||||
fields,
|
||||
buttons,
|
||||
);
|
||||
const rec = o.record;
|
||||
if (!rec) return host;
|
||||
host.append(
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: assumeText(rec),
|
||||
attrs: { "data-role": "support-assume" },
|
||||
}),
|
||||
);
|
||||
if (rec.supports_pending)
|
||||
host.append(
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: t("E03_Sp_Pending"),
|
||||
attrs: { "data-role": "support-pending" },
|
||||
}),
|
||||
);
|
||||
host.append(muted(assumeText(rec), "support-assume"));
|
||||
if (rec.supports_pending) host.append(muted(t("E03_Sp_Pending"), "support-pending"));
|
||||
host.append(slopeTable(rec), forceTable(rec, o.condition), legend());
|
||||
const canvas = createSectionCanvas(() => undefined);
|
||||
canvas.root.style.height = "260px";
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/* =============================================================================
|
||||
* E03_StabSection_UI_Table.ts
|
||||
* E03 표 하나(553-4) — 표 부품(`ui_template_table` · 데이터표 줄 28)을 부르고 촬영 장면 · 시험이 잡는
|
||||
* 걸이(`table.e03-table[data-role]` · `tr`)만 얹음. 모양은 부품 · 여기는 CSS 없음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createTable, type Cell } from "@ui/ui_template_table";
|
||||
|
||||
export interface E03Column {
|
||||
label: string;
|
||||
/** 수 칸 — 오른쪽 맞춤 */
|
||||
num?: boolean;
|
||||
/** 마우스 글 */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface E03TableHandle {
|
||||
/** 표 감싸개(`.ui-table-wrap`) */
|
||||
root: HTMLElement;
|
||||
table: HTMLTableElement;
|
||||
rows: HTMLTableRowElement[];
|
||||
}
|
||||
|
||||
export function e03Table(
|
||||
columns: E03Column[],
|
||||
rows: Cell[][],
|
||||
role?: string,
|
||||
extraClass?: string,
|
||||
): E03TableHandle {
|
||||
const root = createTable(
|
||||
columns.map((c) => ({ label: c.label, ...(c.num ? { align: "end" as const } : {}) })),
|
||||
rows,
|
||||
{ density: "data" },
|
||||
);
|
||||
const table = root.querySelector("table") as HTMLTableElement;
|
||||
table.classList.add("e03-table");
|
||||
if (extraClass) table.classList.add(...extraClass.split(" "));
|
||||
if (role) table.dataset.role = role;
|
||||
columns.forEach((c, i) => {
|
||||
if (c.title) table.tHead?.rows[0].cells[i].setAttribute("title", c.title);
|
||||
});
|
||||
return { root, table, rows: [...table.tBodies[0].rows] };
|
||||
}
|
||||
@@ -7,8 +7,10 @@
|
||||
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { legend, muted } from "./E03_StabSection_UI_Parts";
|
||||
import {
|
||||
bandIndex,
|
||||
RATIO_BREAKS,
|
||||
@@ -48,35 +50,34 @@ export async function openView3d(
|
||||
condition: Condition,
|
||||
conditionLabel: string,
|
||||
): Promise<void> {
|
||||
const shell = el("div", { className: "e03-view3d", attrs: { "data-role": "view3d" } });
|
||||
const head = el("div", { className: "e03-view3d__head" });
|
||||
const legend = el("div", { className: "e03-legend" });
|
||||
legend.append(
|
||||
el("span", { className: "e03-muted", text: `${conditionLabel} · ${t("E03_View3d_Legend")}` }),
|
||||
);
|
||||
const edges = [null, ...RATIO_BREAKS, null];
|
||||
for (let i = 0; i <= RATIO_BREAKS.length; i += 1) {
|
||||
const item = el("span", { className: "e03-legend__item" });
|
||||
const text =
|
||||
const items = [...Array(RATIO_BREAKS.length + 1).keys()].map((i) => ({
|
||||
label:
|
||||
edges[i] === null
|
||||
? `< ${edges[i + 1]}`
|
||||
: edges[i + 1] === null
|
||||
? `≥ ${edges[i]}`
|
||||
: `${edges[i]}–${edges[i + 1]}`;
|
||||
item.append(el("span", { className: `e03-legend__swatch e03-band-${i}` }), text);
|
||||
legend.append(item);
|
||||
}
|
||||
const noneItem = el("span", { className: "e03-legend__item" });
|
||||
noneItem.append(
|
||||
el("span", { className: "e03-legend__swatch e03-swatch-none" }),
|
||||
t("E03_Verdict_none"),
|
||||
);
|
||||
legend.append(noneItem);
|
||||
const close = createButton({ label: t("Common_Btn_Close"), variant: "ghost", size: "sm" });
|
||||
head.append(el("h3", { className: "e03-card__title", text: t("E03_View3d") }), legend, close);
|
||||
: `${edges[i]}–${edges[i + 1]}`,
|
||||
color: `var(--e03-band-${i})`,
|
||||
}));
|
||||
items.push({ label: t("E03_Verdict_none"), color: "var(--e03-none)" });
|
||||
const legendRow = el("div", {
|
||||
className: "e03-legend",
|
||||
children: [muted(`${conditionLabel} · ${t("E03_View3d_Legend")}`), legend(items)],
|
||||
});
|
||||
const host = el("div", { className: "e03-view3d__canvas" });
|
||||
shell.append(head, host);
|
||||
document.body.append(shell);
|
||||
let teardown = (): void => undefined;
|
||||
openModal({
|
||||
title: t("E03_View3d"),
|
||||
closeLabel: t("Common_Btn_Close"),
|
||||
wide: true,
|
||||
dialogClass: "e03-view3d",
|
||||
mount: (body) => {
|
||||
body.closest<HTMLElement>(".ui-modal")!.dataset.role = "view3d";
|
||||
body.append(legendRow, host);
|
||||
},
|
||||
beforeClose: () => (teardown(), true),
|
||||
});
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
@@ -190,12 +191,6 @@ export async function openView3d(
|
||||
}
|
||||
});
|
||||
renderer.dispose();
|
||||
shell.remove();
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
const onKey = (ev: KeyboardEvent): void => {
|
||||
if (ev.key === "Escape") dispose();
|
||||
};
|
||||
document.addEventListener("keydown", onKey);
|
||||
close.addEventListener("click", dispose);
|
||||
teardown = dispose;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
stroke: #6b6b6b;
|
||||
}
|
||||
|
||||
.e03-wall-swatch {
|
||||
.ui-legend__mark.e03-wall-swatch {
|
||||
background: repeating-linear-gradient(45deg, #6b6b6b 0 2px, #c8c8c8 2px 5px);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,18 @@
|
||||
import "./E04_Stab3D_UI_Style.css";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { createKv, type KvRow } from "@ui/ui_template_kv";
|
||||
import { createLegend } from "@ui/ui_template_flow_legend";
|
||||
import { createListRow } from "@ui/ui_template_list_row";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { createProgressBar } from "@ui/ui_template_progress";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createSidePanels } from "@ui/ui_template_side_panels";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import { createPlanMap, type MapLayer, type StationPoint } from "./E04_Stab3D_UI_Map";
|
||||
import { createView3D } from "./E04_Stab3D_UI_View3D";
|
||||
@@ -86,18 +95,36 @@ async function fetch2d(projectId: string): Promise<Station2d[]> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 안내 글 한 줄 — 걸이 이름(`e04-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("e04-empty");
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 지도 범례 — 떠 있는 판(머리 = 제목) 안에 범례 부품 */
|
||||
function legend(layer: MapLayer): HTMLElement {
|
||||
const box = el("div", { className: "e04-legend" });
|
||||
box.append(el("strong", { text: t(layer === "fs" ? "E04_Legend_FS" : "E04_Legend_Volume") }));
|
||||
const rows = layer === "fs" ? legendRows(FS_STEPS, 1) : legendRows(VOLUME_STEPS, 0);
|
||||
for (const row of [...rows, { color: stepColor(FS_STEPS, null), label: t("E04_NoData") }]) {
|
||||
const item = el("span", { className: "e04-legend__row" });
|
||||
const swatch = el("i", { className: "e04-legend__swatch" });
|
||||
swatch.style.background = row.color;
|
||||
item.append(swatch, el("span", { text: row.label }));
|
||||
box.append(item);
|
||||
}
|
||||
return box;
|
||||
const panel = createFloatPanel({
|
||||
title: t(layer === "fs" ? "E04_Legend_FS" : "E04_Legend_Volume"),
|
||||
});
|
||||
panel.root.classList.add("e04-legend");
|
||||
panel.body.append(
|
||||
createLegend(
|
||||
[...rows, { color: stepColor(FS_STEPS, null), label: t("E04_NoData") }].map((row) => ({
|
||||
label: row.label,
|
||||
color: row.color,
|
||||
})),
|
||||
{ vertical: true },
|
||||
),
|
||||
);
|
||||
return panel.root;
|
||||
}
|
||||
|
||||
export function renderE04Stab3D(root: HTMLElement): void {
|
||||
@@ -105,8 +132,7 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
const panel = el("div", { className: "ui-sidebar-panel e04-panel" });
|
||||
const main = el("div", { className: "e04-page" });
|
||||
mountGroupStep(root, ROUTES.E04_STAB_3D, { mainContent: main, leftPanel: panel });
|
||||
if (!projectId)
|
||||
return void main.append(el("p", { className: "e04-empty", text: t("E04_NoProject") }));
|
||||
if (!projectId) return void main.append(emptyBox(t("E04_NoProject")));
|
||||
|
||||
// ---- 가운데 ----
|
||||
const map = createPlanMap();
|
||||
@@ -115,19 +141,14 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
let mode: "plan" | "3d" = "plan";
|
||||
let layer: MapLayer = "fs";
|
||||
const toggle = (labels: [LocaleKey, string][], onPick: (value: string) => void) => {
|
||||
const group = el("div", { className: "e04-toggle" });
|
||||
const buttons = labels.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", () => {
|
||||
buttons.forEach((b) => b.classList.toggle("is-on", b === button));
|
||||
onPick(value);
|
||||
});
|
||||
return button;
|
||||
const group = createSegment({
|
||||
options: labels.map(([key, value]) => ({ value, label: t(key) })),
|
||||
value: labels[0][1],
|
||||
size: 24,
|
||||
onChange: onPick,
|
||||
});
|
||||
group.append(...buttons);
|
||||
return group;
|
||||
group.root.classList.add("e04-toggle");
|
||||
return group.root;
|
||||
};
|
||||
const legendSlot = el("div", { className: "e04-legend-slot" });
|
||||
const paintLegend = (): void => legendSlot.replaceChildren(legend(layer));
|
||||
@@ -183,8 +204,8 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
const sectionCard = createPanelCard(t("E04_Card_Sections"), false);
|
||||
const settings = buildSettingsCards();
|
||||
sectionCard.append(sectionList, settings.rangeRow);
|
||||
const progressBar = el("progress", { className: "e04-progress", attrs: { max: "100" } });
|
||||
const progressText = el("p", { className: "e04-progress__text" });
|
||||
const progressBar = createProgressBar();
|
||||
const progressText = note("", "e04-progress__text");
|
||||
const cancelButton = createButton({
|
||||
label: t("E04_Cancel"),
|
||||
variant: "danger-ghost",
|
||||
@@ -192,7 +213,7 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
onClick: () => void api(`/projects/${projectId}/stab/analyze3d/cancel`, { method: "POST" }),
|
||||
});
|
||||
const progressBox = el("div", { className: "e04-progress-box" });
|
||||
progressBox.append(progressBar, progressText, cancelButton);
|
||||
progressBox.append(progressBar.root, progressText, cancelButton);
|
||||
progressBox.hidden = true;
|
||||
const analyzeButton = createButton({
|
||||
label: t("E04_Analyze"),
|
||||
@@ -207,10 +228,10 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
analyzeButton.disabled = reanalyzeButton.disabled = true;
|
||||
const actions = el("div", { className: "ui-sidebar-actions" });
|
||||
actions.append(reanalyzeButton, analyzeButton);
|
||||
const readOnly = el("p", { className: "e04-hint", text: t("E04_ReadOnly") });
|
||||
const readOnly = note(t("E04_ReadOnly"), "e04-hint");
|
||||
readOnly.hidden = true;
|
||||
panel.append(
|
||||
el("p", { className: "e04-hint", text: t("E04_Hint") }),
|
||||
note(t("E04_Hint"), "e04-hint"),
|
||||
readOnly,
|
||||
sectionCard,
|
||||
settings.basicCard,
|
||||
@@ -258,31 +279,25 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
...items.map((section) => {
|
||||
const key = sectionKey(section.start_m, section.end_m);
|
||||
const saved = savedOf.get(key);
|
||||
const button = el("button", {
|
||||
className: `e04-section${key === sectionKey(start_m, end_m) ? " is-active" : ""}`,
|
||||
attrs: { type: "button", "data-key": key },
|
||||
const row = createListRow({
|
||||
label: `${section.start_m.toFixed(0)} – ${section.end_m.toFixed(0)} m`,
|
||||
trailing: saved
|
||||
? `Fs ${saved.f_min.toFixed(2)}${saved.stale ? ` · ${t("E04_Stale")}` : ""}`
|
||||
: "",
|
||||
stripe: saved ? stepColor(FS_STEPS, saved.f_min) : "transparent",
|
||||
active: key === sectionKey(start_m, end_m),
|
||||
onClick: () => settings.setRange(section.start_m, section.end_m),
|
||||
});
|
||||
const chip = el("i", { className: "e04-section__chip" });
|
||||
chip.style.background = saved ? stepColor(FS_STEPS, saved.f_min) : "transparent";
|
||||
button.append(
|
||||
chip,
|
||||
el("span", { text: `${section.start_m.toFixed(0)} – ${section.end_m.toFixed(0)} m` }),
|
||||
el("span", {
|
||||
className: "e04-section__value",
|
||||
text: saved
|
||||
? `Fs ${saved.f_min.toFixed(2)}${saved.stale ? ` · ${t("E04_Stale")}` : ""}`
|
||||
: "",
|
||||
}),
|
||||
);
|
||||
button.addEventListener("click", () => settings.setRange(section.start_m, section.end_m));
|
||||
return button;
|
||||
row.classList.add("e04-section");
|
||||
row.dataset.key = key;
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function paintResult(): void {
|
||||
if (!result) {
|
||||
resultBody.replaceChildren(el("p", { className: "e04-empty", text: t("E04_Result_None") }));
|
||||
resultBody.replaceChildren(emptyBox(t("E04_Result_None")));
|
||||
return;
|
||||
}
|
||||
const s = result.summary;
|
||||
@@ -319,20 +334,33 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
["E04_Result_Trials", s.trials.toLocaleString()],
|
||||
["E04_Result_Time", `${result.elapsed_s.toFixed(1)} s`],
|
||||
];
|
||||
const list = el("dl", { className: "e04-result__list" });
|
||||
for (const [key, value, color] of rows) {
|
||||
const dd = el("dd", { text: value, attrs: { "data-field": key } });
|
||||
if (key === "E04_Result_Verdict") dd.classList.add(`is-${s.verdict}`);
|
||||
const kv: KvRow[] = rows.map(([key, value, color]) => {
|
||||
const cell = document.createDocumentFragment();
|
||||
if (color) {
|
||||
const chip = el("i", { className: "e04-section__chip" });
|
||||
chip.style.background = color;
|
||||
dd.prepend(chip);
|
||||
cell.append(chip);
|
||||
}
|
||||
list.append(el("dt", { text: t(key) }), dd);
|
||||
}
|
||||
cell.append(value);
|
||||
return {
|
||||
label: t(key),
|
||||
value: cell,
|
||||
...(key === "E04_Result_Verdict" ? { ok: s.verdict === "ok" } : {}),
|
||||
};
|
||||
});
|
||||
const list = createKv(kv);
|
||||
list.classList.add("e04-result__list");
|
||||
list.querySelectorAll("dd").forEach((dd, i) => {
|
||||
dd.setAttribute("data-field", rows[i][0]);
|
||||
if (rows[i][0] === "E04_Result_Verdict") dd.classList.add(`is-${s.verdict}`);
|
||||
});
|
||||
const notes: HTMLElement[] = [];
|
||||
if (result.stale) notes.push(el("p", { className: "e04-warn", text: t("E04_Result_Stale") }));
|
||||
notes.push(el("p", { className: "e04-note", text: t("E04_Result_Note") }));
|
||||
if (result.stale) {
|
||||
const warn = createBanner({ tone: "warning", text: t("E04_Result_Stale") });
|
||||
warn.classList.add("e04-warn");
|
||||
notes.push(warn);
|
||||
}
|
||||
notes.push(note(t("E04_Result_Note"), "e04-note"));
|
||||
resultBody.replaceChildren(list, ...notes);
|
||||
}
|
||||
|
||||
@@ -362,8 +390,8 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
if (!p) break;
|
||||
running = p.running;
|
||||
progressBox.hidden = !p.running;
|
||||
progressBar.value = p.percent;
|
||||
progressText.textContent = `${t("E04_Running")} ${p.percent}% · ${p.message ?? ""}`;
|
||||
progressBar.set(p.percent / 100);
|
||||
progressText.replaceChildren(`${t("E04_Running")} ${p.percent}% · ${p.message ?? ""}`);
|
||||
paintButtons();
|
||||
if (!p.running) {
|
||||
if (p.error) showToast(p.error, "error");
|
||||
@@ -404,12 +432,7 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
try {
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
main.replaceChildren(
|
||||
el("p", {
|
||||
className: "e04-empty",
|
||||
text: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
main.replaceChildren(emptyBox(error instanceof Error ? error.message : String(error)));
|
||||
return;
|
||||
}
|
||||
const ov = overview!;
|
||||
|
||||
@@ -29,17 +29,6 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.e04-toggle {
|
||||
display: inline-flex;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.e04-toggle .ui-btn.is-on {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
.e04-stage {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
@@ -99,23 +88,10 @@
|
||||
right: var(--spacing-8);
|
||||
}
|
||||
|
||||
/* 판 · 범례 · 목록 줄 · 값 목록 · 띠 알림은 부품 — 여기는 자리만 */
|
||||
.e04-view3d__tools {
|
||||
position: absolute;
|
||||
top: var(--spacing-8);
|
||||
left: var(--spacing-8);
|
||||
display: flex;
|
||||
gap: var(--spacing-12);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.e04-view3d__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.e04-legend-slot {
|
||||
@@ -125,30 +101,16 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.e04-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;
|
||||
.e04-legend-slot .e04-legend {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.e04-legend__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.e04-legend__swatch,
|
||||
.e04-section__chip {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
flex: none;
|
||||
margin-right: var(--spacing-4);
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
@@ -156,10 +118,7 @@
|
||||
/* 좌측 */
|
||||
.e04-hint,
|
||||
.e04-note {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.e04-sections {
|
||||
@@ -170,35 +129,6 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.e04-section {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.e04-section:hover {
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
.e04-section.is-active {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.e04-section__value {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.e04-range .ui-field,
|
||||
.e04-advanced .ui-field {
|
||||
min-width: 0;
|
||||
@@ -229,58 +159,15 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.e04-progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.e04-progress__text {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 우측 */
|
||||
.e04-result {
|
||||
padding: var(--spacing-12);
|
||||
}
|
||||
|
||||
.e04-result__list {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--spacing-8) var(--spacing-12);
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.e04-result__list dt {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.e04-result__list dd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.e04-result__list dd.is-ok {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.e04-result__list dd.is-ng {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.e04-warn {
|
||||
margin: var(--spacing-12) 0 0;
|
||||
color: var(--color-danger);
|
||||
font-size: 12px;
|
||||
margin-top: var(--spacing-12);
|
||||
}
|
||||
|
||||
.e04-empty {
|
||||
margin: var(--spacing-16);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createTerrainCompass } from "@ui/ui_template_compass";
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import {
|
||||
@@ -46,13 +48,13 @@ export function createView3D(): View3D {
|
||||
const canvas = el("canvas", { className: "e04-view3d__canvas" });
|
||||
const compass = createTerrainCompass({ sizePx: 88, className: "e04-view3d__compass" });
|
||||
// [임계 활동면] 켬 = 활동체를 걷어낸 지표 + 잘린 활동면(Scoops3D 꼴) · 끔 = 원래 지표
|
||||
const slipToggle = el("input", { attrs: { type: "checkbox" } });
|
||||
slipToggle.checked = true;
|
||||
const tools = el("div", { className: "e04-view3d__tools" });
|
||||
const label = el("label", { className: "e04-view3d__toggle" });
|
||||
label.append(slipToggle, el("span", { text: t("E04_SlipSurface") }));
|
||||
tools.append(label);
|
||||
root.append(canvas, compass.root, tools);
|
||||
const slipBox = createCheckbox(t("E04_SlipSurface"), { checked: true });
|
||||
const slipToggle = slipBox.input;
|
||||
slipBox.root.classList.add("e04-view3d__toggle");
|
||||
const tools = createFloatPanel({ popover: true });
|
||||
tools.root.classList.add("e04-view3d__tools");
|
||||
tools.body.append(slipBox.root);
|
||||
root.append(canvas, compass.root, tools.root);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
renderer.setPixelRatio(window.devicePixelRatio || 1);
|
||||
|
||||
@@ -82,3 +82,54 @@ def test_e02_parts():
|
||||
assert "createTable(" in tables and "createTHead" not in tables
|
||||
assert 'className: "ui-field e02-field"' in tables # 칸 이름 줄은 입력 부품의 ui-field
|
||||
assert ".e02-field" not in css and "border-collapse" not in css
|
||||
|
||||
|
||||
def test_e03_parts():
|
||||
names = [
|
||||
"List", "Page", "Plan", "Prob", "ProbChart", "Reinforce", "Result", "Section", "Support", "View3D",
|
||||
] # fmt: skip
|
||||
ts = {n: _src(f"E03_StabSection/E03_StabSection_UI_{n}.ts") for n in names}
|
||||
css = _src("E03_StabSection/E03_StabSection_UI_Style.css")
|
||||
for n, src in ts.items():
|
||||
assert 'el("table"' not in src, n # 표 = 표 부품(`E03_StabSection_UI_Table.ts`)
|
||||
assert 'el("input"' not in src and 'el("select"' not in src, n
|
||||
assert "e03-legend__item" not in src and "e03-legend__swatch" not in src, n
|
||||
assert "createTable(" in _src("E03_StabSection/E03_StabSection_UI_Table.ts")
|
||||
assert "dataset.role = role" in _src("E03_StabSection/E03_StabSection_UI_Table.ts")
|
||||
assert "createLegend(" in _src("E03_StabSection/E03_StabSection_UI_Parts.ts")
|
||||
assert "openModal(" in ts["View3D"] and 'className: "e03-view3d"' not in ts["View3D"]
|
||||
for key in ("station-list", "slice-table", "methods", "whatif-table", "support-table"):
|
||||
assert f'"{key}"' in "".join(ts.values()), key # 촬영 장면 · 시험이 잡는 data-role
|
||||
for dead in (
|
||||
".e03-legend__item", ".e03-legend__swatch", ".e03-table", ".e03-verdict", ".e03-kpi",
|
||||
".e03-view3d {", ".e03-swatch-none", ".e03-list table", ".e03-list th {",
|
||||
): # fmt: skip
|
||||
assert dead not in css, dead
|
||||
|
||||
|
||||
def test_e04_parts():
|
||||
page = _src("E04_Stab3D/E04_Stab3D_UI_Page.ts")
|
||||
view = _src("E04_Stab3D/E04_Stab3D_UI_View3D.ts")
|
||||
css = _src("E04_Stab3D/E04_Stab3D_UI_Style.css")
|
||||
assert 'el("progress"' not in page and "createProgressBar()" in page
|
||||
for part in (
|
||||
"createSegment(",
|
||||
"createListRow(",
|
||||
"createKv(",
|
||||
"createFloatPanel(",
|
||||
"createBanner(",
|
||||
):
|
||||
assert part in page, part
|
||||
assert 'el("input"' not in view and "createCheckbox(" in view
|
||||
assert (
|
||||
'classList.add("e04-toggle")' in page
|
||||
) # 촬영 장면이 `.e04-toggle button[data-value]` 로 잡음
|
||||
assert 'row.classList.add("e04-section")' in page
|
||||
for dead in (
|
||||
".e04-toggle .ui-btn",
|
||||
".e04-legend__row",
|
||||
".e04-legend__swatch",
|
||||
".e04-result__list",
|
||||
".e04-section:hover",
|
||||
):
|
||||
assert dead not in css, dead
|
||||
|
||||
@@ -81,7 +81,7 @@ const tabsMod = load("ui_template/cad_host/cad_host_tabs.ts", { "@ui/ui_template
|
||||
out.fixedX = tabNode("a").kids.length; // 고정 탭 = 글만(지우기 없음)
|
||||
// ③ 지우기 — 물음 뒤 · 지금 탭이면 다음 탭을 실음
|
||||
await tabs.select("c");
|
||||
const xc = tabNode("c").kids.find((k) => k.className === "cad-tabs__x");
|
||||
const xc = tabNode("c").kids.find((k) => k.className.split(" ").includes("cad-tabs__x"));
|
||||
await xc.on.click({ stopPropagation() {} });
|
||||
out.afterRemove = { ids: tabs.tabs().map((t) => t.id), active: tabs.active().id, confirms: confirms.length };
|
||||
// ④ [+] — create 없으면 아무것도 안 함 · 닫기 기능(B07) = 미저장 탭만 물음
|
||||
@@ -97,7 +97,7 @@ const tabsMod = load("ui_template/cad_host/cad_host_tabs.ts", { "@ui/ui_template
|
||||
await closer.open({ id: "d1", name: "종단" });
|
||||
const before = confirms.length;
|
||||
const d2 = closer.element.kids.find((k) => k.dataset.tabId === "d2");
|
||||
await d2.kids.find((k) => k.className === "cad-tabs__x").on.click({ stopPropagation() {} });
|
||||
await d2.kids.find((k) => k.className.split(" ").includes("cad-tabs__x")).on.click({ stopPropagation() {} });
|
||||
const last = closer.element.kids.find((k) => k.dataset.tabId === "d1").kids.length; // 마지막 탭 = 닫기 없음
|
||||
out.b07 = { log: b07, asked: confirms.length - before, ids: closer.tabs().map((t) => t.id), last };
|
||||
|
||||
@@ -137,7 +137,7 @@ const tabsMod = load("ui_template/cad_host/cad_host_tabs.ts", { "@ui/ui_template
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
const bar = host.kids[0].kids.find((k) => k.className === "cad-tabs");
|
||||
const plus = bar.kids.find((k) => k.className === "cad-tabs__add");
|
||||
const plus = bar.kids.find((k) => k.className.split(" ").includes("cad-tabs__add"));
|
||||
await plus.on.click();
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
inCad = { ...inCad, entities: [...inCad.entities, { type: "Circle" }] }; // 새 장에 하나 그림
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
padding: 10px 18px;
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
box-shadow: 0 2px 10px rgb(0 0 0 / 18%);
|
||||
box-shadow: var(--shadow-lg);
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -38,10 +38,10 @@
|
||||
transform: translateX(-50%);
|
||||
max-width: 70%;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--color-danger, #d33);
|
||||
border: 1px solid var(--color-danger);
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-danger, #d33);
|
||||
color: var(--color-danger);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
}
|
||||
|
||||
.cad-frame-edit__label {
|
||||
font-size: 0.78rem;
|
||||
font-size: var(--text-caption);
|
||||
line-height: 1.4;
|
||||
color: var(--color-text);
|
||||
}
|
||||
@@ -99,8 +99,6 @@
|
||||
.cad-frame-edit__buttons > button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
/* 작도 영역 숫자 칸 넷(169-1 · `cad_host_area.ts`) — 두 줄 두 칸 */
|
||||
|
||||
@@ -86,6 +86,7 @@ export function createFrameTemplateEditor<D extends CadHostDrawing>(
|
||||
const finishButton = createButton({
|
||||
label: "완료",
|
||||
variant: "filled",
|
||||
size: 24,
|
||||
onClick: () => void finish(),
|
||||
});
|
||||
/**
|
||||
@@ -95,6 +96,7 @@ export function createFrameTemplateEditor<D extends CadHostDrawing>(
|
||||
const resetButton = createButton({
|
||||
label: "기본 도각 복원",
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: () => void resetToDefault(),
|
||||
});
|
||||
/**
|
||||
@@ -111,12 +113,14 @@ export function createFrameTemplateEditor<D extends CadHostDrawing>(
|
||||
const importButton = createButton({
|
||||
label: "파일 불러오기",
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: () => fileInput.click(),
|
||||
});
|
||||
|
||||
const cancelButton = createButton({
|
||||
label: "취소",
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: () => leave(),
|
||||
});
|
||||
const bannerButtons = document.createElement("div");
|
||||
|
||||
@@ -66,28 +66,10 @@
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.cad-tabs__x,
|
||||
.cad-tabs__add {
|
||||
padding: 0 4px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cad-tabs__x:hover,
|
||||
.cad-tabs__add:hover {
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* × · + = 아이콘 단추 부품(18 · 22) — 여기는 자리만(553-6) */
|
||||
.cad-tabs__add {
|
||||
align-self: center;
|
||||
margin-left: 2px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.cad-tabs[data-busy="true"] .cad-tabs__tab {
|
||||
|
||||
@@ -233,7 +233,8 @@ export function createCadTabs<D>(options: CadTabsOptions<D>): CadTabs<D> {
|
||||
if ((features.remove || (features.close && list.length > 1)) && !tab.fixed) {
|
||||
const x = document.createElement("button");
|
||||
x.type = "button";
|
||||
x.className = "cad-tabs__x";
|
||||
x.className = "ui-icon-btn ui-icon-btn--ghost cad-tabs__x"; // 아이콘 단추 부품 꼴(553-6)
|
||||
x.dataset.size = "18";
|
||||
x.textContent = "×";
|
||||
x.title = x.ariaLabel = features.remove ? text.remove : text.close;
|
||||
x.addEventListener("click", (event) => {
|
||||
@@ -272,7 +273,8 @@ export function createCadTabs<D>(options: CadTabsOptions<D>): CadTabs<D> {
|
||||
if (features.add) {
|
||||
const plus = document.createElement("button");
|
||||
plus.type = "button";
|
||||
plus.className = "cad-tabs__add";
|
||||
plus.className = "ui-icon-btn ui-icon-btn--ghost cad-tabs__add";
|
||||
plus.dataset.size = "22";
|
||||
plus.textContent = "+";
|
||||
plus.title = plus.ariaLabel = text.add;
|
||||
plus.disabled = busy;
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
}
|
||||
|
||||
.ui-sheet__toolbar .ui-btn.is-on {
|
||||
background: var(--color-mist-violet);
|
||||
background: var(--color-accent-soft);
|
||||
}
|
||||
|
||||
.ui-sheet__hint {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary); /* muted 는 작은 글에 대비 3:1 — 모자람 */
|
||||
font-size: 0.8rem;
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-sheet__scroll {
|
||||
|
||||
Reference in New Issue
Block a user