diff --git a/resources/tester/test_552_2_button.py b/resources/tester/test_552_2_button.py index 9ef5d3955..ae7ad0b63 100644 --- a/resources/tester/test_552_2_button.py +++ b/resources/tester/test_552_2_button.py @@ -45,10 +45,12 @@ def test_여덟_단(): def test_단추_높이_식(): css = _text(UI / "ui_template_button.css") rule = re.search(r"\.ui-btn\[data-size\] \{([^}]*)\}", css).group(1) - assert "calc((var(--ui-size-h) - 2px - var(--ui-size-font)) / 2) var(--ui-size-px)" in rule - assert "height" not in rule # 높이를 박지 않음 + # 552-5 — 여백 = 1em 기준 · 줄 높이 1 (페이지 글 크기 · 줄 높이가 단을 안 바꿈) + assert "calc((var(--ui-size-h) - 2px - 1em) / 2) var(--ui-size-px)" in rule + assert "line-height: 1;" in rule + assert not re.search(r"(^|[^-])height:", rule) # 높이를 박지 않음(line-height 만) # 아이콘은 글 높이 안 — 단 높이가 아이콘 때문에 늘지 않음 - assert "margin-block: calc((var(--ui-size-font) - var(--ui-size-icon)) / 2)" in css + assert "margin-block: calc((1em - var(--ui-size-icon)) / 2)" in css # 32 단 = 옛 기본(여백 8 · 글 14) · 18 단 = 옛 sm(여백 2 · 글 12) rungs = _rungs(css) assert (32 - 2 - 14) / 2 == 8 and rungs[32]["px"] == "16px" diff --git a/resources/tester/test_552_5_part_fixes.py b/resources/tester/test_552_5_part_fixes.py new file mode 100644 index 000000000..00d3bfc59 --- /dev/null +++ b/resources/tester/test_552_5_part_fixes.py @@ -0,0 +1,77 @@ +"""552-5 부품 흠 다섯 지킴 시험 — 화면 실측은 Playwright · ORCA(부품 모음 · B07 · B03). + +① 보조 글 — 글 줄 안 span · small 은 inline(줄 안 끊김) +② 단추 실측 = 단 — 여백 1em 기준 · 줄 높이 1 · 왼쪽 패널 양식(.ui-panel-form) 옛 규칙이 덮지 않음 +③ [hidden] 단추 — 단추 display 가 숨김을 덮지 않음 +④ 파일 놓기 칸 — 칸 전체 누름 · Enter · Space = 파일 고르기 · 단추 누름은 한 번만 +⑤ 표 — 격자 꼴(grid · 세로선) · 합친 칸(colSpan · rowSpan · head) · 기본 꼴 그대로 +""" + +from __future__ import annotations + +import re +from pathlib import Path + +UI = Path(__file__).resolve().parents[2] / "ui_template" +RAW = re.compile(r"#[0-9a-fA-F]{3,8}\b|rgba?\(|--b01-|--draw-") + + +def _t(name: str) -> str: + return (UI / name).read_text(encoding="utf-8") + + +def _rule(css: str, selector: str) -> str: + return re.search(re.escape(selector) + r" \{([^}]*)\}", css).group(1) + + +def test_보조_글_줄_안_inline(): + css = _t("ui_template_text.css") + assert "display: inline;" in _rule(css, ":is(span, small).ui-text") + assert "display: flex;" in _rule(css, ".ui-text") # p · div 는 한 줄 상자 그대로 + assert not RAW.search(css) + + +def test_단추_실측_단(): + css = _t("ui_template_button.css") + rule = _rule(css, ".ui-btn[data-size]") + assert "calc((var(--ui-size-h) - 2px - 1em) / 2)" in rule and "line-height: 1;" in rule + panel = _rule(css, ".ui-panel-form .ui-btn[data-size]:not(.ui-help-btn, .ui-step-nav-btn)") + assert "padding-block: calc((var(--ui-size-h) - 2px - 1em) / 2);" in panel + assert "line-height: 1;" in panel + # 덮는 옛 규칙(글 12 · 줄 높이 normal)은 그대로 — 단 규칙이 더 세게(클래스 넷 > 셋) + overlay = (UI / "ui_template_overlay.css").read_text(encoding="utf-8") + assert ".ui-panel-form .ui-btn:not(.ui-help-btn, .ui-step-nav-btn) {" in overlay + + +def test_숨김_단추(): + css = _t("ui_template_button.css") + assert "display: none !important;" in _rule(css, ":is(.ui-btn, .ui-icon-btn)[hidden]") + + +def test_놓기_칸_전체_누름(): + ts, css = _t("ui_template_drop.ts"), _t("ui_template_drop.css") + assert 'attrs: { role: "button" }' in ts + assert 'root.addEventListener("click"' in ts and "ev.target !== input" in ts + assert 'ev.key !== "Enter" && ev.key !== " "' in ts and "ev.target !== root" in ts + assert "root.tabIndex = on ? -1 : 0" in ts + # 단추는 칸으로 올라감 — 따로 열면 두 번 열림 + pick = re.search(r"const pick = createButton\(\{([^}]*)\}\)", ts).group(1) + assert "onClick" not in pick + assert "cursor: pointer;" in _rule(css, ".ui-drop") + + +def test_표_격자_합친_칸(): + ts, css = _t("ui_template_table.ts"), _t("ui_template_table.css") + assert "grid?: boolean" in ts and '" ui-table--grid"' in ts + assert "export interface SpanCell" in ts + assert "td.colSpan = span.colSpan" in ts and "td.rowSpan = span.rowSpan" in ts + assert 'el(span?.head ? "th" : "td"' in ts + assert "if (columns.length)" in ts # 머리 줄 없는 표 + grid = _rule(css, ".ui-table--grid :is(th, td)") + assert "border: 1px solid var(--color-border);" in grid + assert "border-style: hidden;" in _rule(css, ".ui-table--grid") + # 기본 꼴 = 세로선 없음 그대로 + assert "border-left" not in _rule(css, ".ui-table td") + assert not RAW.search(css) + kit = _t("ui_template_ui_kit_parts.ts") + assert "grid: true" in kit and "rowSpan: 2" in kit diff --git a/ui_template/ui_template_button.css b/ui_template/ui_template_button.css index 6a33afaed..339af7f4b 100644 --- a/ui_template/ui_template_button.css +++ b/ui_template/ui_template_button.css @@ -69,17 +69,28 @@ --ui-size-gap: 8px; } -/* 단추 — 높이 = 글 한 줄 + 위아래 여백 + 테두리(아이콘은 글 높이 안에 맞춤) · 높이를 박지 않아 페이지 여백 덮어쓰기가 옛날처럼 먹음 */ +/* 단추 — 높이 = 글 한 줄(줄 높이 1 = 1em) + 위아래 여백 + 테두리(아이콘은 글 높이 안에 맞춤) · 높이를 박지 않아 페이지 여백 덮어쓰기가 옛날처럼 먹음 + 여백 = 1em 기준이라 페이지가 글 크기를 바꿔도 높이 = 단 */ .ui-btn[data-size] { gap: var(--ui-size-gap); - padding: calc((var(--ui-size-h) - 2px - var(--ui-size-font)) / 2) var(--ui-size-px); + padding: calc((var(--ui-size-h) - 2px - 1em) / 2) var(--ui-size-px); font-size: var(--ui-size-font); + line-height: 1; border-radius: var(--ui-size-radius); } +/* 왼쪽 패널 양식(`.ui-panel-form` · overlay.css)의 옛 단추 규칙(글 12 · 줄 높이 normal · 여백 8)이 단을 덮지 않게 — 글 크기 · 옆 여백은 패널 꼴 그대로 · 높이만 단 */ +.ui-panel-form .ui-btn[data-size]:not(.ui-help-btn, .ui-step-nav-btn) { + padding-block: calc((var(--ui-size-h) - 2px - 1em) / 2); + line-height: 1; +} +/* 숨김 — 단추 display 가 [hidden] 을 덮지 않게(공용 기본 꼴이 안 실린 곳에서도) */ +:is(.ui-btn, .ui-icon-btn)[hidden] { + display: none !important; +} .ui-btn[data-size] > .ui-btn__icon { width: var(--ui-size-icon); height: var(--ui-size-icon); - margin-block: calc((var(--ui-size-font) - var(--ui-size-icon)) / 2); + margin-block: calc((1em - var(--ui-size-icon)) / 2); } .ui-btn[data-size] > .ui-btn__icon svg { width: 100%; diff --git a/ui_template/ui_template_button.ts b/ui_template/ui_template_button.ts index 77bbe3240..713d58b01 100644 --- a/ui_template/ui_template_button.ts +++ b/ui_template/ui_template_button.ts @@ -6,6 +6,7 @@ * 아이콘 단추: 고스트(ghost · 기본) · 선(outline) · 채움(filled) · 켜짐 = aria-pressed * 크기 = `data-size="18|20|22|24|28|32|36|42"`(높이 px) — 단추 · 아이콘 단추 · 입력 · 고르기 · 분할 · 탭 같은 값 * 옛 `size: "sm"` = 18 · 크기를 안 주면 data-size 없이 옛 꼴(높이 32 · 아이콘 붙으면 34) 그대로 + * 실측 높이 = 단(꼴 · 페이지 글 크기 · 왼쪽 패널 양식과 상관없이) · `hidden` 이면 늘 숨음 * 상태 = 마우스 `:hover` · 눌림 `:active` · 막힘 `disabled` — CSS 는 `[data-state]` 로도 받아 부품 모음이 굳혀 보임 * 쓰기 = `createButton({ label: "저장", size: 24, onClick })` · `createIconButton({ icon: "settings", title: "설정", size: 24 })` * 글자 기호 단추(▲ ▼ ✕ ?)는 `glyph: "▲"` — 시험이 글자로 잡음 diff --git a/ui_template/ui_template_drop.css b/ui_template/ui_template_drop.css index ce496ac9b..ea43b96a5 100644 --- a/ui_template/ui_template_drop.css +++ b/ui_template/ui_template_drop.css @@ -11,6 +11,7 @@ background: var(--color-surface-raised); color: var(--color-text); text-align: center; + cursor: pointer; transition: border-color var(--transition-fast), background-color var(--transition-fast); @@ -42,6 +43,7 @@ } .ui-drop[aria-disabled="true"] { opacity: 0.5; + cursor: not-allowed; } .ui-drop:is(.is-error, [data-state="error"]) { border-color: var(--color-danger); diff --git a/ui_template/ui_template_drop.ts b/ui_template/ui_template_drop.ts index 46319100e..e154a7e63 100644 --- a/ui_template/ui_template_drop.ts +++ b/ui_template/ui_template_drop.ts @@ -4,6 +4,7 @@ * 꼴 = 점선 칸(올림 아이콘 · 한 줄 · 고르기 단추 · 받는 꼴 안내) · 진행(`setProgress` → 진행 막대) · 오류(`setError` → 빨간 글) * 크기 = 칸 최소 48 · 단추 28 * 상태 = 기본 · 마우스(파일을 끌어 옴 = .is-over) · 막힘(`disabled`) · 오류(.is-error) + * 누름 = 칸 어디든 누름 · 칸에 초점 두고 Enter · Space = 파일 고르기(옛 B03 놓기 칸 동작) · [파일 고르기] 단추는 같은 일 * 쓰는 법 = `const drop = createDrop({ accept: ".las,.laz", onFiles: (files) => upload(files) }); host.append(drop.root)` * 대신하는 옛 변종 = B03 `b03-file__dropzone` · CAD `body.cad-file-dragging::after` · 숨은 file + 단추(B01 · B08 · M02 · cad_host · help_edit) * ========================================================================== */ @@ -43,7 +44,6 @@ export function createDrop(opts: DropOptions): DropHandle { label: "파일 고르기", variant: "ghost", size: 28, - onClick: () => input.click(), }); const bar = createProgressBar({ thick: 6 }); bar.root.hidden = true; @@ -51,6 +51,7 @@ export function createDrop(opts: DropOptions): DropHandle { error.hidden = true; const root = el("div", { className: "ui-drop", + attrs: { role: "button" }, children: [ el("span", { className: "ui-drop__icon", children: [icon("upload", 24)] }), el("p", { className: "ui-drop__text", text: opts.text ?? "파일을 여기에 끌어 놓거나" }), @@ -69,6 +70,15 @@ export function createDrop(opts: DropOptions): DropHandle { give(input.files); input.value = ""; // 같은 파일을 다시 골라도 change }); + // 단추 누름도 칸으로 올라와 한 번만 엶 · 숨은 input 의 click 이 올라온 것은 거름 + root.addEventListener("click", (ev) => { + if (ev.target !== input && !pick.disabled) input.click(); + }); + root.addEventListener("keydown", (ev) => { + if (ev.target !== root || (ev.key !== "Enter" && ev.key !== " ")) return; + ev.preventDefault(); + if (!pick.disabled) input.click(); + }); const over = (on: boolean): boolean => root.classList.toggle("is-over", on && !pick.disabled); root.addEventListener("dragover", (ev) => { ev.preventDefault(); @@ -85,6 +95,7 @@ export function createDrop(opts: DropOptions): DropHandle { const setDisabled = (on: boolean): void => { pick.disabled = on; + root.tabIndex = on ? -1 : 0; root.setAttribute("aria-disabled", String(on)); }; setDisabled(!!opts.disabled); diff --git a/ui_template/ui_template_table.css b/ui_template/ui_template_table.css index 26914c069..bdcd34984 100644 --- a/ui_template/ui_template_table.css +++ b/ui_template/ui_template_table.css @@ -55,6 +55,23 @@ padding: 0 8px; } +/* 격자표 — 세로선 · 칸마다 테두리(바깥 선은 감쌈 테두리 한 줄) · 가운데 맞춤 · 몸 안 이름 칸(th) = 머리색 */ +.ui-table--grid { + border-style: hidden; + font-variant-numeric: tabular-nums; +} +.ui-table--grid :is(th, td) { + border: 1px solid var(--color-border); + text-align: center; + white-space: nowrap; +} +.ui-table--grid tbody th { + font-weight: 500; + font-size: inherit; + color: var(--color-text); + background: color-mix(in srgb, var(--color-accent) 6%, var(--color-surface)); +} + /* 줄 상태 — 고름 · 바뀜 · 지움 */ .ui-table tbody tr:is(.is-active, [aria-selected="true"]) { background: var(--color-accent-soft); diff --git a/ui_template/ui_template_table.ts b/ui_template/ui_template_table.ts index e7bd8ec9b..24029dca2 100644 --- a/ui_template/ui_template_table.ts +++ b/ui_template/ui_template_table.ts @@ -3,6 +3,8 @@ * 부품 「표」(552-3 · 목록 18) — 머리 줄 + 줄들 · 세로선 없음 · 카드 테두리 · 넘치면 가로 스크롤. * 꼴 = 일반표(줄 40 · 기본) · 데이터표(`density: "data"` · 줄 28 · 글 12) · ⋯ 칸(columns 의 label "" — 줄 끝 `createRowMenu`) * · 한글 배지 칸(칸에 배지 요소) · 오른쪽 맞춤 칸(`align: "end"` · 수) · 빈 줄(`emptyRow`) · 펼침 줄(tr.is-sub) + * · 격자표(`grid: true` · 세로선 · 칸마다 테두리 · 가운데 맞춤 · 몸 안 이름 칸 = th 머리색) — 수량 산출표처럼 칸을 합치는 표 + * · 합친 칸 = 칸 자리에 `{ content, colSpan, rowSpan, head }`(head = th) · columns 를 비우면 머리 줄 없이 줄로만 그림 * 크기 = 줄 40 / 28 · 머리 36 / 28 * 상태 = 줄 마우스(옅은 바탕) · 고름(tr.is-active / aria-selected) · 바뀜(tr.is-changed) · 지움(tr.is-removed · 줄 그음) * 쓰는 법 = `createTable([{ label: "이름" }, { label: "", width: "44px" }], [["임도 1", createRowMenu([…])]])` @@ -14,7 +16,15 @@ import { el } from "./ui_template_elements_base"; import "./ui_template_table.css"; -export type Cell = string | Node; +/** 합친 칸 · 몸 안 이름 칸(head = th) */ +export interface SpanCell { + content: string | Node; + colSpan?: number; + rowSpan?: number; + head?: boolean; +} + +export type Cell = string | Node | SpanCell; export interface Column { label: string; @@ -26,6 +36,8 @@ export interface Column { export interface TableOptions { /** 데이터표(줄 28 · 글 12) */ density?: "data"; + /** 격자표 — 세로선 · 칸 테두리 · 가운데 맞춤 */ + grid?: boolean; } /** 표 껍데기 — 머리 + 빈 tbody (줄은 호출 쪽이 채움 · 펼침 줄 같은 것을 끼우려고 떼어 둠) */ @@ -34,30 +46,39 @@ export function tableShell( opts: TableOptions = {}, ): { root: HTMLElement; tbody: HTMLElement } { const table = el("table", { - className: `ui-table${opts.density === "data" ? " ui-table--data" : ""}`, + className: `ui-table${opts.density === "data" ? " ui-table--data" : ""}${opts.grid ? " ui-table--grid" : ""}`, }); - const hr = el("tr"); - for (const col of columns) { - const th = el("th", { className: col.align ? "is-end" : "", text: col.label }); - if (col.width) th.style.width = col.width; - hr.append(th); + if (columns.length) { + const hr = el("tr"); + for (const col of columns) { + const th = el("th", { className: col.align ? "is-end" : "", text: col.label }); + if (col.width) th.style.width = col.width; + hr.append(th); + } + table.append(el("thead", { children: [hr] })); } const tbody = el("tbody"); - table.append(el("thead", { children: [hr] }), tbody); + table.append(tbody); return { root: el("div", { className: "ui-table-wrap", children: [table] }), tbody }; } -/** 한 줄 — 칸 수는 columns 와 같게 · label "" 인 칸 = ⋯ 칸 */ +/** 한 줄 — 칸 수는 columns 와 같게 · label "" 인 칸 = ⋯ 칸 · 합친 칸은 colSpan 만큼 칸 자리를 건넘 */ export function tableRow(columns: Column[], cells: Cell[]): HTMLTableRowElement { const tr = el("tr"); - cells.forEach((cell, i) => { + let i = 0; + // ponytail: 위 줄 rowSpan 이 차지한 칸 자리는 안 셈 — 합친 표(격자)는 columns 를 비워 맞춤 · ⋯ 칸 꼴이 필요해지면 자리 표를 둠 + for (const cell of cells) { + const span = typeof cell === "string" || cell instanceof Node ? null : cell; const col = columns[i]; - const td = el("td", { + const td = el(span?.head ? "th" : "td", { className: col?.align ? "is-end" : col?.label === "" ? "ui-table__menucell" : "", }); - td.append(cell); + td.append(span ? span.content : (cell as string | Node)); + if (span?.colSpan) td.colSpan = span.colSpan; + if (span?.rowSpan) td.rowSpan = span.rowSpan; tr.append(td); - }); + i += span?.colSpan ?? 1; + } return tr; } diff --git a/ui_template/ui_template_text.css b/ui_template/ui_template_text.css index 38b4df6de..101740680 100644 --- a/ui_template/ui_template_text.css +++ b/ui_template/ui_template_text.css @@ -27,3 +27,11 @@ flex: none; font-weight: var(--font-weight-bold); } +/* 글 줄 안(span · small) = 줄을 끊지 않는 inline 꼴 · 기호 · 아이콘 사이 = 오른쪽 여백 */ +:is(span, small).ui-text { + display: inline; +} +:is(span, small).ui-text > :is(.ui-text__mark, svg) { + margin-right: 4px; + vertical-align: middle; +} diff --git a/ui_template/ui_template_text.ts b/ui_template/ui_template_text.ts index c1a70f8b7..769a0d670 100644 --- a/ui_template/ui_template_text.ts +++ b/ui_template/ui_template_text.ts @@ -4,6 +4,7 @@ * * 꼴 = 흐린 글(muted) · 안내(hint · ⓘ 아이콘) · 상태 글(error · warning · success — 글자 기호 ✗ ! ✓ 같이) · 칸 오류(`.ui-field__error`) * 크기 = 글 12(기본) · 14 — data-text 로 고름 + * 자리 = p · div 는 한 줄 상자(flex) · 글 줄 안 span · small 에 클래스만 붙이면 inline(줄 안 끊김) * 상태 = 오류 = error 꼴(색만으로 안 가름 · 기호 같이) * 쓰기 = `createText("저장 안 한 값이 있음", "warning")` · `createText("단위 m", "muted", 14)` * 대신 = 파일마다 `__muted`(약 30 벌) · `__hint` · `__status` · `__note` · `__warn` · `.b01-sys-error` · `b03-file__error` …(목록 4장) diff --git a/ui_template/ui_template_ui_kit_parts.ts b/ui_template/ui_template_ui_kit_parts.ts index 421eb522c..3d931b6c7 100644 --- a/ui_template/ui_template_ui_kit_parts.ts +++ b/ui_template/ui_template_ui_kit_parts.ts @@ -26,6 +26,7 @@ import { ICON_NAMES, icon } from "./ui_template_icons"; import { createInputField, createSelectField, createSlider } from "./ui_template_input"; import { createSegment } from "./ui_template_segment"; import { createTabs } from "./ui_template_tabs"; +import { el } from "./ui_template_elements_base"; import { createText } from "./ui_template_text"; import { createToggle } from "./ui_template_toggle"; // 552-3 묶음 부품 @@ -34,7 +35,7 @@ import { createRowMenu, type RowMenuItem } from "./ui_template_context_menu"; import { createDrop } from "./ui_template_drop"; import { buildModal, confirmPanel } from "./ui_template_modal"; import { settingCard, settingRow, valueText } from "./ui_template_setting_row"; -import { createTable, type Column } from "./ui_template_table"; +import { createTable, type Cell, type Column } from "./ui_template_table"; import { createCard } from "./ui_template_card"; import { createPanelCard } from "./ui_template_collapsible"; import { createBarChart, createDonut, createLineChart } from "./ui_template_elements_chart"; @@ -122,6 +123,13 @@ const dataRows = [ ["1", "1,080"], ["2", "640"], ]; +/** 552-5 격자표 견본 — 수량 산출표 꼴(합친 칸 · 몸 안 이름 칸) */ +const gridRows = (): Cell[][] => [ + [{ content: "토공 수량", colSpan: 4, head: true }], + [{ content: "깍기", rowSpan: 2, head: true }, { content: "토사", head: true }, "12.40", "m³"], + [{ content: "암", head: true }, "3.10", "m³"], + [{ content: "쌓기", colSpan: 2, head: true }, "8.05", "m³"], +]; const openMenu = (s: KitState, items = KIT_MENU): HTMLElement => { const menu = createRowMenu(items); menu.classList.add("is-open"); @@ -243,8 +251,8 @@ export const KIT_PARTS: KitPart[] = [ demo: (s) => (s === "default" ? row(...ICON_NAMES.map((n) => { const i = icon(n); i.title = n; i.dataset.icon = n; return i; })) : null) }, { key: "slider", name: "슬라이더", group: "basic", kinds: "값 하나 · 이름 · 값 글 · 3열 줄(24)", file: "ui_template_input.ts", demo: (s) => { if (s === "active" || s === "error") return null; const sl = createSlider({ label: "투명도", min: 0, max: 100, value: 60, format: (v) => `${v}%` }); markState(sl.input, s); return sl.root; } }, - { key: "text", name: "보조 글 · 안내 줄", group: "basic", kinds: "흐린 글 · 안내 · 상태(오류 · 경고 · 성공 · 기호 같이) · 12 · 14", file: "ui_template_text.ts", - demo: (s) => (s === "default" ? row(createText("흐린 글 — 마지막 저장 3 분 전"), createText("안내 — 노선을 먼저 고름", "hint"), createText("저장 안 한 값이 있음", "warning"), createText("저장됨", "success"), createText("본문 곁 14", "muted", 14)) : s === "error" ? createText("값이 범위를 벗어남", "error") : null) }, + { key: "text", name: "보조 글 · 안내 줄", group: "basic", kinds: "흐린 글 · 안내 · 상태(오류 · 경고 · 성공 · 기호 같이) · 12 · 14 · 글 줄 안 span · small = inline", file: "ui_template_text.ts", + demo: (s) => (s === "default" ? row(createText("흐린 글 — 마지막 저장 3 분 전"), createText("안내 — 노선을 먼저 고름", "hint"), createText("저장 안 한 값이 있음", "warning"), createText("저장됨", "success"), createText("본문 곁 14", "muted", 14), el("p", { children: ["단가 ", el("span", { className: "ui-text ui-text--muted", text: "(원/m³)" }), " 를 글 줄 안에 ", el("small", { className: "ui-text ui-text--muted", text: "· 끊김 없음" })] })) : s === "error" ? createText("값이 범위를 벗어남", "error") : null) }, // ── 552-3 묶음 ───────────────────────────────────────────── { key: "card", name: "카드", group: "compound", kinds: "선 카드 · 머리 카드 · 고르는 카드 · 여백 12 · 16", file: "ui_template_card.ts", demo: (s) => { const pick = (): HTMLElement => markState(createCard({ title: "새 노선", body: ["노선을 그려 시작"], pad: 12, onSelect: noop }), s); @@ -252,11 +260,11 @@ export const KIT_PARTS: KitPart[] = [ { key: "collapsible", name: "접이 카드", group: "compound", kinds: "패널 카드 · 구역 · 캐럿 ▾ ▸ · ⚙ 곁값", file: "ui_template_collapsible.ts", demo: (s) => { if (s !== "default" && s !== "hover") return null; const open = createPanelCard("구조물 배치", false, noop); open.append(row(createInputField({ placeholder: "측점" }).root)); if (s === "hover") { markState(open.querySelector(".ui-collapsible__title")!, s); return open; } return col(open, createPanelCard("접힌 카드")); } }, - { key: "table", name: "표", group: "compound", kinds: "일반표(줄 40) · 데이터표(줄 28) · ⋯ 칸 · 한글 배지 · 빈 줄 · 줄 고름 · 바뀜 · 지움", file: "ui_template_table.ts", + { key: "table", name: "표", group: "compound", kinds: "일반표(줄 40) · 데이터표(줄 28) · 격자표(세로선 · 합친 칸) · ⋯ 칸 · 한글 배지 · 빈 줄 · 줄 고름 · 바뀜 · 지움", file: "ui_template_table.ts", demo: (s) => { if (s === "disabled" || s === "error") return null; const t = s === "default" ? createTable(KIT_COLS, kitRows()) : createTable(KIT_DATA, dataRows, { density: "data" }); const first = t.querySelector("tbody tr")!; if (s === "hover") markState(first, s); if (s === "active") first.classList.add("is-active"); if (s !== "default") return t; const data = createTable(KIT_DATA, dataRows, { density: "data" }); const rows = data.querySelectorAll("tbody tr"); rows[0].classList.add("is-changed"); rows[1].classList.add("is-removed"); - return col(t, data, createTable(KIT_DATA, [], { empty: "노선 없음" })); } }, + return col(t, data, createTable(KIT_DATA, [], { empty: "노선 없음" }), createTable([], gridRows(), { density: "data", grid: true })); } }, { key: "row-menu", name: "⋯ · 오른쪽 메뉴", group: "compound", kinds: "줄 끝 ⋯(28) · 펼친 판 · 위험 항목(빨강 + 확인창) · 막힌 항목", file: "ui_template_context_menu.ts", demo: (s) => { if (s === "error") return null; if (s === "default") return createRowMenu(KIT_MENU); if (s === "hover") { const m = createRowMenu(KIT_MENU); markState(m.querySelector(".ui-row-menu__btn")!, s); return m; } return openMenu(s, s === "disabled" ? [{ label: "내보내기", onClick: noop, disabled: true }, ...KIT_MENU] : KIT_MENU); } },