diff --git a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts index 6f068462c..6e03e078e 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts @@ -147,7 +147,7 @@ export function renderInputPanel( children: [...GROUPS, "다른 로직" as const, "변수" as const].map((k) => createButton({ label: k === "변수" ? t("Variable") : k === "다른 로직" ? t("Logic") : t(k), - variant: k === kind ? "filled" : "pill", + variant: k === kind ? "filled" : "ghost", size: "sm", onClick: () => k !== kind && on.change(setKind(lg, boxId, k)), }), @@ -193,7 +193,7 @@ function renderGroup( ...(["fixed", "rule"] as const).map((m) => createButton({ label: m === "fixed" ? t("ModeFixed") : t("ModeRule"), - variant: mode === m ? "filled" : "pill", + variant: mode === m ? "filled" : "ghost", size: "sm", onClick: () => ((mode = m), drawChips(), draw()), }), diff --git a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts index 0d5ec6384..6203674e2 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts @@ -445,7 +445,7 @@ export function renderMaterial( ...(["fixed", "choice", "rule"] as CfMaterialMode[]).map((m) => createButton({ label: m === "fixed" ? t("Fixed") : m === "choice" ? t("Choice") : t("Rule"), - variant: mode === m ? "filled" : "pill", + variant: mode === m ? "filled" : "ghost", size: "sm", onClick: () => ((mode = m), drawChips(), drawBody()), }), diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts index 69d7b1ef4..385de7bbd 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts @@ -92,18 +92,20 @@ export function buildPicker(opt: PickerOptions): { root: HTMLElement } { let seq = 0; const buttons = tabs.map((tab) => { - const b = el("button", { - className: "m02-layers__btn", - text: tab.label, - attrs: { type: "button", "data-source": tab.id }, - }); - b.addEventListener("click", () => void show(tab)); + const b = createButton({ label: tab.label, variant: "ghost", onClick: () => void show(tab) }); + b.classList.add("m02-layers__btn"); + b.dataset.source = tab.id; return b; }); async function show(tab: Source): Promise { const mine = ++seq; - buttons.forEach((b) => b.classList.toggle("is-active", b.dataset.source === tab.id)); + buttons.forEach((b) => { + const on = b.dataset.source === tab.id; + b.classList.toggle("is-active", on); + b.classList.toggle("ui-btn--filled", on); + b.classList.toggle("ui-btn--ghost", !on); + }); people.root.hidden = tab.id !== "people"; list.message(); try { diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css index 9d656bd9f..760776347 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css @@ -42,28 +42,6 @@ gap: var(--spacing-4); } -.m02-layers__btn { - padding: var(--spacing-4) var(--spacing-8); - border: 1px solid var(--color-border); - border-radius: var(--radius-buttons); - background: var(--color-surface); - color: var(--color-text); - font: inherit; - font-size: var(--text-body-sm); - cursor: pointer; -} - -.m02-layers__btn:hover { - background: var(--color-mist-violet); -} - -.m02-layers__btn.is-active { - border-color: var(--color-accent); - background: var(--color-mist-violet); - color: var(--color-accent); - font-weight: 600; -} - /* 층 단추 줄 오른쪽 끝 — 「N개 고름」 · 「+」(PLAN 30-2) */ .m02-tree__tools { display: inline-flex; @@ -94,28 +72,6 @@ background: var(--color-mist-violet); } -/* 층 단추 옆 「+」 — 나무 맨 위 폴더 · 항목 더하기(PLAN 29-6) */ -.m02-tree__add { - min-width: 1.75rem; - padding: var(--spacing-4) var(--spacing-8); - border: 1px dashed var(--color-border); - border-radius: var(--radius-buttons); - background: none; - color: var(--color-text-secondary); - font: inherit; - font-weight: 600; - cursor: pointer; -} - -.m02-tree__add:hover { - border-color: var(--color-accent); - color: var(--color-accent); -} - -.m02-tree__add[hidden] { - display: none; -} - /* 남의 회사 「개인」 사람 나무 — 고른 사람 줄 아래 그 사람 라이브러리(PLAN 29-6) */ .m02-people__panel { padding-left: var(--spacing-16); diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 99402a371..3fa73d61b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -10,7 +10,13 @@ * ========================================================================== */ import "./M02_MasterTemplete_TreePanel.css"; -import { createInputField, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; +import { + createButton, + createInputField, + el, + showConfirmDialog, + showToast, +} from "@ui/ui_template_elements"; import { loadingError, showLoading } from "@ui/ui_template_loading"; import { t as L } from "@ui/ui_template_locale"; import { @@ -141,13 +147,15 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { const root = el("div", { className: "m02-tree", children: [body] }); search.root.classList.add("m02-tree__search"); // 층 단추 옆 「+」 — 맨 위에 폴더 · 항목 더하기(뿌리 줄 오른쪽 클릭을 갈음) - const add = el("button", { - className: "m02-tree__add", - text: "+", - attrs: { type: "button", title: L("M02_TreeAddTop"), "aria-label": L("M02_TreeAddTop") }, + const add = createButton({ + label: "+", + variant: "ghost", + onClick: (event) => openTreeMenu(event, topMenu()), }); + add.classList.add("m02-tree__add"); + add.title = L("M02_TreeAddTop"); + add.setAttribute("aria-label", L("M02_TreeAddTop")); add.hidden = !opt.canEdit; - add.addEventListener("click", (event) => openTreeMenu(event, topMenu())); // 「N개 고름」 · 「+」 — 층 단추 줄 오른쪽 끝(30-2 · 부르는 쪽이 붙임) const status = el("span", { className: "m02-tree__picked", attrs: { hidden: "" } }); const tools = el("span", { className: "m02-tree__tools", children: [status, add] }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts index 4c4ba094e..039deddc4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts @@ -9,7 +9,7 @@ * 누르면 `go`(부르는 쪽이 해시로 다시 엶). * ========================================================================== */ -import { el } from "@ui/ui_template_elements"; +import { createButton, el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createTreeRow } from "@ui/ui_template_tree"; import type { SessionUser } from "../A06_Login/A06_Login_Api_Fetch"; @@ -67,15 +67,17 @@ export function createLayerBar(opt: LayerBarOptions): HTMLElement { : "M02_PickerPersonal", ); const current = root === active.root && (root !== "personal" || people || !active.owner); - const b = el("button", { - className: `m02-layers__btn${current ? " is-active" : ""}`, - text: readOnly ? `${name} · ${L("M02_RootReadOnly")}` : name, - attrs: { type: "button", "data-root": root, "aria-pressed": String(current) }, - }); - b.addEventListener("click", () => { - if (!current) opt.go(root, owner); - else opt.reselect?.(); + const b = createButton({ + label: readOnly ? `${name} · ${L("M02_RootReadOnly")}` : name, + variant: current ? "filled" : "ghost", + onClick: () => { + if (!current) opt.go(root, owner); + else opt.reselect?.(); + }, }); + b.classList.add("m02-layers__btn"); + b.dataset.root = root; + b.setAttribute("aria-pressed", String(current)); return b; } diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index 280859a46..17fcd6f02 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -282,7 +282,7 @@ def test_켜진_층_단추는_목록으로_복사_끌기(): """30-4 — 켜진 층 단추를 다시 누르면 그 층 목록(구조물 = 집계표 · 일위대가 단추 벌은 39-2 에서 지움) · 30-2 · 39-6 「〇〇로 복사…」 = 쓸 수 있는 층(지금 층 뺌 · 폴더 고름) · 끌어다 옮기기는 쓸 수 있는 층만.""" roots = _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts") - assert "if (!current) opt.go(root, owner);\n else opt.reselect?.();" in roots + assert "if (!current) opt.go(root, owner);\n else opt.reselect?.();" in roots page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer))' in page # 39-6 — 쓸 수 있는 층 전부(볼 회사 주인까지 · 지금 층 뺌) · 대상 폴더 고르는 창 diff --git a/resources/tester/ui_checks/45-1_M02_단추_꼴.js b/resources/tester/ui_checks/45-1_M02_단추_꼴.js new file mode 100644 index 000000000..17bc5b9e2 --- /dev/null +++ b/resources/tester/ui_checks/45-1_M02_단추_꼴.js @@ -0,0 +1,64 @@ +/* 45-1 — M02 단추 = 공용 기본 단추(알약 꼴 아님) · 알약은 그대로 알약 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · + * 나무에 item1(구조물 조합)이 있음 + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) + * 알약 꼴 = 모서리 반지름 ≥ 높이 / 2 - 1(캡슐) · 진짜 알약 = 알약 · 칩 · 이름표 클래스(.ss-link · *__pill · *-chip · .ui-tag) + * 기대 수치: + * - pillButtons = [](M02 안 보이는 단추 중 알약 꼴 0 · 머리 공용 메뉴는 M02 밖이라 뺌) + * - layers = 층 단추가 ui-btn · 켜진 층은 filled · add = 「+」 가 ui-btn--ghost + * - smRadius = 4(작은 단추 모서리 = 8 / 2) · buttons ≥ 20 + * - pillOn: [알약] 켬 → realPills > 0 · 모두 알약 꼴(realRound = realPills) → 끔으로 되돌림(pressedAfter = "false") + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 6000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await w(150); + } + return null; + }; + const vis = (b) => b && !b.hidden && b.offsetParent !== null; + const round = (e) => { + const r = e.getBoundingClientRect(); + return parseFloat(getComputedStyle(e).borderTopLeftRadius) >= r.height / 2 - 1; + }; + const REAL = ".ss-link, [class*='__pill'], [class*='-chip'], [class*='__chip'], .ui-tag"; + const scope = () => + [...document.querySelectorAll("button")].filter( + (b) => vis(b) && !b.closest("header, .app-header") && !b.matches(REAL), + ); + const out = {}; + const node = await until(() => document.querySelector('[data-node="i|item1"]')); + if (!node) return JSON.stringify({ error: "item1 없음 — 기슭막이 폴더를 펼쳐 둘 것" }); + node.click(); + await until(() => document.querySelector(".m02-struct__card-tools .ui-btn")); + await w(1500); + const all = scope(); + out.buttons = all.length; + out.pillButtons = all + .filter((b) => round(b) && b.getBoundingClientRect().height > 0) + .map((b) => `${b.innerText.trim().slice(0, 12)} | ${b.className}`); + out.layers = [...document.querySelectorAll(".m02-layers__btn")].map( + (b) => `${b.innerText.trim()}:${b.classList.contains("ui-btn--filled") ? "filled" : b.classList.contains("ui-btn--ghost") ? "ghost" : b.className}`, + ); + out.add = document.querySelector(".m02-tree__add")?.className ?? null; + const sm = all.find((b) => b.classList.contains("ui-btn--sm")); + out.smRadius = sm ? parseFloat(getComputedStyle(sm).borderTopLeftRadius) : null; + // [알약] 켬 → 진짜 알약은 알약 꼴 그대로 → 끔으로 되돌림 + const pillBtn = [...document.querySelectorAll(".m02-struct__card-tools button")].find( + (b) => vis(b) && b.innerText.trim() === "알약", + ); + if (pillBtn) { + const before = pillBtn.getAttribute("aria-pressed"); + if (before !== "true") pillBtn.click(); + await w(600); + const real = [...document.querySelectorAll(".ss-link")].filter(vis); + out.pillOn = { realPills: real.length, realRound: real.filter(round).length }; + if (before !== "true") pillBtn.click(); + await w(300); + out.pillOn.pressedAfter = pillBtn.getAttribute("aria-pressed"); + } else out.pillOn = "알약 단추 없음"; + return JSON.stringify(out); +})() diff --git a/ui_template/ui_template_elements_styles.ts b/ui_template/ui_template_elements_styles.ts index 0eb579239..ec674b701 100644 --- a/ui_template/ui_template_elements_styles.ts +++ b/ui_template/ui_template_elements_styles.ts @@ -33,10 +33,11 @@ const BASE_CSS = ` .ui-btn:disabled { opacity: 0.5; cursor: not-allowed; } .ui-btn__icon { display: inline-flex; width: 16px; height: 16px; } -/* 작은 크기 — 다섯 모양 모두에 먹음(padding · 글자만 바꿈) */ +/* 작은 크기 — 다섯 모양 모두에 먹음(padding · 글자 · 모서리) · 낮은 키에 8px 모서리면 알약 꼴이라 반으로 */ .ui-btn--sm { padding: 2px var(--spacing-8); font-size: var(--text-caption); + border-radius: calc(var(--radius-buttons) / 2); } .ui-btn--filled {