feat(부품): 634-1 여럿 고르기 메뉴 · 한 행 두 영역 · 막힘 글 · 새 아이콘(요구 546)
- createPickMenu multi — menuitemcheckbox · 눌러도 안 닫힘 · values() · 줄 icon · disabled · title · setItem · 단추 ▾ + 켠 수 배지(0 = 없음) · 하나라도 켜면 눌림 · 밀기 · 창 크기 바뀌면 닫힘 · offLabel 없으면 끔 줄 · 눌림 없음 - createMapToolRow — 그룹1 왼쪽(행 반까지) · 그룹2 오른쪽 · 영역마다 min-width 0 + 자기 가로 밀기 · [?] 판은 행 아래 - mapToolTip — 평소 제목만 · 막힘 때만 「제목 — 까닭」 · MAP_TOOLS 글 = 표 호버제목 - 아이콘 — 묶음 넷(streams · signpost · parcelHouse · ban) · 겹친 셋 대신(contours · cutFill · lineament) · 스냅 간격 spacing - 배지 count · accent 덧 · 부품 모음 견본 「고르기 메뉴(하나 · 여럿)」 · 지도 도구줄 한 행 견본 - 시험 568_1 고침 — 예정노선 둘 줄이 한 줄 꼴이 되어 목록에 잡힘 · 새 지킴 634_1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,8 @@ def test_단추_목록_아이콘_차례():
|
||||
"footprint",
|
||||
"measure",
|
||||
"station-snap",
|
||||
"expected-draw",
|
||||
"expected-original",
|
||||
]
|
||||
names = _icon_names()
|
||||
assert all(icon in names for _, icon, _ in rows)
|
||||
@@ -66,6 +68,8 @@ def test_단추_목록_아이콘_차례():
|
||||
"footprint",
|
||||
"measure",
|
||||
"station-snap",
|
||||
"expected-draw",
|
||||
"expected-original",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""634-1 부품(요구 546) — 여럿 고르기 메뉴(안 닫힘 · 켠 수 배지 · 줄 아이콘 · 막힘) · 한 행 두 영역 · 견본 · 아이콘."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
UI = Path(__file__).resolve().parents[2] / "ui_template"
|
||||
MENU = (UI / "ui_template_context_menu.ts").read_text(encoding="utf-8")
|
||||
MENU_CSS = (UI / "ui_template_context_menu.css").read_text(encoding="utf-8")
|
||||
TOOLS = (UI / "ui_template_map_tools.ts").read_text(encoding="utf-8")
|
||||
TOOLS_CSS = (UI / "ui_template_map_tools.css").read_text(encoding="utf-8")
|
||||
ICONS = (UI / "ui_template_icons.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_여럿_고르기_안_닫힘_배지():
|
||||
pick = MENU[MENU.index("export function createPickMenu(") :]
|
||||
assert '"menuitemcheckbox"' in pick and "values," in pick and "setItem(value, patch)" in pick
|
||||
click = pick[pick.index('b.addEventListener("click"') :]
|
||||
click = click[: click.index("pop.append(b);")]
|
||||
# 여럿 = 켬끔 뒤 바로 돌아감(setOpen(false) 앞)
|
||||
assert click.index("if (multi)") < click.index("setOpen(false)")
|
||||
assert (
|
||||
'createBadge("0", "accent", { kind: "count"' in pick
|
||||
and "badge.hidden = count === 0;" in pick
|
||||
)
|
||||
assert 'root.setAttribute("aria-pressed", String(count > 0));' in pick
|
||||
assert 'window.addEventListener("resize", onShift);' in pick # 창 크기 = 닫힘
|
||||
assert 'document.addEventListener("scroll", onShift, true);' in pick # 밀기 = 닫힘
|
||||
assert ".ui-pick-menu--multi" in MENU_CSS and ".ui-row-menu__item:disabled" in MENU_CSS
|
||||
|
||||
|
||||
def test_한_행_두_영역():
|
||||
assert "export function createMapToolRow(left: HTMLElement, right: HTMLElement)" in TOOLS
|
||||
assert ":scope > .ui-map-tools__hint" in TOOLS # [?] 판은 행 아래
|
||||
first = TOOLS_CSS[TOOLS_CSS.index(".ui-map-row__line > .ui-map-tools:first-child {") :]
|
||||
first = first[: first.index("}")]
|
||||
assert "flex-shrink: 0;" in first and "max-width: 50%;" in first # 모자라면 그룹2 먼저
|
||||
area = TOOLS_CSS[TOOLS_CSS.index(".ui-map-row__line > .ui-map-tools {") :]
|
||||
assert "min-width: 0;" in area[: area.index("}")]
|
||||
|
||||
|
||||
def test_마우스_글_막힘만_까닭():
|
||||
tip = TOOLS[TOOLS.index("export function mapToolTip(") :]
|
||||
assert "why ? `${el.dataset.tip} — ${why}` : el.dataset.tip" in tip[: tip.index("\n}")]
|
||||
assert 'title: "도움말"' in TOOLS
|
||||
|
||||
|
||||
def test_아이콘_겹침_없음():
|
||||
table = TOOLS[TOOLS.index("export const MAP_TOOLS") : TOOLS.index("satisfies")]
|
||||
icons = re.findall(r'icon: "(\w+)"', table)
|
||||
assert len(icons) == len(set(icons))
|
||||
for name in (
|
||||
"contours",
|
||||
"cutFill",
|
||||
"lineament",
|
||||
"spacing",
|
||||
"streams",
|
||||
"signpost",
|
||||
"parcelHouse",
|
||||
"ban",
|
||||
):
|
||||
assert re.search(rf"^ {name}:", ICONS, re.M), name
|
||||
|
||||
|
||||
def test_견본():
|
||||
parts = (UI / "ui_template_ui_kit_parts.ts").read_text(encoding="utf-8")
|
||||
frames = (UI / "ui_template_ui_kit_frames.ts").read_text(encoding="utf-8")
|
||||
assert 'name: "고르기 메뉴(하나 · 여럿)"' in parts and "demo: pickDemo" in parts
|
||||
assert "createMapToolRow(layers.root, tools.root)" in frames
|
||||
@@ -84,3 +84,9 @@
|
||||
.ui-badge[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 수 · 진행색(634-1 · 지도 겹 묶음 켠 수) */
|
||||
.ui-badge--count.ui-badge--accent {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
@@ -128,6 +128,32 @@
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* 줄 아이콘(634-1) — ✓ · 아이콘 · 글 한 줄 */
|
||||
.ui-pick-menu__pop .ui-row-menu__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
.ui-pick-menu__pop .ui-row-menu__item:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* 여럿 고르기 단추(634-1) — 아이콘 + 켠 수 배지 + ▾ */
|
||||
.ui-icon-btn.ui-pick-menu--multi {
|
||||
width: auto;
|
||||
min-width: var(--ui-size-h);
|
||||
grid-auto-flow: column;
|
||||
gap: 2px;
|
||||
padding-inline: var(--spacing-4);
|
||||
}
|
||||
.ui-pick-menu__caret {
|
||||
font-size: var(--text-caption);
|
||||
line-height: 1;
|
||||
}
|
||||
.ui-pick-menu--multi > .ui-badge[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 숨김 — display 가 [hidden] 을 덮지 않게(공용 기본 꼴이 안 실린 곳 · 페이지 꾸밈 위에서도) */
|
||||
:is(.ui-row-menu, .ui-row-menu__btn, .ui-row-menu__item)[hidden] {
|
||||
display: none !important;
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createIconButton } from "./ui_template_button";
|
||||
import type { IconName } from "./ui_template_icons";
|
||||
import { createBadge } from "./ui_template_badge";
|
||||
import { icon, type IconName } from "./ui_template_icons";
|
||||
import "./ui_template_context_menu.css";
|
||||
import { showConfirmDialog } from "./ui_template_modal";
|
||||
|
||||
@@ -299,58 +300,125 @@ export function createRowMenu(items: RowMenuItem[]): HTMLElement {
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
* 부품 「아이콘 고르기 메뉴」(PLAN 626-1 · 요구 530 · 지도 도구줄 단추 하나 + 아래 판)
|
||||
* 꼴 = 아이콘 단추(32 · 고른 것 있으면 눌림) → 아래 판 = 항목 하나만 고름(✓) + 맨 끝 「끔」
|
||||
* 자리 = 판은 화면 맨 위(body · fixed) — 도구줄 가로 밀기 칸에 잘리지 않게 · 단추 바로 아래
|
||||
* 상태 = 고름 `aria-checked` · 단추 `aria-pressed` · 막힘(단추 disabled)
|
||||
* 부품 「고르기 메뉴」(PLAN 626-1 · 634-1 · 요구 530 · 546 · 지도 도구줄 단추 하나 + 아래 판)
|
||||
* 꼴 = 하나(기본) — 아이콘 단추(32) → 아래 판 = 항목 하나만 고름(✓) · 누르면 닫힘 · `offLabel` 이면 맨 끝 「끔」 줄 + 고르면 눌림
|
||||
* 여럿(`multi`) — 아이콘 + 켠 수 배지(0 = 없음) + ▾ · 판 = 켬끔 줄(✓) · 눌러도 안 닫힘 · 하나라도 켜면 눌림
|
||||
* 줄 = `icon` · `title`(마우스 글) · `disabled`(흐림) — 바꿈은 `setItem(value, { checked, disabled, title })`
|
||||
* 자리 = 판은 화면 맨 위(body · fixed) — 도구줄 가로 밀기 칸에 잘리지 않게 · 단추 바로 아래 · 밀기 · 창 크기 바뀌면 닫힘
|
||||
* 상태 = 고름 `aria-checked` · 단추 `aria-pressed` · 막힘(단추 · 줄 disabled)
|
||||
* 쓰는 법 = `createPickMenu({ icon: "compass", title, items: [{ value, label }], offLabel: "끔", onPick })`
|
||||
* → { root, set(value | null), value() } · `onPick` 은 누를 때만(set 은 안 부름)
|
||||
* → { root, set(value | null), value(), values(), setItem() } · `onPick` 은 누를 때만(set · setItem 은 안 부름)
|
||||
* 여럿 = `createPickMenu({ multi: true, icon, title, items: [{ value, label, icon }], onPick: (value) => 켬끔 })`
|
||||
* 한 번에 하나 · 밖 누름 · Esc 로 닫힘 = 위 장부(enterMenu · leaveMenu)
|
||||
* ========================================================================== */
|
||||
|
||||
export interface PickMenuItem {
|
||||
value: string;
|
||||
label: string;
|
||||
title?: string;
|
||||
icon?: IconName;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface PickMenu {
|
||||
root: HTMLButtonElement;
|
||||
/** 하나 — 고름 바꿈 · 여럿 — 그 하나만 켬(null = 모두 끔). */
|
||||
set: (value: string | null) => void;
|
||||
value: () => string | null;
|
||||
/** 켠(고른) 줄 값들. */
|
||||
values: () => string[];
|
||||
/** 줄 하나 고침 — 켬 · 막힘 · 마우스 글. */
|
||||
setItem: (
|
||||
value: string,
|
||||
patch: { checked?: boolean; disabled?: boolean; title?: string },
|
||||
) => void;
|
||||
}
|
||||
|
||||
export function createPickMenu(opts: {
|
||||
icon: IconName;
|
||||
title: string;
|
||||
items: Array<{ value: string; label: string; title?: string }>;
|
||||
offLabel: string;
|
||||
items: PickMenuItem[];
|
||||
offLabel?: string;
|
||||
multi?: boolean;
|
||||
onPick: (value: string | null) => void;
|
||||
}): PickMenu {
|
||||
let current: string | null = null;
|
||||
const root = createIconButton({ icon: opts.icon, title: opts.title, size: 32, pressed: false });
|
||||
const multi = !!opts.multi;
|
||||
const root = createIconButton({
|
||||
icon: opts.icon,
|
||||
title: opts.title,
|
||||
size: 32,
|
||||
pressed: multi || opts.offLabel ? false : undefined,
|
||||
});
|
||||
root.setAttribute("aria-haspopup", "menu");
|
||||
root.setAttribute("aria-expanded", "false");
|
||||
const badge = createBadge("0", "accent", { kind: "count", size: 18 });
|
||||
if (multi) {
|
||||
root.classList.add("ui-pick-menu--multi");
|
||||
badge.hidden = true;
|
||||
const caret = Object.assign(document.createElement("span"), { textContent: "▾" });
|
||||
caret.className = "ui-pick-menu__caret";
|
||||
root.append(badge, caret);
|
||||
}
|
||||
const pop = document.createElement("div");
|
||||
pop.className = "ui-row-menu__pop ui-pick-menu__pop";
|
||||
pop.setAttribute("role", "menu");
|
||||
pop.hidden = true;
|
||||
const rows = [...opts.items, { value: "", label: opts.offLabel, title: undefined }].map(
|
||||
(item) => {
|
||||
const b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "ui-row-menu__item";
|
||||
b.textContent = item.label;
|
||||
if (item.title) b.title = item.title;
|
||||
b.dataset.value = item.value;
|
||||
b.setAttribute("role", "menuitemradio");
|
||||
b.addEventListener("click", () => {
|
||||
setOpen(false);
|
||||
set(item.value || null);
|
||||
opts.onPick(current);
|
||||
});
|
||||
pop.append(b);
|
||||
return b;
|
||||
},
|
||||
);
|
||||
const all: PickMenuItem[] = [
|
||||
...opts.items,
|
||||
...(!multi && opts.offLabel ? [{ value: "", label: opts.offLabel }] : []),
|
||||
];
|
||||
const rows = all.map((item) => {
|
||||
const b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "ui-row-menu__item";
|
||||
if (item.icon) b.append(icon(item.icon, 18));
|
||||
b.append(Object.assign(document.createElement("span"), { textContent: item.label }));
|
||||
if (item.title) b.title = item.title;
|
||||
b.disabled = !!item.disabled;
|
||||
b.dataset.value = item.value;
|
||||
b.setAttribute("role", multi ? "menuitemcheckbox" : "menuitemradio");
|
||||
b.addEventListener("click", () => {
|
||||
if (multi) {
|
||||
mark(b, b.getAttribute("aria-checked") !== "true");
|
||||
return opts.onPick(item.value);
|
||||
}
|
||||
setOpen(false);
|
||||
set(item.value || null);
|
||||
opts.onPick(current);
|
||||
});
|
||||
pop.append(b);
|
||||
return b;
|
||||
});
|
||||
const rowOf = (value: string): HTMLButtonElement | undefined =>
|
||||
rows.find((row) => row.dataset.value === value);
|
||||
const values = (): string[] =>
|
||||
rows
|
||||
.filter((row) => row.getAttribute("aria-checked") === "true")
|
||||
.map((row) => row.dataset.value!);
|
||||
let current: string | null = null;
|
||||
/** 단추 눌림 · 배지 — 줄 켬에 맞춤. */
|
||||
function paint(): void {
|
||||
if (!multi) {
|
||||
if (opts.offLabel) root.setAttribute("aria-pressed", String(current !== null));
|
||||
return;
|
||||
}
|
||||
const count = values().length;
|
||||
root.setAttribute("aria-pressed", String(count > 0));
|
||||
badge.textContent = String(count);
|
||||
badge.hidden = count === 0;
|
||||
}
|
||||
function mark(row: HTMLButtonElement, checked: boolean): void {
|
||||
row.setAttribute("aria-checked", String(checked));
|
||||
paint();
|
||||
}
|
||||
const entry: MenuEntry = {
|
||||
close: () => setOpen(false),
|
||||
contains: (target) => target instanceof Node && (root.contains(target) || pop.contains(target)),
|
||||
};
|
||||
/** 밀기(도구줄 · 쪽) · 창 크기 = 판이 단추에서 떨어짐 → 닫음(판 안 밀기는 그대로). */
|
||||
const onShift = (event: Event): void => {
|
||||
if (!(event.target instanceof Node && pop.contains(event.target))) setOpen(false);
|
||||
};
|
||||
function setOpen(open: boolean): void {
|
||||
if (open) {
|
||||
document.body.append(pop);
|
||||
@@ -361,6 +429,11 @@ export function createPickMenu(opts: {
|
||||
left: `${at.left}px`,
|
||||
right: "auto",
|
||||
});
|
||||
window.addEventListener("resize", onShift);
|
||||
document.addEventListener("scroll", onShift, true);
|
||||
} else {
|
||||
window.removeEventListener("resize", onShift);
|
||||
document.removeEventListener("scroll", onShift, true);
|
||||
}
|
||||
pop.hidden = !open;
|
||||
root.setAttribute("aria-expanded", String(open));
|
||||
@@ -370,14 +443,26 @@ export function createPickMenu(opts: {
|
||||
}
|
||||
function set(value: string | null): void {
|
||||
current = value;
|
||||
root.setAttribute("aria-pressed", String(value !== null));
|
||||
for (const row of rows)
|
||||
row.setAttribute("aria-checked", String((row.dataset.value || null) === value));
|
||||
paint();
|
||||
}
|
||||
set(null);
|
||||
root.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
setOpen(pop.hidden === true);
|
||||
});
|
||||
return { root, set, value: () => current };
|
||||
return {
|
||||
root,
|
||||
set,
|
||||
value: () => current,
|
||||
values,
|
||||
setItem(value, patch) {
|
||||
const row = rowOf(value);
|
||||
if (!row) return;
|
||||
if (patch.disabled !== undefined) row.disabled = patch.disabled;
|
||||
if (patch.title !== undefined) row.title = patch.title;
|
||||
if (patch.checked !== undefined) mark(row, patch.checked);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,6 +158,17 @@ const PATHS = {
|
||||
"M16 5l3 3l-2 1l4 4l-3 1l4 4h-9|M15 21l0 -3|M8 13l-2 -2|M8 12l2 -2|M8 21v-13|M5.824 16a3 3 0 0 1 -2.743 -3.69a3 3 0 0 1 .304 -4.833a3 3 0 0 1 4.615 -3.707a3 3 0 0 1 4.614 3.707a3 3 0 0 1 .305 4.833a3 3 0 0 1 -2.919 3.695h-4z",
|
||||
plant:
|
||||
"M7 15h10v4a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2z|M12 9a6 6 0 0 0 -6 -6h-3v2a6 6 0 0 0 6 6h3|M12 11a6 6 0 0 1 6 -6h3v1a6 6 0 0 1 -6 6h-3|M12 15l0 -6",
|
||||
// 634-1 — 지도 도구 · 겹(겹친 셋 대신 · 1m 등고선 · 절성토선 · 지질구조선 · 스냅 간격)
|
||||
contours:
|
||||
"M10 12a2 1.6 0 1 0 4 0a2 1.6 0 1 0 -4 0|M6.5 12a5.5 4.2 0 1 0 11 0a5.5 4.2 0 1 0 -11 0|M3 12a9 7.5 0 1 0 18 0a9 7.5 0 1 0 -18 0",
|
||||
cutFill: "M10 4l-1.5 16|M14 4l1.5 16|M6 4l-3.5 16|M18 4l3.5 16",
|
||||
lineament: "M3 18l3 -4|M8 11.5l2 -3|M12 7l2 3|M16 12.5l2 -3|M20 6.5l1 -1.5",
|
||||
spacing: "M4 5v14|M20 5v14|M8 12h8|M8 12l2 -2|M8 12l2 2|M16 12l-2 -2|M16 12l-2 2",
|
||||
// 634-1 — 지도 겹 묶음(수계망도 · 도로 · 토지·건물 · 제한·재해)
|
||||
streams: "M12 21v-7|M12 14l-5 -7|M12 14l6 -9|M8.8 9.5l-4.8 -1.5|M15.6 8.6l4.4 -0.6",
|
||||
signpost: "M12 21v-11|M12 5v-2|M5 5h11l3 2.5l-3 2.5h-11z|M9 21h6",
|
||||
parcelHouse: "M3 21h18|M5 21v-9l7 -6l7 6v9|M10 21v-5h4v5",
|
||||
ban: "M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0|M5.7 5.7l12.6 12.6",
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof PATHS;
|
||||
|
||||
@@ -62,8 +62,38 @@
|
||||
display: grid;
|
||||
}
|
||||
|
||||
/* 한 행 두 영역(634-1) — 왼쪽 겹 줄 · 오른쪽 도구줄 · 영역마다 자기 가로 밀기(min-width 0) ·
|
||||
왼쪽은 행 반까지 제 폭 · 모자라면 오른쪽이 먼저 밀림 · [?] 판은 행 아래 · 빈 자리는 지도 조작 통과 */
|
||||
.ui-map-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ui-map-row__line {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-8);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-map-row__line > .ui-map-tools {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-map-row__line > .ui-map-tools:first-child {
|
||||
flex-shrink: 0;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
/* 숨김 — display 가 [hidden] 을 덮지 않게 */
|
||||
.ui-map-tools[hidden],
|
||||
.ui-map-row[hidden],
|
||||
.ui-map-tools__hint[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* hint: { lines: ["휠 = 확대"] } }) → { root, bar, hint, button(act) }
|
||||
* 페이지 문구가 다르면 `mapToolButton("measure", { title })` 를 묶음에 그대로 끼움 · 고르기 · 칩도 요소로 끼움
|
||||
* 누름 동작은 페이지가 붙임(`onClick` 또는 `[data-act]` 로 찾아 붙임) · 자리는 페이지 CSS(root)
|
||||
* 한 행 두 영역 = `createMapToolRow(겹 줄 root, 도구줄 root)`(634-1) · 막힘 글 = `mapToolTip(단추, 까닭)`
|
||||
* 대신하는 옛 변종 = B05 `b05-route__view-controls` 글자 유리 단추 줄 · `b05-routeedit__hint` 안내 판 · D03 `tool()` 지역 함수
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -20,29 +21,19 @@ import "./ui_template_map_tools.css";
|
||||
|
||||
/** 단추 목록 — 차례 = 도구줄 차례 · toggle = 켬끔 단추(aria-pressed) */
|
||||
export const MAP_TOOLS = {
|
||||
undo: { icon: "undo", title: "되돌리기 (Ctrl+Z)" },
|
||||
redo: { icon: "redo", title: "다시하기 (Ctrl+Y)" },
|
||||
view3d: { icon: "cube", title: "3D 보기" },
|
||||
"rotate-ccw": { icon: "rotateCcw", title: "반시계로 돌리기" },
|
||||
"rotate-cw": { icon: "rotateCw", title: "시계로 돌리기" },
|
||||
"contour-minor": { icon: "ripple", title: "1m 등고선", toggle: true },
|
||||
undo: { icon: "undo", title: "되돌리기" },
|
||||
redo: { icon: "redo", title: "다시하기" },
|
||||
view3d: { icon: "cube", title: "3D" },
|
||||
"rotate-ccw": { icon: "rotateCcw", title: "좌 회전" },
|
||||
"rotate-cw": { icon: "rotateCw", title: "우 회전" },
|
||||
"contour-minor": { icon: "contours", title: "등고선 1m", toggle: true },
|
||||
stations: { icon: "tallymarks", title: "측점", toggle: true },
|
||||
slopes: { icon: "percentage", title: "기울기", toggle: true },
|
||||
footprint: { icon: "road", title: "절/성토선", toggle: true },
|
||||
measure: { icon: "ruler", title: "거리 재기", toggle: true },
|
||||
"station-snap": { icon: "magnet", title: "측점 스냅", toggle: true },
|
||||
"expected-draw": {
|
||||
icon: "pencil",
|
||||
title:
|
||||
"예정노선 변경 — 켜면 예정노선이 맨 위 · 예정노선 페이지와 같은 긋기 · 고르기 · 고치기 → 평면선을 예정노선 따라 통째 다시 추적 (켠 동안 계획선 · 구조물은 안 집힘)",
|
||||
toggle: true,
|
||||
},
|
||||
"expected-original": {
|
||||
icon: "lineDashed",
|
||||
title:
|
||||
"예정노선(원본) — 파일로 올린 예정노선을 옅은 점선으로 보이기 · 숨기기 (고친 예정노선과 다를 때만 보임)",
|
||||
toggle: true,
|
||||
},
|
||||
slopes: { icon: "percentage", title: "길이·기울기", toggle: true },
|
||||
footprint: { icon: "cutFill", title: "절성토선", toggle: true },
|
||||
measure: { icon: "ruler", title: "길이측정", toggle: true },
|
||||
"station-snap": { icon: "magnet", title: "스냅", toggle: true },
|
||||
"expected-draw": { icon: "pencil", title: "예정노선 변경", toggle: true },
|
||||
"expected-original": { icon: "lineDashed", title: "예정노선 원본", toggle: true },
|
||||
} satisfies Record<string, { icon: IconName; title: string; toggle?: boolean }>;
|
||||
|
||||
export type MapToolAct = keyof typeof MAP_TOOLS;
|
||||
@@ -68,9 +59,18 @@ export function mapToolButton(act: MapToolAct, opts: MapToolButtonOptions = {}):
|
||||
onClick: opts.onClick,
|
||||
});
|
||||
button.dataset.act = act;
|
||||
button.dataset.tip = opts.title ?? tool.title;
|
||||
return button;
|
||||
}
|
||||
|
||||
/** 마우스 글(요구 546) — 평소 제목만 · 막힘이면 「제목 — 까닭」 · 제목 = `data-tip`(없으면 지금 글). */
|
||||
export function mapToolTip(el: HTMLElement, why?: string | null): void {
|
||||
el.dataset.tip ??= el.title;
|
||||
const text = why ? `${el.dataset.tip} — ${why}` : el.dataset.tip;
|
||||
el.title = text;
|
||||
el.setAttribute("aria-label", text);
|
||||
}
|
||||
|
||||
export interface MapToolsOptions {
|
||||
/** 묶음들 — 사이에 나눔선 · act 이름 또는 페이지가 만든 요소 */
|
||||
groups: (MapToolAct | HTMLElement)[][];
|
||||
@@ -119,7 +119,7 @@ export function createMapTools(opts: MapToolsOptions): MapTools {
|
||||
let hint: HTMLElement | null = null;
|
||||
if (opts.hint) {
|
||||
const { lines, onToggle } = opts.hint;
|
||||
const toggle = createIconButton({ icon: "help", title: "조작 도움말 펼치기 · 접기", size: 32 });
|
||||
const toggle = createIconButton({ icon: "help", title: "도움말", size: 32 });
|
||||
toggle.dataset.act = "hint-toggle";
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "ui-map-tools__hint";
|
||||
@@ -154,3 +154,18 @@ export function createMapTools(opts: MapToolsOptions): MapTools {
|
||||
button: (act) => root.querySelector<HTMLButtonElement>(`[data-act="${act}"]`),
|
||||
};
|
||||
}
|
||||
|
||||
/** 한 행 두 영역(634-1 · 요구 546) — 왼쪽(지도 겹) · 오른쪽(도구) · 영역마다 자기 가로 밀기 · 모자라면 오른쪽 먼저 밀림 ·
|
||||
* [?] 안내 판은 행 아래로 옮김 · 자리(위 · 왼쪽 · 오른쪽)는 페이지 CSS(root). */
|
||||
export function createMapToolRow(left: HTMLElement, right: HTMLElement): HTMLElement {
|
||||
const root = document.createElement("div");
|
||||
root.className = "ui-map-row";
|
||||
const line = document.createElement("div");
|
||||
line.className = "ui-map-row__line";
|
||||
line.append(left, right);
|
||||
const hints = [left, right].flatMap((area) =>
|
||||
Array.from(area.querySelectorAll<HTMLElement>(":scope > .ui-map-tools__hint")),
|
||||
);
|
||||
root.append(line, ...hints);
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import { createFloatPanel } from "./ui_template_float_panel";
|
||||
import { createFormGrid, createPageFrame } from "./ui_template_general_layout";
|
||||
import { createInputField } from "./ui_template_input";
|
||||
import { createListRow } from "./ui_template_list_row";
|
||||
import { createMapTools, mapToolButton } from "./ui_template_map_tools";
|
||||
import { createPickMenu } from "./ui_template_context_menu";
|
||||
import { createMapToolRow, createMapTools, mapToolButton } from "./ui_template_map_tools";
|
||||
import { createWorkflowPanelHandle } from "./ui_template_overlay";
|
||||
import { createPanelHead } from "./ui_template_panel_head";
|
||||
import { createPublicFrame } from "./ui_template_public_layout";
|
||||
@@ -237,7 +238,30 @@ function mapTools(): HTMLElement {
|
||||
],
|
||||
});
|
||||
tools.button("contour-minor")!.disabled = true;
|
||||
return tools.root;
|
||||
// 634-1 한 행 두 영역 — 왼쪽 겹 줄(단독 + 묶음 메뉴) · 오른쪽 도구줄
|
||||
const water = createPickMenu({
|
||||
multi: true,
|
||||
icon: "streams",
|
||||
title: "수계망도",
|
||||
onPick: () => {},
|
||||
items: [
|
||||
{ value: "stream", label: "세류", icon: "droplet" },
|
||||
{ value: "water", label: "하천", icon: "ripple" },
|
||||
],
|
||||
});
|
||||
water.setItem("stream", { checked: true });
|
||||
const layers = createMapTools({
|
||||
label: "지도 겹",
|
||||
groups: [
|
||||
[
|
||||
createIconButton({ icon: "satellite", title: "위성사진", size: 32, pressed: true }),
|
||||
water.root,
|
||||
],
|
||||
],
|
||||
});
|
||||
const row = createMapToolRow(layers.root, tools.root);
|
||||
row.style.width = "100%";
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 열쇠 → 견본(상태별 · null = 그 상태 없음) */
|
||||
|
||||
@@ -31,7 +31,7 @@ import { createText } from "./ui_template_text";
|
||||
import { createToggle } from "./ui_template_toggle";
|
||||
// 552-3 묶음 부품
|
||||
import { createBanner } from "./ui_template_banner";
|
||||
import { createRowMenu, type RowMenuItem } from "./ui_template_context_menu";
|
||||
import { createPickMenu, 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";
|
||||
@@ -134,6 +134,18 @@ const gridRows = (): Cell[][] => [
|
||||
[{ content: "암", head: true }, "3.10", "m³"],
|
||||
[{ content: "쌓기", colSpan: 2, head: true }, "8.05", "m³"],
|
||||
];
|
||||
/** 634-1 견본 — 고르기 메뉴 하나(추천 노선 꼴) · 여럿(지도 겹 묶음 · 켠 수 배지 · 막힌 줄) */
|
||||
const pickDemo = (s: KitState): HTMLElement | null => {
|
||||
if (s !== "default" && s !== "disabled") return null;
|
||||
const one = createPickMenu({ icon: "compass", title: "추천노선", items: [{ value: "a", label: "노선 가" }, { value: "b", label: "노선 나" }], offLabel: "끔", onPick: noop });
|
||||
const many = createPickMenu({ multi: true, icon: "streams", title: "수계망도", onPick: noop,
|
||||
items: [{ value: "stream", label: "세류", icon: "droplet" }, { value: "water", label: "하천", icon: "ripple" }, { value: "bands", label: "계류100m", icon: "borderOuter", disabled: true, title: "계류100m — 지형 받기 뒤" }] });
|
||||
one.set("a");
|
||||
many.setItem("stream", { checked: true });
|
||||
many.setItem("water", { checked: true });
|
||||
if (s === "disabled") [one.root, many.root].forEach((b) => (b.disabled = true));
|
||||
return row(one.root, many.root);
|
||||
};
|
||||
const openMenu = (s: KitState, items = KIT_MENU): HTMLElement => {
|
||||
const menu = createRowMenu(items);
|
||||
menu.classList.add("is-open");
|
||||
@@ -322,6 +334,7 @@ export const KIT_PARTS: KitPart[] = [
|
||||
{ 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<HTMLElement>(".ui-row-menu__btn")!, s); return m; }
|
||||
return openMenu(s, s === "disabled" ? [{ label: "내보내기", onClick: noop, disabled: true }, ...KIT_MENU] : KIT_MENU); } },
|
||||
{ key: "pick-menu", name: "고르기 메뉴(하나 · 여럿)", group: "compound", kinds: "아이콘 단추 32 → 아래 판 · 하나(✓ · 누르면 닫힘 · 끔 줄) · 여럿(켬끔 ✓ · 안 닫힘 · 켠 수 배지 · ▾) · 줄 아이콘 · 막힌 줄 흐림", file: "ui_template_context_menu.ts", demo: pickDemo },
|
||||
{ key: "empty", name: "빈 상태", group: "compound", kinds: "선 아이콘 · 한 줄 · 주 단추 · 사용법 · 작은 꼴", file: "ui_template_empty.ts",
|
||||
demo: (s) => (s === "default" ? col(createEmpty({ icon: "folder", text: "아직 프로젝트가 없음", hint: "새 프로젝트를 만들어 시작", action: createButton({ label: "새 프로젝트", onClick: noop }), onGuide: noop }), createEmpty({ text: "고른 측점 없음", small: true })) : null) },
|
||||
{ key: "modal", name: "모달 · 확인창", group: "compound", kinds: "보통(720) · 넓음 · 폭 단 24rem · 40rem · 960 · 넓은 중간 1100 × 680 · 긴 제목 = 한 줄 말줄임 · 확인 · 위험 확인 · 글 묻기(prompt 대신)", file: "ui_template_modal.ts",
|
||||
@@ -370,7 +383,7 @@ export const KIT_PARTS: KitPart[] = [
|
||||
demo: FRAME_DEMOS["drawer"] },
|
||||
{ key: "toolbar", name: "도구줄", group: "frame", kinds: "단추 묶음 · 나눔선 · 밀기 · 유리 · 24 · 28 · 32", file: "ui_template_toolbar.ts",
|
||||
demo: FRAME_DEMOS["toolbar"] },
|
||||
{ key: "map-tools", name: "지도 도구줄", group: "frame", kinds: "아이콘 단추 한 벌 · 묶음 나눔선 · 켬끔(aria-pressed) · [?] 안내 판 · 페이지가 쓸 단추만", file: "ui_template_map_tools.ts",
|
||||
{ key: "map-tools", name: "지도 도구줄", group: "frame", kinds: "아이콘 단추 한 벌 · 묶음 나눔선 · 켬끔(aria-pressed) · [?] 안내 판 · 페이지가 쓸 단추만 · 한 행 두 영역(겹 · 도구 · 영역마다 밀기)", file: "ui_template_map_tools.ts",
|
||||
demo: FRAME_DEMOS["map-tools"] },
|
||||
{ key: "steps", name: "단계 줄", group: "frame", kinds: "가로 막대 · compact(28) · 세로 목록(40 · ☰ 서랍)", file: "ui_template_workflow_layout.ts",
|
||||
demo: FRAME_DEMOS.steps },
|
||||
|
||||
Reference in New Issue
Block a user