Files
Aislo/A00_Common/app_settings.ts
T
eomsangdonandClaude Sonnet 5.5 cadff2af90 feat(설정): 357-1·2 저장 버튼 [닫기]와 같은 크기 · 저장하면 바로 닫힘 · 단축키 탭 한 줄씩(입력 = 동작) · 키 칩
- 단추 → 버튼 (마우스 · 다른 곳 단축키 글)
- test_app_settings 저장 뒤 닫힘으로 고침

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
2026-10-16 20:38:46 +09:00

175 lines
6.1 KiB
TypeScript

/* =============================================================================
* app_settings.ts
* ☰ [설정] 창(PLAN 332-2 · 요구 224-8) — 탭 셋: ① 마우스 ② CAD 단축키 ③ 다른 곳 단축키(읽기 전용).
*
* - 고침은 창 안 사본에만 · [저장] 때만 계정 pref(`input-prefs`)로 한 번 씀 — 읽기는 `input_prefs.ts`
* - 저장 안 한 고침이 있으면 닫을 때 물음(askLeave) · 잘못 있는 별칭은 저장 안 함
* - 로그인 전엔 이 브라우저에만 남음(계정 올리기는 `b_page_state` 가 로그인 뒤에)
* ========================================================================== */
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { askLeave } from "@ui/ui_template_leave_dialog";
import { t as L } from "@ui/ui_template_locale";
import { openModal } from "@ui/ui_template_modal";
import { writeState } from "./b_page_state";
import { inputPrefs, type InputPrefs, type WheelZoom } from "./input_prefs";
import { buildCadAliasTab, checkCadAliases } from "./app_settings_cad";
import "./app_settings.css";
type LocaleKey = Parameters<typeof L>[0];
type TabId = "mouse" | "cad" | "keys";
const TABS: [TabId, LocaleKey][] = [
["mouse", "App_Settings_Tab_Mouse"],
["cad", "App_Settings_Tab_Cad"],
["keys", "App_Settings_Tab_Keys"],
];
const WHEEL: [WheelZoom, LocaleKey][] = [
["pull", "App_Settings_Wheel_Pull"],
["push", "App_Settings_Wheel_Push"],
];
const MOUSE_RULES: LocaleKey[] = [
"App_Settings_Rule_Pan",
"App_Settings_Rule_Pick",
"App_Settings_Rule_Right",
"App_Settings_Rule_Pinch",
];
/** ③ 안내 — [자리, 키] */
const KEY_ROWS: [LocaleKey, LocaleKey][] = [
["App_Settings_Keys_All", "App_Settings_Keys_All_Keys"],
["App_Settings_Keys_Profile", "App_Settings_Keys_Profile_Keys"],
["App_Settings_Keys_RouteEdit", "App_Settings_Keys_RouteEdit_Keys"],
["App_Settings_Keys_Section", "App_Settings_Keys_Section_Keys"],
["App_Settings_Keys_Cad", "App_Settings_Keys_Cad_Keys"],
["App_Settings_Keys_Table", "App_Settings_Keys_Table_Keys"],
["App_Settings_Keys_M02", "App_Settings_Keys_M02_Keys"],
];
/** 비교 · 저장 꼴 — 별칭 명령 차례를 맞춤 */
const normalize = (p: InputPrefs): InputPrefs => ({
wheelZoom: p.wheelZoom,
cadAliases: Object.fromEntries(Object.entries(p.cadAliases).sort(([a], [b]) => (a < b ? -1 : 1))),
});
const snapshot = (p: InputPrefs): string => JSON.stringify(normalize(p));
/** 규칙 목록 — 한 줄 = 「입력 = 동작」 · 입력은 키 칩(` · ` 로 여럿) · 동작 없는 줄은 칩만 */
function ruleList(keys: LocaleKey[]): HTMLElement {
const items = keys
.flatMap((key) => L(key).split("\n"))
.map((line) => {
const at = line.indexOf(" = ");
const input = at < 0 ? line : line.slice(0, at);
return el("li", {
children: [
el("span", {
className: "app-settings__keys",
children: input.split(" · ").map((text) => el("kbd", { text })),
}),
...(at < 0 ? [] : [el("span", { text: line.slice(at + 3) })]),
],
});
});
return el("ul", { className: "app-settings__rules", children: items });
}
function mouseTab(draft: InputPrefs): HTMLElement {
const group = el("div", {
className: "app-settings__radios",
attrs: { role: "radiogroup", "aria-label": L("App_Settings_Wheel") },
});
for (const [mode, key] of WHEEL) {
const input = el("input", {
attrs: { type: "radio", name: "app-settings-wheel", value: mode },
});
input.checked = draft.wheelZoom === mode;
input.addEventListener("change", () => (draft.wheelZoom = mode));
group.append(el("label", { children: [input, L(key)] }));
}
return el("div", {
children: [
el("h4", { text: L("App_Settings_Wheel") }),
group,
el("h4", { text: L("App_Settings_Rules") }),
ruleList(MOUSE_RULES),
],
});
}
function keysTab(): HTMLElement {
return el("div", {
children: KEY_ROWS.flatMap(([place, keys]) => [el("h4", { text: L(place) }), ruleList([keys])]),
});
}
/** ☰ [설정] — 창을 열고 지금 설정을 사본으로 고침 */
export function openAppSettings(): void {
const draft = normalize(inputPrefs());
let saved = snapshot(draft);
const cad = buildCadAliasTab(draft.cadAliases);
const panels: Record<TabId, HTMLElement> = {
mouse: mouseTab(draft),
cad: cad.root,
keys: keysTab(),
};
const tabs = el("div", {
className: "app-settings__tabs ui-subtabs",
attrs: { role: "tablist" },
});
const show = (id: TabId): void => {
tabs.replaceChildren(
...TABS.map(([tab, key]) => {
const button = createButton({
label: L(key),
variant: tab === id ? "filled" : "ghost",
size: "sm",
onClick: () => show(tab),
});
button.setAttribute("role", "tab");
button.setAttribute("aria-selected", String(tab === id));
button.dataset.settingsTab = tab;
return button;
}),
);
for (const [tab, panel] of Object.entries(panels)) panel.hidden = tab !== id;
};
/** [저장] — 잘못 있는 별칭이면 CAD 탭을 보이고 안 씀 */
const save = (): boolean => {
const errors = checkCadAliases(draft.cadAliases);
cad.mark(errors);
if (Object.keys(errors).length) {
show("cad");
showToast(L("App_Settings_Cad_Invalid"), "error");
return false;
}
if (snapshot(draft) !== saved) writeState("input-prefs", normalize(draft));
saved = snapshot(draft);
showToast(L("App_Settings_Saved"), "success");
return true;
};
const saveButton = createButton({
label: L("Common_Btn_Save"),
variant: "filled",
onClick: () => save() && modal.close(),
});
const modal = openModal({
title: L("App_Settings"),
closeLabel: L("Common_Btn_Close"),
dialogClass: "app-settings",
actions: [saveButton],
beforeClose: async () => {
if (snapshot(draft) === saved) return true;
const choice = await askLeave(L("App_Settings_Leave"));
return choice === "discard" || (choice === "save" && save());
},
mount: (body) => {
show("mouse");
body.append(tabs, ...Object.values(panels));
},
});
}