Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
568 lines
20 KiB
TypeScript
568 lines
20 KiB
TypeScript
/* =============================================================================
|
|
* M01_EasyCalc_Entry.ts
|
|
* 쉬운 단가산출 시험 길 — 들어가는 단추 · 큰 창 틀 · 시작 화면 · 고치는 화면(위 띠) · 모든 파일 잇기
|
|
* 모양은 `docs/raw/M01_쉬운단가/설계.md` · 이 폴더째 지워도 됨(폴더 밖 참조는 LogicLab.ts 두 줄).
|
|
* 잇기: 열기(openEasy) → toDoc → renderCards · renderSheet · renderTrialStrip → 바뀔 때마다
|
|
* EasyHistory.push · calcLater → 저장(새로 newEasy · 내 것 editEasy · 품셈 기본은 copyEasy 뒤 그 복사본)
|
|
* · 지우기(usesOf 먼저 · 물음 한 번) · Ctrl+Z · Ctrl+S
|
|
* ========================================================================== */
|
|
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import {
|
|
createButton,
|
|
createInputField,
|
|
createSelectField,
|
|
createTag,
|
|
el,
|
|
showConfirmDialog,
|
|
showToast,
|
|
} from "@ui/ui_template_elements";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import type { CalcAnswer, LogicInput } from "../M01_MasterData_UI_Logic_Api";
|
|
import { formatMoney } from "../M01_MasterData_UI_Logic_Money";
|
|
import {
|
|
calcLater,
|
|
copyEasy,
|
|
deleteEasy,
|
|
editEasy,
|
|
listEasy,
|
|
newEasy,
|
|
openEasy,
|
|
plainError,
|
|
usesOf,
|
|
type EasyListItem,
|
|
} from "./M01_EasyCalc_Api";
|
|
import { renderCards } from "./M01_EasyCalc_Cards";
|
|
import {
|
|
checkName,
|
|
EasyHistory,
|
|
newDoc,
|
|
setEasyModelLanguage,
|
|
toDoc,
|
|
toRow,
|
|
type EasyCard,
|
|
type EasyDoc,
|
|
} from "./M01_EasyCalc_Model";
|
|
import { renderSheet, renderTrialStrip } from "./M01_EasyCalc_Sheet";
|
|
import "./M01_EasyCalc_Style.css";
|
|
|
|
const TEXT = {
|
|
Open: ["쉬운 단가산출(시험)", "Easy unit-price (trial)"],
|
|
Title: ["쉬운 단가산출", "Easy unit-price"],
|
|
CloseWindow: ["창 닫기", "Close window"],
|
|
Close: ["닫기", "Close"],
|
|
Loading: ["불러오는 중", "Loading"],
|
|
LoadFailed: ["목록을 못 불러옴 — {v}", "Could not load the list — {v}"],
|
|
Start_Ask: ["무엇과 비슷한가요?", "What is it similar to?"],
|
|
Start_Search: ["이름으로 찾기 — 예: 거푸집", "Search by name — e.g. formwork"],
|
|
Start_Blank: ["빈 종이로 새로 만들기", "Start from a blank sheet"],
|
|
Start_Mine: ["내가 만든 것", "Made by me"],
|
|
Start_MineNone: ["아직 없음", "None yet"],
|
|
Start_Found: ["찾은 것", "Found"],
|
|
Start_Count: ["{n} 개 중 {m} 개 보임 — 더 좁혀 찾기", "Showing {m} of {n} — narrow the search"],
|
|
Start_None: ["찾은 것 없음", "Nothing found"],
|
|
Per: ["{u} 당", "per {u}"],
|
|
CannotOpen: ["쉬운 길로 못 엶", "Not for the easy view"],
|
|
Name: ["이름", "Name"],
|
|
NamePlaceholder: ["예: 거푸집 설치", "e.g. formwork setup"],
|
|
Spec: ["크기", "Size"],
|
|
Unit: ["단위", "Unit"],
|
|
Total: ["{u} 만드는 데 {v} 원", "{v} won to make {u}"],
|
|
Undo: ["되돌리기", "Undo"],
|
|
Save: ["저장", "Save"],
|
|
Delete: ["지우기", "Delete"],
|
|
Unsaved: ["저장 안 함", "Not saved"],
|
|
Saving: ["저장 중", "Saving"],
|
|
Saved: ["저장했음", "Saved"],
|
|
SaveFailed: ["저장 못 함", "Not saved"],
|
|
BaseNote: [
|
|
"품셈 기본은 읽기만 — 고치려면 아래 단추로 내 것을 만듦",
|
|
"Standard items are read-only — use the button below to make your own copy",
|
|
],
|
|
BaseCopy: ["이걸 바탕으로 고치기", "Edit a copy of this"],
|
|
Copying: ["내 것으로 만드는 중", "Making your copy"],
|
|
Copied: ["내 것으로 만들었음 — 이제 고칠 수 있음", "Your copy is ready — you can edit it now"],
|
|
Cards: ["무엇이 들어가나", "What goes in"],
|
|
Sheet: ["실무 일위대가표로 보기", "View as a unit-cost sheet"],
|
|
BadLines: [
|
|
"⚠ 표시 줄을 먼저 고쳐 주세요 — 빈 이름 · 숫자 아님 · 0 · 0 보다 작은 수는 저장 안 됨",
|
|
"Fix the ⚠ lines first — empty names, non-numbers, 0 or negative amounts can't be saved",
|
|
],
|
|
Uses_Wait: ["쓰는 곳을 찾는 중 — 10초쯤 걸림", "Looking for places that use it — about 10 s"],
|
|
Uses_Block: ["쓰는 곳이 있어 못 지움 — {v}", "Cannot delete — it is used by {v}"],
|
|
Delete_Ask: ["「{v}」 을(를) 지울까요? 되살릴 수 없음", "Delete “{v}”? This cannot be undone"],
|
|
Delete_Ok: ["지우기", "Delete"],
|
|
Deleted: ["지웠음", "Deleted"],
|
|
Leave_Ask: [
|
|
"저장 안 한 것이 있음 — 저장하지 않고 닫을까요?",
|
|
"There are unsaved changes — close without saving?",
|
|
],
|
|
Leave_Ok: ["저장 안 하고 닫기", "Close without saving"],
|
|
} as const;
|
|
|
|
type TextKey = keyof typeof TEXT;
|
|
|
|
const te = (key: TextKey, vars: Record<string, string | number> = {}): string =>
|
|
Object.entries(vars).reduce(
|
|
(text, [k, v]) => text.replace(`{${k}}`, String(v)),
|
|
(TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]) as string,
|
|
);
|
|
|
|
/** 목록에 한 번에 보이는 줄 수 */
|
|
const SHOWN = 50;
|
|
|
|
/** 「원/㎡」 → 「㎡」 */
|
|
const unitOf = (resultUnit: string): string => resultUnit.replace(/^원\//, "");
|
|
/** 「㎡」 → 「1㎡」 · 「100m」 은 그대로 */
|
|
const amountOf = (resultUnit: string): string => {
|
|
const unit = unitOf(resultUnit);
|
|
return /^\d/.test(unit) ? unit : `1${unit}`;
|
|
};
|
|
|
|
/** 저장을 막는 줄 — 이름 빔 · 수량이 숫자 아님 · 0 · 음수(서버가 0 · 음수를 받아 줌) · 같은 줄 · 너무 큼은 경고만 */
|
|
const blocksSave = (c: EasyCard): boolean =>
|
|
!c.name.trim() ||
|
|
(c.qtyFormula === null && !(c.qty !== null && Number.isFinite(c.qty) && c.qty > 0));
|
|
|
|
/** 카드를 넣어 새로 생긴 설계 값의 시험값 — 서버 자동값(`master_auto.values`)과 같은 규칙:
|
|
* 고르기 = 첫째 · 범위 = 낮은 끝 · 그 밖 숫자 0 · 값이 「원」 인 가격 값은 비워 둠(0 원처럼 안 보이게) */
|
|
function fillNew(
|
|
inputs: LogicInput[],
|
|
trial: Record<string, unknown>,
|
|
known: Set<string>,
|
|
): Record<string, unknown> {
|
|
const out = { ...trial };
|
|
for (const spec of inputs) {
|
|
if (known.has(spec.이름)) continue;
|
|
known.add(spec.이름);
|
|
if (out[spec.이름] !== undefined || String(spec.단위 ?? "").includes("원")) continue;
|
|
out[spec.이름] = spec.고르기?.length ? spec.고르기[0] : (spec.범위?.[0] ?? 0);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 「추가」 옆에 놓는 단추 — 누르면 큰 창 */
|
|
export function createEasyCalcButton(): HTMLButtonElement {
|
|
return createButton({ label: te("Open"), variant: "filled", onClick: openEasyCalc });
|
|
}
|
|
|
|
function openEasyCalc(): void {
|
|
setEasyModelLanguage(currentLanguageIndex);
|
|
let body: HTMLElement;
|
|
/** 고치는 화면이 열려 있을 때만 */
|
|
let paper: { history: EasyHistory; save: () => Promise<void>; undo: () => void } | null = null;
|
|
let logics: EasyListItem[] | null = null;
|
|
let query = "";
|
|
|
|
/** 저장 안 한 것이 있으면 한 번 물음 — 닫아도 되면 true */
|
|
const mayLeave = async (): Promise<boolean> =>
|
|
!paper?.history.dirty || showConfirmDialog(te("Leave_Ask"), te("Leave_Ok"));
|
|
|
|
/** Ctrl+Z · Ctrl+S — 이 창 안에서만(고르기 창 등 다른 창은 제 몫) */
|
|
const onKey = (ev: KeyboardEvent): void => {
|
|
if (!paper || !(ev.ctrlKey || ev.metaKey)) return;
|
|
const key = ev.key.toLowerCase();
|
|
if (key === "z" && !ev.shiftKey) {
|
|
ev.preventDefault();
|
|
paper.undo();
|
|
} else if (key === "s") {
|
|
ev.preventDefault();
|
|
void paper.save();
|
|
}
|
|
};
|
|
|
|
/* ---------- 시작 화면 ---------- */
|
|
|
|
const listRow = (item: EasyListItem): HTMLElement => {
|
|
const row = el("button", {
|
|
className: "m01-easycalc__item",
|
|
attrs: { type: "button", title: item.where },
|
|
children: [
|
|
el("span", { className: "m01-easycalc__item-name", text: item.name }),
|
|
createTag(item.owner, item.isBase ? "neutral" : "accent"),
|
|
el("span", {
|
|
className: "m01-easycalc__item-unit",
|
|
text: item.canOpen ? te("Per", { u: amountOf(item.resultUnit) }) : item.resultUnit,
|
|
}),
|
|
...(item.canOpen ? [] : [createTag(te("CannotOpen"), "warning")]),
|
|
],
|
|
});
|
|
if (item.whyNot) row.title = item.whyNot;
|
|
row.disabled = !item.canOpen;
|
|
row.addEventListener("click", () => void openPaper(item.key));
|
|
return row;
|
|
};
|
|
|
|
const showStart = async (): Promise<void> => {
|
|
paper = null;
|
|
const status = el("p", { className: "m01-easycalc__muted", text: te("Loading") });
|
|
const mineHost = el("div", { className: "m01-easycalc__list" });
|
|
const foundHost = el("div", { className: "m01-easycalc__list" });
|
|
const search = createInputField({
|
|
type: "search",
|
|
placeholder: te("Start_Search"),
|
|
value: query,
|
|
onInput: (v) => {
|
|
query = v;
|
|
drawFound();
|
|
},
|
|
});
|
|
search.root.classList.add("m01-easycalc__search");
|
|
|
|
const drawFound = (): void => {
|
|
if (!logics) return;
|
|
const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
const hits = words.length
|
|
? logics.filter((x) => words.every((w) => x.name.toLowerCase().includes(w)))
|
|
: logics;
|
|
status.textContent = !hits.length
|
|
? te("Start_None")
|
|
: hits.length > SHOWN
|
|
? te("Start_Count", { n: hits.length.toLocaleString(), m: SHOWN })
|
|
: "";
|
|
foundHost.replaceChildren(...hits.slice(0, SHOWN).map(listRow));
|
|
};
|
|
|
|
body.replaceChildren(
|
|
el("div", {
|
|
className: "m01-easycalc__start",
|
|
children: [
|
|
el("div", {
|
|
className: "m01-easycalc__start-col",
|
|
children: [
|
|
el("h3", { className: "m01-easycalc__ask", text: te("Start_Ask") }),
|
|
search.root,
|
|
el("h4", { text: te("Start_Found") }),
|
|
status,
|
|
foundHost,
|
|
],
|
|
}),
|
|
el("div", {
|
|
className: "m01-easycalc__start-col m01-easycalc__start-side",
|
|
children: [
|
|
createBigButton(te("Start_Blank"), () => openBlank()),
|
|
el("h4", { text: te("Start_Mine") }),
|
|
mineHost,
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
search.input.focus();
|
|
|
|
try {
|
|
logics ??= await listEasy("");
|
|
} catch (error) {
|
|
status.textContent = te("LoadFailed", { v: plainError(error) });
|
|
return;
|
|
}
|
|
const mine = logics.filter((x) => !x.isBase);
|
|
mineHost.replaceChildren(
|
|
...(mine.length
|
|
? mine.map(listRow)
|
|
: [el("p", { className: "m01-easycalc__muted", text: te("Start_MineNone") })]),
|
|
);
|
|
drawFound();
|
|
};
|
|
|
|
/** 단위 고르기 후보 — 목록에 다섯 번 넘게 쓰인 「원/…」 차례 · 설명 붙은 드문 단위는 뺌(연 로직 것은 따로 더함) */
|
|
const unitChoices = (): string[] => {
|
|
const count = new Map<string, number>();
|
|
for (const x of logics ?? []) {
|
|
if (x.canOpen) count.set(x.resultUnit, (count.get(x.resultUnit) ?? 0) + 1);
|
|
}
|
|
return [...count.entries()]
|
|
.filter(([u, n]) => n >= 5 && !/[\s()「·]/.test(u))
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([u]) => u);
|
|
};
|
|
|
|
/* ---------- 고치는 화면 ---------- */
|
|
|
|
const openBlank = (): void => showPaper(newDoc("", unitOf(unitChoices()[0] ?? "원/㎡")), {});
|
|
|
|
const openPaper = async (key: string): Promise<void> => {
|
|
body.replaceChildren(el("p", { className: "m01-easycalc__muted", text: te("Loading") }));
|
|
try {
|
|
const opened = await openEasy(key);
|
|
showPaper(
|
|
toDoc(opened.row, { key, version: opened.version, isBase: opened.isBase }),
|
|
opened.trial,
|
|
);
|
|
} catch (error) {
|
|
showToast(plainError(error), "error");
|
|
void showStart();
|
|
}
|
|
};
|
|
|
|
const showPaper = (first: EasyDoc, firstTrial: Record<string, unknown>): void => {
|
|
let doc = first;
|
|
let trial = firstTrial;
|
|
let ans: CalcAnswer | null = null;
|
|
let history = new EasyHistory();
|
|
history.push(doc);
|
|
let busy = false;
|
|
|
|
const unsaved = createTag(te("Unsaved"), "warning");
|
|
const total = el("div", { className: "m01-easycalc__total" });
|
|
const note = el("p", { className: "m01-easycalc__note", attrs: { role: "status" } });
|
|
const trialHost = el("div", { className: "m01-easycalc__part m01-easycalc__trial" });
|
|
const cardsHost = el("div", { className: "m01-easycalc__cards" });
|
|
const sheetHost = el("div");
|
|
let trialShape = "";
|
|
/** 연 때 있던 설계 값 — 서버 자동값을 그대로 둠 · 이 밖에 새로 생긴 값만 채움 */
|
|
const known = new Set(doc.inputs.map((i) => i.이름));
|
|
|
|
const name = createInputField({
|
|
label: te("Name"),
|
|
placeholder: te("NamePlaceholder"),
|
|
onInput: (v) => edit({ ...doc, name: v }),
|
|
});
|
|
name.root.classList.add("m01-easycalc__name");
|
|
const spec = createInputField({ label: te("Spec"), onInput: (v) => edit({ ...doc, spec: v }) });
|
|
const unit = createSelectField({
|
|
label: te("Unit"),
|
|
options: [],
|
|
onChange: (v) => edit({ ...doc, resultUnit: v }),
|
|
});
|
|
const undo = createButton({ label: te("Undo"), variant: "ghost", onClick: () => doUndo() });
|
|
const saveButton = createButton({ label: te("Save"), onClick: () => void doSave() });
|
|
const deleteButton = createButton({
|
|
label: te("Delete"),
|
|
variant: "danger",
|
|
onClick: () => void doDelete(),
|
|
});
|
|
const close = createButton({
|
|
label: te("Close"),
|
|
variant: "ghost",
|
|
onClick: () =>
|
|
void (async () => {
|
|
if (await mayLeave()) void showStart();
|
|
})(),
|
|
});
|
|
const actions = el("div", { className: "m01-easycalc__actions" });
|
|
const baseBar = el("div", {
|
|
className: "m01-easycalc__base",
|
|
children: [
|
|
el("p", { text: te("BaseNote") }),
|
|
createBigButton(te("BaseCopy"), () => void doCopy()),
|
|
],
|
|
});
|
|
|
|
/** 위 띠 칸을 doc 로 — 열 때 · 되돌릴 때 · 저장 뒤 */
|
|
const fillStrip = (): void => {
|
|
const units = unitChoices();
|
|
if (!units.includes(doc.resultUnit)) units.unshift(doc.resultUnit);
|
|
unit.setOptions(
|
|
units.map((u) => ({ value: u, text: unitOf(u) })),
|
|
doc.resultUnit,
|
|
);
|
|
name.input.value = doc.name;
|
|
spec.input.value = doc.spec;
|
|
for (const box of [name.input, spec.input, unit.select]) box.disabled = doc.isBase;
|
|
actions.replaceChildren(
|
|
...(doc.isBase ? [] : [undo, saveButton]),
|
|
...(doc.key && !doc.isBase ? [deleteButton] : []),
|
|
close,
|
|
);
|
|
baseBar.hidden = !doc.isBase;
|
|
};
|
|
|
|
/** 금액 · 카드 · 표 — 바뀔 때마다(pending) · 계산 답이 올 때마다(answer) */
|
|
const draw = (pending: boolean, answer = false): void => {
|
|
const value = ans?.ok ? formatMoney(ans.result ?? ans.sums?.["계"] ?? 0) : "—";
|
|
total.textContent = te("Total", { u: amountOf(doc.resultUnit), v: value });
|
|
total.classList.toggle("is-pending", pending);
|
|
sheetHost.classList.toggle("is-pending", pending);
|
|
unsaved.hidden = !history.dirty;
|
|
undo.disabled = !history.canUndo;
|
|
const shape = JSON.stringify(doc.inputs);
|
|
if (shape !== trialShape) {
|
|
trialShape = shape;
|
|
trial = fillNew(doc.inputs, trial, known);
|
|
renderTrialStrip(trialHost, doc.inputs, trial, (next) => {
|
|
trial = next;
|
|
calc();
|
|
});
|
|
}
|
|
drawCards(answer);
|
|
renderSheet(sheetHost, { name: doc.name, resultUnit: doc.resultUnit }, ans);
|
|
};
|
|
|
|
/** 계산 답만 왔을 때 카드 칸에 글을 넣는 중이면 그 칸을 떠난 뒤 그림(넣던 글이 안 날아가게).
|
|
* doc 가 바뀌면 늘 바로 그림 — 카드가 옛 doc 를 쥐고 있으면 그다음 조작이 옛 판본 위에 쌓임 */
|
|
let waiting = false;
|
|
const drawCards = (answer = false): void => {
|
|
const active = document.activeElement;
|
|
if (answer && active instanceof HTMLInputElement && cardsHost.contains(active)) {
|
|
if (!waiting) {
|
|
waiting = true;
|
|
cardsHost.addEventListener(
|
|
"focusout",
|
|
() =>
|
|
setTimeout(() => {
|
|
waiting = false;
|
|
drawCards(true);
|
|
}),
|
|
{ once: true },
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
renderCards(cardsHost, doc, ans, { change: edit, trial: () => trial });
|
|
};
|
|
|
|
const calc = (): void => {
|
|
draw(true);
|
|
calcLater(toRow(doc), trial, (next, plain) => {
|
|
ans = next;
|
|
note.textContent = plain ?? "";
|
|
draw(false, true);
|
|
});
|
|
};
|
|
|
|
const edit = (next: EasyDoc): void => {
|
|
doc = next;
|
|
history.push(doc);
|
|
calc();
|
|
};
|
|
|
|
const doUndo = (): void => {
|
|
const back = history.undo();
|
|
if (!back) return;
|
|
doc = back;
|
|
fillStrip();
|
|
calc();
|
|
};
|
|
|
|
/** 저장 뒤 · 본뜬 뒤 — 서버가 준 줄로 다시 엶 · 되돌리기 기록은 여기서 새로 */
|
|
const reopen = (next: EasyDoc): void => {
|
|
doc = next;
|
|
history = new EasyHistory();
|
|
history.push(doc);
|
|
logics = null;
|
|
fillStrip();
|
|
calc();
|
|
};
|
|
|
|
const doSave = async (): Promise<void> => {
|
|
if (doc.isBase || busy) return;
|
|
const mine = (logics ?? []).filter((x) => !x.isBase && x.key !== doc.key).map((x) => x.name);
|
|
const bad = checkName(doc.name, mine);
|
|
name.setError(bad ?? undefined);
|
|
if (bad) return void name.input.focus();
|
|
if (doc.cards.some(blocksSave)) return void (note.textContent = te("BadLines"));
|
|
busy = true;
|
|
saveButton.disabled = true;
|
|
note.textContent = te("Saving");
|
|
try {
|
|
const row = toRow(doc);
|
|
const saved =
|
|
doc.key && doc.version ? await editEasy(doc.key, doc.version, row) : await newEasy(row);
|
|
reopen(toDoc(saved.row, { key: saved.key, version: saved.version, isBase: false }));
|
|
note.textContent = "";
|
|
showToast(te("Saved"), "success");
|
|
} catch (error) {
|
|
note.textContent = `${te("SaveFailed")} — ${plainError(error)}`;
|
|
} finally {
|
|
busy = false;
|
|
saveButton.disabled = false;
|
|
}
|
|
};
|
|
|
|
const doCopy = async (): Promise<void> => {
|
|
if (!doc.key || busy) return;
|
|
busy = true;
|
|
note.textContent = te("Copying");
|
|
try {
|
|
const saved = await copyEasy(doc.key);
|
|
reopen(toDoc(saved.row, { key: saved.key, version: saved.version, isBase: false }));
|
|
showToast(te("Copied"), "success");
|
|
} catch (error) {
|
|
note.textContent = plainError(error);
|
|
} finally {
|
|
busy = false;
|
|
}
|
|
};
|
|
|
|
const doDelete = async (): Promise<void> => {
|
|
if (!doc.key || !doc.version || doc.isBase || busy) return;
|
|
busy = true;
|
|
deleteButton.disabled = true;
|
|
note.textContent = te("Uses_Wait");
|
|
try {
|
|
const uses = await usesOf(doc.key);
|
|
const blocking = uses.filter((u) => u.key);
|
|
if (blocking.length) {
|
|
note.textContent = te("Uses_Block", {
|
|
v: blocking.map((u) => (u.where ? `${u.where} ${u.name}` : u.name)).join(", "),
|
|
});
|
|
return;
|
|
}
|
|
note.textContent = uses.map((u) => u.name).join(" · ");
|
|
if (!(await showConfirmDialog(te("Delete_Ask", { v: doc.name }), te("Delete_Ok")))) {
|
|
return;
|
|
}
|
|
await deleteEasy(doc.key, doc.version);
|
|
logics = null;
|
|
paper = null;
|
|
showToast(te("Deleted"), "success");
|
|
void showStart();
|
|
} catch (error) {
|
|
note.textContent = plainError(error);
|
|
} finally {
|
|
busy = false;
|
|
deleteButton.disabled = false;
|
|
}
|
|
};
|
|
|
|
paper = {
|
|
get history() {
|
|
return history;
|
|
},
|
|
save: doSave,
|
|
undo: doUndo,
|
|
};
|
|
|
|
body.replaceChildren(
|
|
el("div", {
|
|
className: "m01-easycalc__strip",
|
|
children: [name.root, spec.root, unit.root, total, unsaved, actions],
|
|
}),
|
|
note,
|
|
baseBar,
|
|
trialHost,
|
|
el("section", {
|
|
className: "m01-easycalc__part",
|
|
children: [el("h4", { text: te("Cards") }), cardsHost],
|
|
}),
|
|
el("details", {
|
|
className: "m01-easycalc__part m01-easycalc__sheet",
|
|
children: [el("summary", { text: te("Sheet") }), sheetHost],
|
|
}),
|
|
);
|
|
fillStrip();
|
|
calc();
|
|
if (!doc.isBase) name.input.focus();
|
|
};
|
|
|
|
// 화면 함수들을 다 만든 뒤에 엶 — mount 가 바로 showStart 를 부름
|
|
openModal({
|
|
title: te("Title"),
|
|
closeLabel: te("CloseWindow"),
|
|
dialogClass: "m01-easycalc",
|
|
beforeClose: mayLeave,
|
|
mount: (host) => {
|
|
body = host;
|
|
(host.parentElement ?? host).addEventListener("keydown", onKey);
|
|
void showStart();
|
|
},
|
|
});
|
|
}
|
|
|
|
function createBigButton(label: string, onClick: () => void): HTMLButtonElement {
|
|
const button = createButton({ label, variant: "filled", onClick });
|
|
button.classList.add("m01-easycalc__big");
|
|
return button;
|
|
}
|