feat(ui_template): 지도 도구줄 부품 · 아이콘 여덟 · 부품 모음 견본 (PLAN 568-1 · 요구 395-1)
- ui_template_map_tools.ts · .css — 아이콘 단추 한 벌(차례 · 나눔선 · 켬끔 aria-pressed · [?] 안내 판) - 아이콘 더함 — ripple · tallymarks · percentage · magnet · satellite · droplet · borderAll · angle(Tabler MIT) - 부품 모음 틀 묶음에 「지도 도구줄」 견본 · 시험 test_568_1 · test_552_4 목록에 map-tools Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ PARTS = {
|
||||
"float-panel": ("ui_template_float_panel", "createFloatPanel", "ui-float-panel", (32,)),
|
||||
"drawer": ("ui_template_side_panels", "createSidePanels", "ui-side-panels", ()),
|
||||
"toolbar": ("ui_template_toolbar", "createToolbar", "ui-toolbar", (24, 28, 32)),
|
||||
"map-tools": ("ui_template_map_tools", "createMapTools", "ui-map-tools", ()),
|
||||
"steps": ("ui_template_workflow_layout", "createStepBar", "ui-workflow-layout__step", (28, 40)),
|
||||
"panel-head": ("ui_template_panel_head", "createPanelHead", "ui-panel-head", (32,)),
|
||||
"list-row": ("ui_template_list_row", "createListRow", "ui-list-row", (28, 32, 36)),
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""568-1 지도 도구줄 부품 지킴 시험 — 화면 확인은 부품 모음(`#/b01-account?view=ui-kit`) · ORCA.
|
||||
|
||||
· 단추 목록의 아이콘이 모두 아이콘 묶음에 있음 · 새 아이콘 여덟(도구 넷 · 겹 넷)
|
||||
· 켬끔 단추 = aria-pressed · [?] = data-act="hint-toggle" + aria-expanded · 안내 판 기본 접힘
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_568_1_map_tools.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
UI = ROOT / "ui_template"
|
||||
TS = (UI / "ui_template_map_tools.ts").read_text(encoding="utf-8")
|
||||
ICONS = (UI / "ui_template_icons.ts").read_text(encoding="utf-8")
|
||||
CSS = (UI / "ui_template_map_tools.css").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _icon_names() -> set[str]:
|
||||
body = ICONS[ICONS.index("const PATHS") : ICONS.index("} as const")]
|
||||
return set(re.findall(r"^ (\w+):", body, re.M))
|
||||
|
||||
|
||||
def test_새_아이콘_여덟():
|
||||
names = _icon_names()
|
||||
for n in (
|
||||
"ripple",
|
||||
"tallymarks",
|
||||
"percentage",
|
||||
"magnet",
|
||||
"satellite",
|
||||
"droplet",
|
||||
"borderAll",
|
||||
"angle",
|
||||
):
|
||||
assert n in names, n
|
||||
|
||||
|
||||
def test_단추_목록_아이콘_차례():
|
||||
table = TS[TS.index("export const MAP_TOOLS") : TS.index("satisfies")]
|
||||
rows = re.findall(
|
||||
r'^ "?([\w-]+)"?: \{ icon: "(\w+)", title: "[^"]+"(, toggle: true)? \}', table, re.M
|
||||
)
|
||||
acts = [a for a, _, _ in rows]
|
||||
assert acts == [
|
||||
"undo",
|
||||
"redo",
|
||||
"view3d",
|
||||
"rotate-ccw",
|
||||
"rotate-cw",
|
||||
"contour-minor",
|
||||
"stations",
|
||||
"slopes",
|
||||
"footprint",
|
||||
"measure",
|
||||
"station-snap",
|
||||
]
|
||||
names = _icon_names()
|
||||
assert all(icon in names for _, icon, _ in rows)
|
||||
toggles = {a for a, _, t in rows if t}
|
||||
assert toggles == {
|
||||
"contour-minor",
|
||||
"stations",
|
||||
"slopes",
|
||||
"footprint",
|
||||
"measure",
|
||||
"station-snap",
|
||||
}
|
||||
|
||||
|
||||
def test_켬끔_안내_판():
|
||||
assert "pressed: tool.toggle ? (opts.pressed ?? false) : undefined" in TS
|
||||
assert "button.dataset.act = act;" in TS
|
||||
assert 'toggle.dataset.act = "hint-toggle";' in TS
|
||||
assert 'toggle.setAttribute("aria-expanded", String(open));' in TS
|
||||
assert "setOpen(opts.hint.open ?? false);" in TS # 기본 접힘
|
||||
hint = CSS[CSS.index(".ui-map-tools__hint {") :]
|
||||
assert "display: none;" in hint[: hint.index("}")]
|
||||
assert "display: grid;" in CSS[CSS.index(".ui-map-tools__hint.is-open {") :]
|
||||
assert "pointer-events: none;" in CSS # 빈 자리 · 글판은 지도 조작 통과
|
||||
@@ -123,6 +123,21 @@ const PATHS = {
|
||||
mail: "M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10|M3 7l9 6l9 -6",
|
||||
inbox:
|
||||
"M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12|M4 13h3l3 3h4l3 -3h3",
|
||||
// 568-1 — 지도 도구(1m 등고선 · 측점 · 기울기 · 측점 스냅)
|
||||
ripple: "M3 7c3 -2 6 -2 9 0s6 2 9 0|M3 17c3 -2 6 -2 9 0s6 2 9 0|M3 12c3 -2 6 -2 9 0s6 2 9 0",
|
||||
tallymarks: "M6 5l0 14|M10 5l0 14|M14 5l0 14|M18 5l0 14|M3 17l18 -10",
|
||||
percentage: "M16 17a1 1 0 1 0 2 0a1 1 0 0 0 -2 0|M6 7a1 1 0 1 0 2 0a1 1 0 0 0 -2 0|M6 18l12 -12",
|
||||
magnet:
|
||||
"M4 13v-8a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v8a2 2 0 0 0 6 0v-8a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v8a8 8 0 0 1 -16 0|M4 8l5 0|M15 8l4 0",
|
||||
// 568-1 — 지도 겹(위성 · 세류선 · 지적도 · 급경사)
|
||||
satellite:
|
||||
"M3.707 6.293l2.586 -2.586a1 1 0 0 1 1.414 0l5.586 5.586a1 1 0 0 1 0 1.414l-2.586 2.586a1 1 0 0 1 -1.414 0l-5.586 -5.586a1 1 0 0 1 0 -1.414z|M6 10l-3 3l3 3l3 -3|M10 6l3 -3l3 3l-3 3|M12 12l1.5 1.5|M14.5 17a2.5 2.5 0 0 0 2.5 -2.5|M15 21a6 6 0 0 0 6 -6",
|
||||
droplet:
|
||||
"M7.502 19.423c2.602 2.105 6.395 2.105 8.996 0c2.602 -2.105 3.262 -5.708 1.566 -8.546l-4.89 -7.26c-.42 -.625 -1.287 -.803 -1.936 -.397a1.376 1.376 0 0 0 -.41 .397l-4.893 7.26c-1.695 2.838 -1.035 6.441 1.567 8.546z",
|
||||
borderAll:
|
||||
"M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z|M4 12l16 0|M12 4l0 16",
|
||||
angle:
|
||||
"M21 19h-18l9 -15|M20.615 15.171h.015|M19.515 11.771h.015|M17.715 8.671h.015|M15.415 5.971h.015",
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof PATHS;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/* 지도 화면 제어 도구줄(568-1) — 도구줄 + 아래 [?] 안내 판 · 자리는 페이지가 줌 · 빈 자리는 지도 조작 통과 */
|
||||
.ui-map-tools {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-8);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ui-map-tools > .ui-toolbar {
|
||||
max-width: 100%;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 안내 판 — 기본 접힘 · 2열 · 글판은 클릭 통과 */
|
||||
.ui-map-tools__hint {
|
||||
display: none;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 2px var(--spacing-16);
|
||||
max-width: 100%;
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 65%, transparent);
|
||||
border-radius: var(--radius-cards);
|
||||
background: color-mix(in srgb, var(--color-surface-raised) 72%, transparent);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-map-tools__hint.is-open {
|
||||
display: grid;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/* =============================================================================
|
||||
* ui_template_map_tools.ts
|
||||
* 지도 화면 제어 도구줄(568-1) — 평면 지도 위 아이콘 단추 한 벌 · B05 평면 · D03 평면이 같이 씀.
|
||||
* 꼴 = 유리 도구줄(`createToolbar` glass · 32) 한 줄 · 묶음 사이 나눔선 · 단추는 모두 아이콘(마우스 올리면 이름)
|
||||
* [?] 안내 판 = 도구줄 아래 2열 글판(기본 접힘 · 클릭은 지도로 통과)
|
||||
* 크기 = 단 32 하나(지도 위 도구)
|
||||
* 상태 = 켬끔 단추는 `aria-pressed`(눌림 꼴) · 막힘 `disabled` · [?] 는 `aria-expanded`
|
||||
* 쓰는 법 = createMapTools({ label: "평면 도구", groups: [["undo", "redo"], ["rotate-ccw", "rotate-cw", "measure"]],
|
||||
* hint: { lines: ["휠 = 확대"] } }) → { root, bar, hint, button(act) }
|
||||
* 페이지 문구가 다르면 `mapToolButton("measure", { title })` 를 묶음에 그대로 끼움 · 고르기 · 칩도 요소로 끼움
|
||||
* 누름 동작은 페이지가 붙임(`onClick` 또는 `[data-act]` 로 찾아 붙임) · 자리는 페이지 CSS(root)
|
||||
* 대신하는 옛 변종 = B05 `b05-route__view-controls` 글자 유리 단추 줄 · `b05-routeedit__hint` 안내 판 · D03 `tool()` 지역 함수
|
||||
* ========================================================================== */
|
||||
|
||||
import { createIconButton } from "./ui_template_button";
|
||||
import type { IconName } from "./ui_template_icons";
|
||||
import { createToolbar } from "./ui_template_toolbar";
|
||||
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 },
|
||||
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 },
|
||||
} satisfies Record<string, { icon: IconName; title: string; toggle?: boolean }>;
|
||||
|
||||
export type MapToolAct = keyof typeof MAP_TOOLS;
|
||||
|
||||
export interface MapToolButtonOptions {
|
||||
/** 마우스 글 — 없으면 목록 이름 */
|
||||
title?: string;
|
||||
/** 켬끔 단추 처음 값(기본 false) */
|
||||
pressed?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
}
|
||||
|
||||
/** 아이콘 단추 하나(`data-act` = act) */
|
||||
export function mapToolButton(act: MapToolAct, opts: MapToolButtonOptions = {}): HTMLButtonElement {
|
||||
const tool: { icon: IconName; title: string; toggle?: boolean } = MAP_TOOLS[act];
|
||||
const button = createIconButton({
|
||||
icon: tool.icon,
|
||||
title: opts.title ?? tool.title,
|
||||
size: 32,
|
||||
pressed: tool.toggle ? (opts.pressed ?? false) : undefined,
|
||||
disabled: opts.disabled,
|
||||
onClick: opts.onClick,
|
||||
});
|
||||
button.dataset.act = act;
|
||||
return button;
|
||||
}
|
||||
|
||||
export interface MapToolsOptions {
|
||||
/** 묶음들 — 사이에 나눔선 · act 이름 또는 페이지가 만든 요소 */
|
||||
groups: (MapToolAct | HTMLElement)[][];
|
||||
/** 보조기기 이름 */
|
||||
label: string;
|
||||
/** [?] 안내 판(맨 앞 묶음) — 줄마다 한 칸 · 2열 */
|
||||
hint?: { lines: string[]; open?: boolean; onToggle?: (open: boolean) => void };
|
||||
}
|
||||
|
||||
export interface MapTools {
|
||||
root: HTMLElement;
|
||||
bar: HTMLElement;
|
||||
hint: HTMLElement | null;
|
||||
button(act: MapToolAct): HTMLButtonElement | null;
|
||||
}
|
||||
|
||||
export function createMapTools(opts: MapToolsOptions): MapTools {
|
||||
const root = document.createElement("div");
|
||||
root.className = "ui-map-tools";
|
||||
const groups = opts.groups.map((group) =>
|
||||
group.map((item) => (typeof item === "string" ? mapToolButton(item) : item)),
|
||||
);
|
||||
let hint: HTMLElement | null = null;
|
||||
if (opts.hint) {
|
||||
const { lines, onToggle } = opts.hint;
|
||||
const toggle = createIconButton({ icon: "help", title: "조작 도움말 펼치기 · 접기", size: 32 });
|
||||
toggle.dataset.act = "hint-toggle";
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "ui-map-tools__hint";
|
||||
for (const line of lines)
|
||||
panel.append(Object.assign(document.createElement("span"), { textContent: line }));
|
||||
const setOpen = (open: boolean): void => {
|
||||
panel.classList.toggle("is-open", open);
|
||||
toggle.setAttribute("aria-expanded", String(open));
|
||||
};
|
||||
setOpen(opts.hint.open ?? false);
|
||||
toggle.addEventListener("click", () => {
|
||||
const open = !panel.classList.contains("is-open");
|
||||
setOpen(open);
|
||||
onToggle?.(open);
|
||||
});
|
||||
groups.unshift([toggle]);
|
||||
hint = panel;
|
||||
}
|
||||
const bar = createToolbar({
|
||||
glass: true,
|
||||
size: 32,
|
||||
label: opts.label,
|
||||
items: groups.flatMap((group, i) => (i ? ["divider" as const, ...group] : group)),
|
||||
});
|
||||
root.append(bar);
|
||||
if (hint) root.append(hint);
|
||||
return {
|
||||
root,
|
||||
bar,
|
||||
hint,
|
||||
button: (act) => root.querySelector<HTMLButtonElement>(`[data-act="${act}"]`),
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,7 @@ 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 { createWorkflowPanelHandle } from "./ui_template_overlay";
|
||||
import { createPanelHead } from "./ui_template_panel_head";
|
||||
import { createPublicFrame } from "./ui_template_public_layout";
|
||||
@@ -218,6 +219,27 @@ function toolbar(): HTMLElement {
|
||||
return box;
|
||||
}
|
||||
|
||||
function mapTools(): HTMLElement {
|
||||
const tools = createMapTools({
|
||||
label: "평면 도구",
|
||||
hint: { lines: ["휠 = 확대", "가운데 끌기 = 이동", "Ctrl+휠 = 돌리기", "Esc = 그리기 끝"] },
|
||||
groups: [
|
||||
["undo", "redo"],
|
||||
["view3d", "rotate-ccw", "rotate-cw"],
|
||||
[
|
||||
"contour-minor",
|
||||
"stations",
|
||||
mapToolButton("slopes", { pressed: true }),
|
||||
"footprint",
|
||||
"measure",
|
||||
],
|
||||
["station-snap"],
|
||||
],
|
||||
});
|
||||
tools.button("contour-minor")!.disabled = true;
|
||||
return tools.root;
|
||||
}
|
||||
|
||||
/** 열쇠 → 견본(상태별 · null = 그 상태 없음) */
|
||||
export const FRAME_DEMOS: Record<string, (state: KitState) => HTMLElement | null> = {
|
||||
"work-frame": (s) => (s === "default" ? workFrame() : null),
|
||||
@@ -226,6 +248,7 @@ export const FRAME_DEMOS: Record<string, (state: KitState) => HTMLElement | null
|
||||
"float-panel": (s) => (s === "default" ? floatPanel() : null),
|
||||
drawer: (s) => (s === "default" ? drawer() : null),
|
||||
toolbar: (s) => (s === "default" ? toolbar() : null),
|
||||
"map-tools": (s) => (s === "default" ? mapTools() : null),
|
||||
steps,
|
||||
"panel-head": (s) => (s === "default" ? panelHead() : null),
|
||||
"list-row": listRow,
|
||||
|
||||
@@ -363,6 +363,8 @@ 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",
|
||||
demo: FRAME_DEMOS["map-tools"] },
|
||||
{ key: "steps", name: "단계 줄", group: "frame", kinds: "가로 막대 · compact(28) · 세로 목록(40 · ☰ 서랍)", file: "ui_template_workflow_layout.ts",
|
||||
demo: FRAME_DEMOS.steps },
|
||||
{ key: "panel-head", name: "패널 머리", group: "frame", kinds: "제목 · 부제 · 오른쪽 단추 · 밑줄", file: "ui_template_panel_head.ts",
|
||||
|
||||
Reference in New Issue
Block a user