refactor(A00): ☰ 설정 창 · 서랍 터치 고르기 → 부품 상자 — 보조 탭 · 라디오 · 키 칩(배지) · CAD 별칭 표 · 분할 고르기 (PLAN 553-3)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 22:34:04 +09:00
co-authored by Claude Sonnet 5.5
parent 361ce51659
commit 97fc00fafb
4 changed files with 60 additions and 111 deletions
+9 -43
View File
@@ -1,10 +1,11 @@
/* ☰ [설정] 창(PLAN 332-2) — 탭 줄 · 휠 방향 고름 · 표 */
/* ☰ [설정] 창(PLAN 332-2) — 탭 줄 = 부품 보조 탭 · 휠 방향 = 부품 라디오 · CAD 별칭 표 = 부품 표 */
.app-settings .ui-text[hidden] {
display: none; /* 부품 보조 글은 display:flex 라 hidden 을 이김 */
}
.app-settings__tabs {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-8);
padding-bottom: var(--spacing-8);
border-bottom: 1px solid var(--color-border);
}
.app-settings__radios {
@@ -13,25 +14,13 @@
gap: var(--spacing-16);
}
.app-settings__radios > label {
display: inline-flex;
align-items: center;
gap: var(--spacing-8);
.app-settings__radios > .ui-checkbox {
min-height: 32px;
cursor: pointer;
}
.app-settings__table {
width: 100%;
border-collapse: collapse;
}
.app-settings__table th,
.app-settings__table td {
padding: var(--spacing-4) var(--spacing-8);
border-bottom: 1px solid var(--color-border);
text-align: left;
vertical-align: middle;
/* 표 머리는 창 몸통을 따라 붙음 — 부품 표 겉틀의 가로 스크롤은 풂 */
.app-settings .ui-table-wrap {
overflow: visible;
}
.app-settings__table thead th {
@@ -40,18 +29,6 @@
background: var(--color-surface-raised);
}
.app-settings__table input[aria-invalid="true"] {
border-color: var(--color-danger);
}
.app-settings__error {
color: var(--color-danger);
}
.app-settings__error:empty {
display: none;
}
.app-settings__rules {
margin: 0 0 var(--spacing-8);
padding: 0;
@@ -77,14 +54,3 @@
.app-settings__keys:only-child {
grid-column: 1 / -1;
}
.app-settings__keys > kbd {
padding: 0 var(--spacing-8);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface-raised);
color: var(--color-text);
font-family: var(--font-body);
font-size: 0.85em;
line-height: 1.7;
}
+20 -25
View File
@@ -7,12 +7,15 @@
* - 로그인 전엔 이 브라우저에만 남음(계정 올리기는 `b_page_state` 가 로그인 뒤에)
* ========================================================================== */
import { createBadge } from "@ui/ui_template_badge";
import { createCheckbox } from "@ui/ui_template_checkbox";
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { askLeave } from "@ui/ui_template_leave_dialog";
import { t as L } from "@ui/ui_template_locale";
import { openModal } from "@ui/ui_template_modal";
import { writeState } from "./b_page_state";
import { inputPrefs, type InputPrefs, type WheelZoom } from "./input_prefs";
import { fillTabs } from "./b_page_tabs";
import { buildCadAliasTab, checkCadAliases } from "./app_settings_cad";
import "./app_settings.css";
@@ -66,7 +69,7 @@ function ruleList(keys: LocaleKey[]): HTMLElement {
children: [
el("span", {
className: "app-settings__keys",
children: input.split(" · ").map((text) => el("kbd", { text })),
children: input.split(" · ").map((text) => createBadge(text, "neutral")),
}),
...(at < 0 ? [] : [el("span", { text: line.slice(at + 3) })]),
],
@@ -81,12 +84,14 @@ function mouseTab(draft: InputPrefs): HTMLElement {
attrs: { role: "radiogroup", "aria-label": L("App_Settings_Wheel") },
});
for (const [mode, key] of WHEEL) {
const input = el("input", {
attrs: { type: "radio", name: "app-settings-wheel", value: mode },
});
input.checked = draft.wheelZoom === mode;
input.addEventListener("change", () => (draft.wheelZoom = mode));
group.append(el("label", { children: [input, L(key)] }));
group.append(
createCheckbox(L(key), {
radio: "app-settings-wheel",
value: mode,
checked: draft.wheelZoom === mode,
onChange: () => (draft.wheelZoom = mode),
}).root,
);
}
return el("div", {
children: [
@@ -114,25 +119,15 @@ export function openAppSettings(): void {
cad: cad.root,
keys: keysTab(),
};
const tabs = el("div", {
className: "app-settings__tabs ui-subtabs",
attrs: { role: "tablist" },
});
const tabs = el("div", { className: "app-settings__tabs" });
const show = (id: TabId): void => {
tabs.replaceChildren(
...TABS.map(([tab, key]) => {
const button = createButton({
label: L(key),
variant: tab === id ? "filled" : "ghost",
size: "sm",
onClick: () => show(tab),
});
button.setAttribute("role", "tab");
button.setAttribute("aria-selected", String(tab === id));
button.dataset.settingsTab = tab;
return button;
}),
);
fillTabs(tabs, {
tabs: TABS.map(([tab, key]) => ({ key: tab, label: L(key) })),
value: id,
kind: "pill",
data: "settingsTab",
onChange: (tab) => show(tab as TabId),
});
for (const [tab, panel] of Object.entries(panels)) panel.hidden = tab !== id;
};
+17 -15
View File
@@ -8,7 +8,10 @@
* ========================================================================== */
import catalog from "../B07_DesignDetail/openwebcad/src/commands/command-catalog.json";
import { setUiSize } from "@ui/ui_template_button";
import { el } from "@ui/ui_template_elements";
import { createTable } from "@ui/ui_template_table";
import { createText } from "@ui/ui_template_text";
import { t as L } from "@ui/ui_template_locale";
export interface CadCommandRow {
@@ -67,13 +70,15 @@ export function buildCadAliasTab(aliases: Record<string, string[]>): {
root: HTMLElement;
mark: (errors: Record<string, AliasError>) => void;
} {
const message = el("p", { className: "app-settings__error", attrs: { role: "alert" } });
const message = createText("", "error");
message.hidden = true;
const inputs = new Map<string, HTMLInputElement>();
const rows = CAD_COMMANDS.map((command) => {
const input = el("input", {
className: "ui-input ui-input--sm",
className: "ui-input",
attrs: { type: "text", "aria-label": `${command.id} ${L("App_Settings_Cad_Mine")}` },
});
setUiSize(input, 24);
input.value = (aliases[command.id] ?? []).join(", ");
input.dataset.cadCommand = command.id;
input.addEventListener("input", () => {
@@ -82,25 +87,21 @@ export function buildCadAliasTab(aliases: Record<string, string[]>): {
else delete aliases[command.id];
});
inputs.set(command.id, input);
const cells = [command.id, command.label, command.aliases.join(", ")].map((text) =>
el("td", { text }),
);
return el("tr", { children: [...cells, el("td", { children: [input] })] });
return [command.id, command.label, command.aliases.join(", "), input];
});
const head = el("tr", {
children: (
const table = createTable(
(
[
"App_Settings_Cad_Command",
"App_Settings_Cad_Label",
"App_Settings_Cad_Default",
"App_Settings_Cad_Mine",
] as const
).map((key) => el("th", { text: L(key) })),
});
const table = el("table", {
className: "app-settings__table",
children: [el("thead", { children: [head] }), el("tbody", { children: rows })],
});
).map((key) => ({ label: L(key) })),
rows,
{ density: "data" },
);
table.classList.add("app-settings__table");
const mark = (errors: Record<string, AliasError>): void => {
for (const [id, input] of inputs) {
const error = errors[id];
@@ -109,7 +110,8 @@ export function buildCadAliasTab(aliases: Record<string, string[]>): {
input.title = error ? L(ERROR_TEXT[error]) : "";
}
const kinds = [...new Set(Object.values(errors))];
message.textContent = kinds.map((k) => L(ERROR_TEXT[k])).join(" · ");
message.lastChild!.textContent = kinds.map((k) => L(ERROR_TEXT[k])).join(" · ");
message.hidden = !kinds.length;
};
const root = el("div", {
children: [el("p", { text: L("App_Settings_Cad_Help") }), message, table],
+14 -28
View File
@@ -9,6 +9,7 @@
import { ui_locales, currentLanguageIndex, setLanguage } from "@ui/ui_template_locale";
import { createButton } from "@ui/ui_template_elements";
import { createSegment } from "@ui/ui_template_segment";
import { THEME_STORAGE_KEY, LANG_STORAGE_KEY, ROUTES } from "@config/config_frontend";
import { navigateTo, isAuthenticated, setLeaveGuard, takeLeaveGuard } from "./router";
import { readTouchMode, setTouchMode, type TouchMode } from "./device_context";
@@ -246,35 +247,23 @@ function buildTouchModeSwitch(): HTMLElement {
box.className = "app-drawer__touch";
const label = document.createElement("span");
label.textContent = L("App_TouchMode");
const group = document.createElement("div");
group.className = "app-drawer__row";
group.setAttribute("role", "radiogroup");
group.setAttribute("aria-label", L("App_TouchMode"));
const modes: [TouchMode, keyof typeof ui_locales][] = [
["auto", "App_TouchMode_Auto"],
["on", "App_TouchMode_On"],
["off", "App_TouchMode_Off"],
];
const buttons = modes.map(([mode, key]) => {
const b = createButton({
label: L(key),
variant: "pill",
onClick: () => {
setTouchMode(mode);
mark();
},
});
b.setAttribute("role", "radio");
b.dataset.touchMode = mode;
return b;
// 부품 「분할 고르기」(36) — 칸마다 data-touch-mode
const group = createSegment({
options: modes.map(([mode, key]) => ({ value: mode, label: L(key) })),
value: readTouchMode(),
size: 36,
ariaLabel: L("App_TouchMode"),
onChange: (mode) => setTouchMode(mode as TouchMode),
});
const mark = (): void => {
const now = readTouchMode();
for (const b of buttons) b.setAttribute("aria-checked", String(b.dataset.touchMode === now));
};
mark();
group.append(...buttons);
box.append(label, group);
group.root.querySelectorAll<HTMLElement>("[data-value]").forEach((b) => {
b.dataset.touchMode = b.dataset.value;
});
box.append(label, group.root);
return box;
}
@@ -522,11 +511,8 @@ html[data-touch="on"] :is(.app-drawer__nav > *, .app-drawer__site > summary, .ap
color: var(--color-text-secondary);
font-size: var(--text-caption);
}
.app-drawer__touch .app-drawer__row { padding-inline: 0; }
.app-drawer__touch [aria-checked="true"] {
background: var(--color-mist-violet);
color: var(--color-accent);
}
.app-drawer__touch .ui-segment { display: flex; }
.app-drawer__touch .ui-segment__item { flex: 1 1 0; min-width: 0; }
/* 휴대폰 폭 — 오른쪽 단추(언어 · 테마 · 로그인)는 서랍 안으로 */
html[data-size="compact"] .app-header { padding: 0 var(--spacing-16); gap: var(--spacing-8); }
html[data-size="compact"] .app-actions { display: none; }