From 97fc00fafb3298a7e222cada45ccce1e92557b88 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 22:34:04 +0900 Subject: [PATCH] =?UTF-8?q?refactor(A00):=20=E2=98=B0=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B0=BD=20=C2=B7=20=EC=84=9C=EB=9E=8D=20=ED=84=B0=EC=B9=98?= =?UTF-8?q?=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=E2=86=92=20=EB=B6=80=ED=92=88?= =?UTF-8?q?=20=EC=83=81=EC=9E=90=20=E2=80=94=20=EB=B3=B4=EC=A1=B0=20?= =?UTF-8?q?=ED=83=AD=20=C2=B7=20=EB=9D=BC=EB=94=94=EC=98=A4=20=C2=B7=20?= =?UTF-8?q?=ED=82=A4=20=EC=B9=A9(=EB=B0=B0=EC=A7=80)=20=C2=B7=20CAD=20?= =?UTF-8?q?=EB=B3=84=EC=B9=AD=20=ED=91=9C=20=C2=B7=20=EB=B6=84=ED=95=A0=20?= =?UTF-8?q?=EA=B3=A0=EB=A5=B4=EA=B8=B0=20(PLAN=20553-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- A00_Common/app_settings.css | 52 ++++++---------------------------- A00_Common/app_settings.ts | 45 +++++++++++++---------------- A00_Common/app_settings_cad.ts | 32 +++++++++++---------- A00_Common/app_shell.ts | 42 +++++++++------------------ 4 files changed, 60 insertions(+), 111 deletions(-) diff --git a/A00_Common/app_settings.css b/A00_Common/app_settings.css index c7d32a4d9..a1cdc7da4 100644 --- a/A00_Common/app_settings.css +++ b/A00_Common/app_settings.css @@ -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; -} diff --git a/A00_Common/app_settings.ts b/A00_Common/app_settings.ts index 8e8efc4d5..17e6cbb27 100644 --- a/A00_Common/app_settings.ts +++ b/A00_Common/app_settings.ts @@ -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; }; diff --git a/A00_Common/app_settings_cad.ts b/A00_Common/app_settings_cad.ts index 217623f03..b32884f86 100644 --- a/A00_Common/app_settings_cad.ts +++ b/A00_Common/app_settings_cad.ts @@ -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): { root: HTMLElement; mark: (errors: Record) => void; } { - const message = el("p", { className: "app-settings__error", attrs: { role: "alert" } }); + const message = createText("", "error"); + message.hidden = true; const inputs = new Map(); 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): { 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): void => { for (const [id, input] of inputs) { const error = errors[id]; @@ -109,7 +110,8 @@ export function buildCadAliasTab(aliases: Record): { 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], diff --git a/A00_Common/app_shell.ts b/A00_Common/app_shell.ts index 018c26a4e..b79578b37 100644 --- a/A00_Common/app_shell.ts +++ b/A00_Common/app_shell.ts @@ -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("[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; }