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