- 분할 고르기 부품에 풂 꼴(clearable) · 부품 모음 견본에 더함 - 방식 칸(map_draw_kind)에 켬끔 꼴(onActive · setActive) — 켬 = 긋기 시작 · 고른 방식 다시 누름 = 긋기 끝 · 긋는 중 다른 방식 = 방식만 바뀜 - D03 · B05 그리기 칸 [긋기] 단추 걷음 · 긋기가 Enter · Esc · 붙어 끝나도 칸 켬 맞춤 · 노드 판은 그대로 - 시험 757_1 새로 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
420 lines
36 KiB
TypeScript
420 lines
36 KiB
TypeScript
/* =============================================================================
|
||
* ui_template_ui_kit_parts.ts
|
||
* 부품 모음 화면(552-1)의 부품 목록 — 한 줄 = 표준 부품 하나 · 화면은 `ui_template_ui_kit.ts`.
|
||
*
|
||
* 부품 규칙(552-2 ~ 552-4 가 따름):
|
||
* 자리 = `ui_template/` 한 곳 · 부품 하나 = `ui_template_<이름>.ts` (+ 같은 이름 `.css`) · 700 줄 넘으면 `_<갈래>` 로 나눔
|
||
* 이름 = 만들기 `create<부품>(opts)` → 요소 또는 { root, … } 손잡이 · 클래스 `ui-<부품>` · 속 `ui-<부품>__<조각>` · 꼴 `ui-<부품>--<꼴>`
|
||
* 크기 = `data-size="18|20|22|24|28|32|36|42"`(높이 px · 여덟 단) — 단추 · 아이콘 단추 · 입력 · 고르기 · 분할 고르기 · 탭 같은 값
|
||
* 옛 `size: "sm"` = 18 · `compact` = 32 · 지금 높이는 가까운 단(같은 거리면 표 · 도구 = 작은 단 · 주 동작 · 양식 = 큰 단)
|
||
* 상태 = 마우스 `:hover` · 눌림 `:active` · 고름 `.is-active` / `aria-selected` · 막힘 `disabled` / `aria-disabled` · 오류 `.is-error` / `aria-invalid`
|
||
* → CSS 는 `:is(:hover, [data-state="hover"])` · `:is(:active, [data-state="active"])` 로 써서 이 화면이 상태를 굳혀 보임
|
||
* 색 = 공통 값(`--color-*` · `--radius-*` · `--shadow-*`)만 · 날 색 · `--b01-*` · 그림 값(`--draw-*`) 금지
|
||
* 글꼴 = `--font-ui` 상속 · 글자 크기 = `--text-*`
|
||
* 아이콘 = `ui_template_icons.ts` 의 `icon(name, size)` 만 · 그림 이모지 금지 · 글자 기호(→ ▾ ▲▼ ✓)는 시험이 잡아 그대로
|
||
* 옛 이름 = 지금 공용 클래스(`ui-btn--filled` · `ui-collapsible__title` · `ui-sidebar-actions` …)는 부품이 그대로 냄(시험 · 가이드 촬영)
|
||
* 머리 = 파일 머리에 무엇 · 꼴 · 크기 · 상태 · 쓰는 법 한 줄씩
|
||
* 부품을 만들면 아래 자기 줄의 `file` 을 맞추고 `demo` 를 채움 — 빈 칸 = 「빈 자리」로 보임.
|
||
* 몫 = 552-2 기본 · 552-3 묶음 · 552-4 틀 — 세 묶음이 떨어져 있어 서로 줄을 안 건드림.
|
||
* ========================================================================== */
|
||
|
||
import { createBadge } from "./ui_template_badge";
|
||
import { createButton, createIconButton, type UiSize } from "./ui_template_button";
|
||
import { createCheckbox } from "./ui_template_checkbox";
|
||
import { tooltipBox } from "./ui_template_hover_info";
|
||
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 묶음 부품
|
||
import { createBanner } from "./ui_template_banner";
|
||
import { createPickMenu, createRowMenu, type RowMenuItem } from "./ui_template_context_menu";
|
||
import { createDrop } from "./ui_template_drop";
|
||
import { createDivider } from "./ui_template_divider";
|
||
import { createPager } from "./ui_template_pager";
|
||
import { buildModal, confirmPanel } from "./ui_template_modal";
|
||
import { settingCard, settingRow, valueText } from "./ui_template_setting_row";
|
||
import { createTable, type Cell, type Column } from "./ui_template_table";
|
||
import { createCard } from "./ui_template_card";
|
||
import { createCollapsibleHead, createPanelCard } from "./ui_template_collapsible";
|
||
import { createCalcFormula, createCalcFormulaRow, createCalcRow } from "./ui_template_calc_row";
|
||
import { createDataTable } from "./ui_template_data_table";
|
||
import { createHopyoGrid } from "./ui_template_hopyo_grid";
|
||
import { createBarChart, createDonut, createLineChart } from "./ui_template_elements_chart";
|
||
import { createEmpty } from "./ui_template_empty";
|
||
import { createLegend } from "./ui_template_flow_legend";
|
||
import { createKv } from "./ui_template_kv";
|
||
import { createProgressBar, createProgressCircle } from "./ui_template_progress";
|
||
import { createRangeBar } from "./ui_template_range_bar";
|
||
import { createStat, statGrid } from "./ui_template_stat";
|
||
import { toastNode } from "./ui_template_toast";
|
||
import { FRAME_DEMOS } from "./ui_template_ui_kit_frames";
|
||
|
||
/** 상태 줄 다섯 칸 */
|
||
export const KIT_STATES = ["default", "hover", "active", "disabled", "error"] as const;
|
||
export type KitState = (typeof KIT_STATES)[number];
|
||
export const KIT_STATE_LABEL: Record<KitState, string> = {
|
||
default: "기본",
|
||
hover: "마우스",
|
||
active: "눌림",
|
||
disabled: "막힘",
|
||
error: "오류",
|
||
};
|
||
|
||
/** 단추 · 입력 높이 여덟 단(요구 377 · 378) */
|
||
export const KIT_SIZES = [18, 20, 22, 24, 28, 32, 36, 42] as const;
|
||
|
||
export type KitGroup = "basic" | "compound" | "frame";
|
||
export const KIT_GROUP_LABEL: Record<KitGroup, string> = {
|
||
basic: "기본 부품 (552-2)",
|
||
compound: "묶음 부품 (552-3)",
|
||
frame: "틀 (552-4)",
|
||
};
|
||
|
||
export interface KitPart {
|
||
key: string;
|
||
name: string;
|
||
group: KitGroup;
|
||
/** 꼴 · 갈래 */
|
||
kinds: string;
|
||
/** 만들 파일(`ui_template/` 안) */
|
||
file: string;
|
||
/** 지금 견본 — 상태마다 하나 · null = 그 상태 없음 · 없으면 빈 자리 */
|
||
demo?: (state: KitState) => HTMLElement | null;
|
||
/** 크기 단 견본 — 여덟 단 한 줄(크기 없는 부품은 없음) */
|
||
sizes?: (size: UiSize) => HTMLElement;
|
||
/** 견본이 옮기기 전 PageKit · 옛 공용 부품인지 */
|
||
legacy?: boolean;
|
||
}
|
||
|
||
/** 상태 굳히기 — 마우스 · 눌림 · 오류 = data-state · 막힘 = disabled */
|
||
export function markState<T extends HTMLElement>(node: T, state: KitState): T {
|
||
if (state === "disabled") {
|
||
if ("disabled" in node) (node as unknown as { disabled: boolean }).disabled = true;
|
||
node.setAttribute("aria-disabled", "true");
|
||
} else if (state !== "default") {
|
||
node.dataset.state = state;
|
||
if (state === "error") {
|
||
node.classList.add("is-error");
|
||
node.setAttribute("aria-invalid", "true");
|
||
}
|
||
}
|
||
return node;
|
||
}
|
||
|
||
const row = (...children: HTMLElement[]): HTMLElement => {
|
||
const box = document.createElement("div");
|
||
box.className = "ui-kit__demo-row";
|
||
box.append(...children);
|
||
return box;
|
||
};
|
||
|
||
const noop = (): void => {};
|
||
|
||
/** 552-3 견본 — 표 칸 · 줄 · ⋯ 메뉴 항목 · 펼친 ⋯ 판 */
|
||
const KIT_COLS: Column[] = [{ label: "노선" }, { label: "", width: "44px" }];
|
||
const KIT_DATA: Column[] = [{ label: "노선" }, { label: "연장", align: "end" }];
|
||
const KIT_MENU: RowMenuItem[] = [
|
||
{ label: "이름 바꾸기", onClick: noop },
|
||
{ label: "삭제", danger: true, onClick: noop },
|
||
];
|
||
const kitRows = (): (string | HTMLElement)[][] => [
|
||
["1", createRowMenu(KIT_MENU)],
|
||
["2", createRowMenu(KIT_MENU)],
|
||
];
|
||
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³"],
|
||
];
|
||
/** 634-1 견본 — 고르기 메뉴 하나(스냅 꼴) · 여럿(지도 겹 묶음 · 켜면 눌림 · 막힌 줄 · 추천노선 꼴 색 점 줄 · 589-12) */
|
||
const pickDemo = (s: KitState): HTMLElement | null => {
|
||
if (s !== "default" && s !== "disabled") return null;
|
||
const one = createPickMenu({ icon: "magnet", title: "스냅", items: [{ value: "a", label: "10 m" }, { value: "b", label: "측점 No." }], offLabel: "끔", onPick: noop });
|
||
const colored = createPickMenu({ multi: true, icon: "compass", title: "추천노선", onPick: noop,
|
||
items: [{ value: "a", label: "노선 가", color: "var(--draw-map-rec-1)" }, { value: "b", label: "노선 나", color: "var(--draw-map-rec-2)" }] });
|
||
colored.setItem("a", { checked: true });
|
||
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, colored.root].forEach((b) => (b.disabled = true));
|
||
return row(one.root, many.root, colored.root);
|
||
};
|
||
/** 709-1 견본 — 쪽 단추(가운데 쪽 · 한 장뿐이라 이전 · 다음 다 막힘) · 수평 구분선(소제목 사이) */
|
||
const pagerDemo = (s: KitState): HTMLElement | null =>
|
||
s === "default" ? row(createPager(1234, 30, 2, noop), createPager(12, 30, 1, noop)) : null;
|
||
const dividerDemo = (s: KitState): HTMLElement | null => {
|
||
if (s !== "default") return null;
|
||
const box = document.createElement("div");
|
||
box.style.cssText = "display:flex;flex-direction:column;width:200px";
|
||
const label = (t: string): HTMLElement => Object.assign(document.createElement("span"), { textContent: t });
|
||
box.append(label("옵션"), createDivider(), label("자재"));
|
||
return box;
|
||
};
|
||
const openMenu = (s: KitState, items = KIT_MENU): HTMLElement => {
|
||
const menu = createRowMenu(items);
|
||
menu.classList.add("is-open");
|
||
const pop = menu.querySelector<HTMLElement>(".ui-row-menu__pop")!;
|
||
pop.hidden = false;
|
||
pop.style.position = "static";
|
||
markState(pop.querySelector<HTMLElement>(".ui-row-menu__item")!, s);
|
||
return menu;
|
||
};
|
||
|
||
/** 세로로 쌓기(552-3 견본) */
|
||
const col = (...children: HTMLElement[]): HTMLElement => {
|
||
const box = row(...children);
|
||
box.style.flexDirection = "column";
|
||
box.style.alignItems = "stretch";
|
||
return box;
|
||
};
|
||
/** 557-2 견본 — 접이 구역 머리 · 칸 램프 표 · 규칙 꼴 키 칩 · 계산 줄 */
|
||
const kitSection = (): HTMLElement => {
|
||
const box = el("section", { className: "ui-collapsible" });
|
||
box.append(
|
||
createCollapsibleHead({ title: "조합 연결", sub: "연결됨 1 / 4", level: 4 }).root,
|
||
createText("배관 · 본체 미연결"),
|
||
);
|
||
return box;
|
||
};
|
||
const kitLampTable = (): HTMLElement =>
|
||
createDataTable({
|
||
columns: [
|
||
{ key: "st", label: "측점" },
|
||
{ key: "c", label: "단가연결" },
|
||
],
|
||
rows: [
|
||
{ st: "No.1", c: "단가연결" },
|
||
{ st: "No.2", c: "연결됨" },
|
||
],
|
||
cellClass: (i, key) =>
|
||
key === "st"
|
||
? `ui-data-table__lamp${i ? " is-linked" : ""}`
|
||
: `ui-data-table__lamp ui-data-table__lamp--only${i ? " is-linked" : ""}`,
|
||
}).root;
|
||
const kitKeys = (...keys: string[]): DocumentFragment => {
|
||
const f = document.createDocumentFragment();
|
||
f.append(...keys.map((k) => createBadge(k, "neutral")));
|
||
return f;
|
||
};
|
||
const kitCalc = (): HTMLElement => {
|
||
const input = (v: string): HTMLElement => {
|
||
const f = createInputField({ type: "number", compact: true });
|
||
f.input.value = v;
|
||
f.input.classList.add("ui-calc-row__in");
|
||
return f.input;
|
||
};
|
||
const out = (v: string, key = false): HTMLElement =>
|
||
el("span", { className: `ui-calc-row__out${key ? " is-key" : ""}`, text: v });
|
||
return col(
|
||
createCalcFormula("Q = 1/3.6 × f × rt × A"),
|
||
createCalcRow("f : 유출계수", input("0.8")),
|
||
createCalcRow("A : 유역면적 (㎢)", input("0.46"), out("46.00 ha")),
|
||
createCalcFormulaRow("경심", createCalcFormula("R = A / P"), out("0.250")),
|
||
createCalcFormulaRow(
|
||
"유속 (m/s)",
|
||
createCalcFormula("V = 1/n × R^2/3^ × S^1/2^"),
|
||
out("2.41", true),
|
||
),
|
||
);
|
||
};
|
||
/** 552-2 견본 조각 — 상태를 안쪽 요소에 굳힘 */
|
||
const OPTS = [
|
||
{ value: "a", text: "임도" },
|
||
{ value: "b", text: "예정노선" },
|
||
];
|
||
interface FieldDemo {
|
||
root: HTMLElement;
|
||
node: HTMLElement;
|
||
setError?: (m?: string) => void;
|
||
}
|
||
function fieldDemo(s: KitState, make: () => FieldDemo): HTMLElement | null {
|
||
if (s === "active") return null;
|
||
const f = make();
|
||
markState(f.node, s);
|
||
if (s === "error") f.setError?.("값을 넣어야 함");
|
||
return f.root;
|
||
}
|
||
function checkDemo(s: KitState): HTMLElement | null {
|
||
if (s === "active" || s === "error") return null;
|
||
const boxes = [
|
||
createCheckbox("등고선", { checked: true }),
|
||
createCheckbox("밝음", { radio: `kit-${s}`, checked: true }),
|
||
createCheckbox("어두움", { radio: `kit-${s}` }),
|
||
createCheckbox("지적", { chip: true, checked: true }),
|
||
createCheckbox("유역", { chip: true }),
|
||
];
|
||
for (const b of boxes) {
|
||
if (s === "disabled") b.input.disabled = true;
|
||
else markState(b.root, s);
|
||
}
|
||
return row(...boxes.map((b) => b.root));
|
||
}
|
||
function segmentDemo(s: KitState): HTMLElement | null {
|
||
if (s === "error") return null;
|
||
const seg = createSegment({
|
||
options: [
|
||
{ value: "list", label: "목록" },
|
||
{ value: "card", label: "카드" },
|
||
{ value: "map", icon: "map", title: "지도" },
|
||
],
|
||
value: "list",
|
||
});
|
||
const items = [...seg.root.children] as HTMLButtonElement[];
|
||
if (s === "disabled") items.forEach((b) => markState(b, s));
|
||
else markState(items[1], s);
|
||
// 풂 꼴(589-9) — 고른 칸을 다시 누르면 모두 풀림
|
||
const clearable = createSegment({
|
||
options: [{ value: "a", label: "방식 가" }, { value: "b", label: "방식 나" }],
|
||
value: "a",
|
||
clearable: true,
|
||
});
|
||
if (s === "disabled") [...clearable.root.children].forEach((b) => markState(b as HTMLElement, s));
|
||
return row(seg.root, clearable.root);
|
||
}
|
||
function tabsDemo(s: KitState): HTMLElement | null {
|
||
if (s === "active" || s === "error") return null;
|
||
const line = createTabs({
|
||
tabs: [
|
||
{ key: "a", label: "개요" },
|
||
{ key: "b", label: "구성원" },
|
||
{ key: "c", label: "기록" },
|
||
],
|
||
value: "a",
|
||
});
|
||
const pill = createTabs({
|
||
kind: "pill",
|
||
tabs: [
|
||
{ key: "a", label: "토공" },
|
||
{ key: "b", label: "배수" },
|
||
{ key: "c", label: "구조물" },
|
||
],
|
||
value: "a",
|
||
});
|
||
line.setBadge("b", 3);
|
||
for (const t of [line, pill]) markState(t.root.children[1] as HTMLButtonElement, s);
|
||
const box = document.createElement("div");
|
||
box.className = "ui-kit__demo-col";
|
||
box.append(line.root, pill.root);
|
||
return box;
|
||
}
|
||
|
||
// prettier-ignore
|
||
export const KIT_PARTS: KitPart[] = [
|
||
// ── 552-2 기본 ─────────────────────────────────────────────
|
||
{ key: "button", name: "단추", group: "basic", kinds: "주 · 보조 · 고스트 글 · 위험 · 위험 고스트 · 유리 · 글 링크 · 갈래 색(옵션값 · 요소 연결) · 여덟 단", file: "ui_template_button.ts",
|
||
demo: (s) => (s === "error" ? null : row(...([["저장", "filled"], ["취소", "ghost"], ["보기", "pill"], ["삭제", "danger"], ["초기화", "danger-ghost"], ["사용법", "link"]] as const).map(([label, variant]) => markState(createButton({ label, variant, size: 28, onClick: noop }), s)), ...([["옵션값", "option"], ["요소 연결", "link"]] as const).map(([label, tone]) => markState(createButton({ label, variant: "ghost", size: 20, tone, onClick: noop }), s)))),
|
||
sizes: (z) => createButton({ label: String(z), size: z, iconStart: "plus", onClick: noop }) },
|
||
{ key: "icon-button", name: "아이콘 단추", group: "basic", kinds: "고스트 · 선 · 채움 · 유리 · 켜짐(aria-pressed) · 글자 기호 · 여덟 단 · title 필수", file: "ui_template_button.ts",
|
||
demo: (s) => (s === "error" ? null : row(markState(createIconButton({ icon: "settings", title: "설정" }), s), markState(createIconButton({ icon: "refresh", title: "새로 고침", variant: "outline" }), s), markState(createIconButton({ icon: "plus", title: "추가", variant: "filled" }), s), markState(createIconButton({ icon: "eye", title: "보기", pressed: true }), s), markState(createIconButton({ glyph: "▲", title: "위로", variant: "outline" }), s), markState(createIconButton({ icon: "adjustments", title: "조정", variant: "glass" }), s))),
|
||
sizes: (z) => createIconButton({ icon: "settings", title: `설정 ${z}`, size: z, variant: "outline" }) },
|
||
{ key: "input", name: "입력", group: "basic", kinds: "글 · 수 · 단위 붙음 · 찾기 · 날짜 · 여러 줄 · 여덟 단", file: "ui_template_input.ts",
|
||
demo: (s) => fieldDemo(s, () => { const f = createInputField({ placeholder: "폭", type: "number", unit: "m", size: 32 }); return { root: f.root, node: f.input, setError: f.setError }; }),
|
||
sizes: (z) => createInputField({ placeholder: String(z), size: z }).input },
|
||
{ key: "select", name: "고르기", group: "basic", kinds: "한 개 · 화살표 = 셰브런 · 여덟 단", file: "ui_template_input.ts",
|
||
demo: (s) => fieldDemo(s, () => { const f = createSelectField({ options: OPTS, size: 32 }); return { root: f.root, node: f.select }; }),
|
||
sizes: (z) => createSelectField({ options: OPTS, size: z }).select },
|
||
{ key: "check", name: "체크 · 라디오", group: "basic", kinds: "체크 · 라디오 · 글 붙음 · 칩(켜면 채움)", file: "ui_template_checkbox.ts", demo: checkDemo },
|
||
{ key: "toggle", name: "켜기", group: "basic", kinds: "스위치 · 켜기 칩(색 점) · 20 · 24 · 28 · 32", file: "ui_template_toggle.ts",
|
||
demo: (s) => (s === "error" ? null : row(...[createToggle({ label: "등고선", checked: true }), createToggle({ label: "지적" }), createToggle({ kind: "chip", label: "유역", checked: true, dot: "var(--color-info)" })].map((t) => markState(t.root, s)))),
|
||
sizes: (z) => createToggle({ label: String(z), size: z, checked: true }).root },
|
||
{ key: "segment", name: "분할 고르기", group: "basic", kinds: "글 · 아이콘 · 둘 ~ 다섯 칸 · 20 · 24 · 28 · 32", file: "ui_template_segment.ts", demo: segmentDemo,
|
||
sizes: (z) => createSegment({ options: [{ value: "a", label: String(z) }, { value: "b", icon: "map", title: "지도" }], value: "a", size: z }).root },
|
||
{ key: "tabs", name: "탭", group: "basic", kinds: "밑줄 탭(한 꼴) · 수 배지 · 보조 탭(작은 알약) · 문서 탭은 꼴 그대로", file: "ui_template_tabs.ts", demo: tabsDemo },
|
||
{ key: "badge", name: "배지", group: "basic", kinds: "상태 · 역할 · 종류 · 수 · 램프(✓ ✗ 같이) · 점", file: "ui_template_badge.ts",
|
||
demo: (s) => (s === "default" ? row(createBadge("진행 중", "accent"), createBadge("완료", "success"), createBadge("멈춤", "warning"), createBadge("오류", "danger"), createBadge("안정성", "info"), createBadge("임시", "neutral"), createBadge("3", "danger", { kind: "count" }), createBadge("통과", "success", { kind: "lamp" }), createBadge("미달", "danger", { kind: "lamp" }), createBadge("확인 필요", "warning", { kind: "dot" }), createBadge("작게", "neutral", { size: 18 })) : null) },
|
||
{ key: "tooltip", name: "툴팁", group: "basic", kinds: "마우스 글 · 값 풍선 · 그림 위 설명", file: "ui_template_hover_info.ts",
|
||
demo: (s) => (s === "default" ? row(tooltipBox("누르면 값을 고침"), tooltipBox("구간 1 · 높이 2.4 m\n기울기 1 : 1.5")) : null) },
|
||
{ key: "icon", name: "아이콘", group: "basic", kinds: "Tabler 선 · 16 · 18 · 24 · 32 · 이모지 대신", file: "ui_template_icons.ts",
|
||
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 · 글 줄 안 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);
|
||
return s === "error" ? null : s === "default" ? col(createCard({ title: "개요", body: ["임도 1 노선 · 1,080 m"], pad: 16 }), createCard({ title: "머리 카드", body: ["몸"], pad: 12, actions: [createButton({ label: "열기", variant: "ghost", size: "sm", onClick: noop })] }), createCard({ title: "고른 카드", body: ["골라짐"], pad: 12, onSelect: noop, selected: true })) : pick(); } },
|
||
{ 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<HTMLElement>(".ui-collapsible__title")!, s); return open; } return col(open, createPanelCard("접힌 카드"), kitSection()); } },
|
||
{ 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<HTMLElement>("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: "노선 없음" }), createTable([], gridRows(), { density: "data", grid: true }), kitLampTable()); } },
|
||
{ 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",
|
||
demo: (s) => { if (s !== "default") return null; const m = buildModal({ title: "구조물 고르기", closeLabel: "닫기", width: "40rem" }, noop); m.body.append("몸 — 표 · 양식이 들어감"); m.dialog.style.margin = "0";
|
||
const long = buildModal({ title: "배수관 · 횡배수관 · 측구 · 집수정 · 옹벽 구조물을 한꺼번에 고르기", closeLabel: "닫기", width: "24rem" }, noop); long.body.append("[닫기] 는 한 줄 · 제목은 말줄임"); long.dialog.style.margin = "0";
|
||
const mid = buildModal({ title: "운반 단가 — 넓은 중간 창", closeLabel: "닫기", wide: true, width: "1100px" }, noop); mid.body.append("몸 — 두 칸이 따로 스크롤"); mid.dialog.style.cssText = "margin:0;max-width:100%;height:160px";
|
||
return col(m.dialog, long.dialog, mid.dialog, confirmPanel({ message: "노선을 저장할까요?" }).panel, confirmPanel({ title: "삭제", message: "임도 2 노선을 지울까요? 되돌릴 수 없음", okLabel: "삭제", danger: true }).panel, confirmPanel({ message: "블록 이름", field: { placeholder: "예: 배수관" } }).panel, confirmPanel({ title: "같은 내용 있음", message: "「SK-000001 유로폼(보통)」 과 내용이 같음", choices: [{ label: "그래도 새로", value: "new" }, { label: "그것을 씀", value: "use" }] }).panel); } },
|
||
{ key: "toast", name: "알림", group: "compound", kinds: "토스트 4꼴(성공 · 오류 · 정보 · 경고) · 로딩 가림막 · 진행 가림막", file: "ui_template_toast.ts",
|
||
demo: (s) => (s === "default" ? col(toastNode("저장됨", "success"), toastNode("저장 못 함 — 다시 시도", "error"), toastNode("노선을 불러옴", "info"), toastNode("측점 간격이 넓음", "warning")) : null) },
|
||
{ key: "banner", name: "띠 알림", group: "compound", kinds: "왼쪽 띠 · 정보 · 경고 · 위험 · 성공(✓) · 펼침 · 안 단추 · 닫기", file: "ui_template_banner.ts",
|
||
demo: (s) => (s === "default" ? col(createBanner({ title: "안내", text: "초기값에서 시작함" }), createBanner({ tone: "warning", text: "저장 안 한 고침이 있음", actions: [createButton({ label: "저장", size: "sm", onClick: noop })] }), createBanner({ tone: "danger", text: "횡단 3 곳 계산 못 함", details: "No.12 · No.15 · No.20" }), createBanner({ tone: "success", text: "검토 끝", onClose: noop })) : null) },
|
||
{ key: "stat", name: "숫자 카드", group: "compound", kinds: "값 · 단위 · 변화 · 게이지 · 판정", file: "ui_template_stat.ts",
|
||
demo: (s) => (s === "default" ? statGrid([createStat({ label: "진행 중", value: 12, unit: "건", delta: "+3" }), createStat({ label: "디스크", value: "71 %", gauge: 0.71, tone: "warn" }), createStat({ label: "오류", value: 2, delta: "-1", tone: "bad" }), createStat({ label: "회사", value: 8, compact: true })]) : null) },
|
||
{ key: "kv", name: "값 목록", group: "compound", kinds: "이름 : 값 줄 · 판정(✓ ✗) · 큰 값 · 규칙 꼴(키 칩 = 설명)", file: "ui_template_kv.ts",
|
||
demo: (s) => (s === "default" ? col(createKv([{ label: "연장", value: "1,080 m", big: true }, { label: "종단 경사", value: "12 %", ok: true }, { label: "최소 곡선반경", value: "12 m", ok: false }, { label: "비고", value: null }]), createKv([{ label: kitKeys("Ctrl", "Z"), value: "되돌리기" }, { label: kitKeys("휠 끌기"), value: "화면 옮기기" }, { label: kitKeys("Esc") }], { rule: true })) : null) },
|
||
{ key: "calc-row", name: "계산 줄", group: "compound", kinds: "값 줄(이름 · 값 112 · 덧칸) · 식 줄(이름 · 식 · 값) · 위 · 아래첨자 · 중요 결과 굵게", file: "ui_template_calc_row.ts",
|
||
demo: (s) => (s === "default" ? kitCalc() : null) },
|
||
{ key: "hopyo-grid", name: "일위대가 격자", group: "compound", kinds: "호표 머리(이름 · 규격 · 단위 · 1 단위 합계) · 구성 줄(품명 · 규격 · 수량 · 단위 · 비목 단가 / 금액 · 비고) · 줄 꼬리표 품셈 · 고정 · 합계 줄 빈 줄 붉게", file: "ui_template_hopyo_grid.ts", demo: (s) => (s === "default" ? createHopyoGrid([{ key: "D", no: 17, name: "돌기슭막이 H=2.0", unit: "m", total: { sum: 121748.6, labor: 94447.5, material: 0, expense: 27301.1 }, lines: [{ name: "돌쌓기 찰쌓기", spec: "뒷길이 35cm", qty: 2.09, unit: "㎡", prices: { sum: 52995, labor: 41250, expense: 11745 }, amounts: { sum: 110759.6, labor: 86212.5, expense: 24547.1 }, note: "산근 1호표", tag: "품셈" }, { name: "야면석 채집 운반", spec: "계곡 내", qty: 0.45, unit: "㎥", prices: { sum: 24420, labor: 18300, expense: 6120 }, amounts: { sum: 10989, labor: 8235, expense: 2754 }, tag: "고정" }, { name: "잡재료", spec: "노무비의", qty: null, unit: "%", prices: {}, amounts: {}, note: "수량 없음", tag: "품셈" }] }]).root : null) },
|
||
{ key: "progress", name: "진행 막대", group: "compound", kinds: "막대 6 · 8 · 쌓임 막대 · 원 · 원 귀퉁이(corner) · 오류", file: "ui_template_progress.ts",
|
||
demo: (s) => { if (s === "error") return markState(createProgressBar({ value: 0.6, label: "실패" }).root, s); if (s !== "default") return null; const ring = createProgressCircle({ size: 48 }); ring.set(0.4);
|
||
const corner = createProgressCircle({ size: 40, corner: true }); corner.set(0.65, "지형 65%"); const box = document.createElement("div"); box.style.cssText = "position:relative;height:72px;border:1px dashed var(--color-border);border-radius:var(--radius-inputs)"; box.append(corner.root);
|
||
return col(createProgressBar({ value: 0.4, label: "40 %" }).root, createProgressBar({ value: 0.7, thick: 6 }).root, createProgressBar({ parts: [{ value: 2, tone: "soft" }, { value: 3 }, { value: 1, tone: "warn" }], label: "6 건" }).root, ring.root, box); } },
|
||
{ key: "mini-chart", name: "작은 그래프", group: "compound", kinds: "막대 · 원 · 선", file: "ui_template_elements_chart.ts",
|
||
demo: (s) => (s === "default" ? col(createBarChart([{ label: "산림설계", value: 12 }, { label: "녹색기술", value: 7 }, { label: "그 밖 3 곳", value: 4 }]), createDonut([{ label: "임도", value: 5 }, { label: "예정노선", value: 3 }, { label: "안정성", value: 2 }]), createLineChart({ series: [{ name: "CPU", values: [20, 35, 30, 50, 42, 60], colorIndex: 0 }], height: 120 })) : null) },
|
||
{ key: "legend", name: "범례", group: "compound", kinds: "색 칩 · 선 · 점선 · 점 · 꺼짐 · 세로", file: "ui_template_flow_legend.ts",
|
||
demo: (s) => (s === "default" ? col(createLegend([{ label: "임도", color: "var(--color-chart-0)" }, { label: "설계선", color: "var(--color-chart-2)", kind: "line" }, { label: "원지반선", color: "var(--color-chart-1)", kind: "dash" }, { label: "관", color: "var(--color-chart-3)", kind: "dot" }, { label: "꺼진 층", color: "var(--color-chart-0)", off: true }], { onToggle: noop }), createLegend([{ label: "절토", color: "var(--color-danger)" }, { label: "성토", color: "var(--color-info)" }], { vertical: true })) : null) },
|
||
{ key: "range-bar", name: "범위 바", group: "compound", kinds: "칸(넓이 = 경계 비례) · 칸 사이 경계값 하나 · 칸 눌러 고름 · 색 상자 · 칠 안 함 빗금 · 경계 오류 · 24 · 28", file: "ui_template_range_bar.ts",
|
||
demo: (s) => { if (s !== "default" && s !== "error") return null; const bar = createRangeBar({ edges: [0, 26.8, 70, 83.9, 250], colors: ["", "c0", "c1", "c2"], palette: [0, 1, 2, 3].map((n) => ({ value: `c${n}`, name: `색 ${n + 1}`, color: `var(--color-chart-${n})` })), noneLabel: "칠 안 함", unit: "%", ends: ["0 %", "250 % 이상"], hint: (v) => `${Math.round(v)}`, onChange: noop });
|
||
if (s === "error") bar.setError(2); return bar.root; } },
|
||
{ key: "setting-row", name: "설정 줄", group: "compound", kinds: "제목 · 설명 왼쪽 · 오른쪽 값 · 조작 · 줄 52 · 좁은 꼴(줄 28)", file: "ui_template_setting_row.ts",
|
||
demo: (s) => (s === "default" ? col(settingCard([settingRow("회사 이름", "보고서 머리에 들어감", valueText("산림설계")), settingRow("단위", "", valueText(null)), settingRow("기본 노폭", "새 노선에 먼저 들어감", createInputField({ placeholder: "3.0 m" }).root), settingRow("글꼴 파일", "", createButton({ label: "바꾸기", variant: "ghost", size: 28, onClick: noop }))]), settingCard([settingRow("측면 한계각(°)", "", createInputField({ placeholder: "30", size: 24 }).root, { compact: true }), settingRow("균열 모드", "", valueText("없음"), { compact: true })])) : null) },
|
||
{ key: "pager", name: "쪽 단추", group: "compound", kinds: "이전 · 다음 · 「n / N 쪽 · 전체 줄」 · 첫 쪽 이전 막힘 · 끝 쪽 다음 막힘 · 표 바로 아래 · 줄 수 = 화면 높이 셈(fitRows)", file: "ui_template_pager.ts", demo: pagerDemo },
|
||
{ key: "divider", name: "수평 구분선", group: "compound", kinds: "가로 한 줄 · 왼쪽 판 칸 사이 · 패널 안 소제목 사이 · 시험 계산 옵션 / 자재 사이", file: "ui_template_divider.ts", demo: dividerDemo },
|
||
{ key: "drop", name: "파일 놓기 칸", group: "compound", kinds: "끌어 놓기 · 고르기 단추 · 받는 꼴 안내 · 진행 · 오류", file: "ui_template_drop.ts",
|
||
demo: (s) => { if (s === "active") return null; const d = createDrop({ accept: ".las,.laz", hint: "LAS · LAZ · 2 GB 까지", onFiles: noop, disabled: s === "disabled" });
|
||
if (s === "error") d.setError("LAS · LAZ 파일만 받음"); if (s === "hover" || s === "error") markState(d.root, s);
|
||
if (s !== "default") return d.root; const busy = createDrop({ onFiles: noop }); busy.setProgress(0.4, "40 %"); return col(d.root, busy.root); } },
|
||
// ── 552-4 틀 ───────────────────────────────────────────────
|
||
{ key: "work-frame", name: "작업 화면 틀", group: "frame", kinds: "왼쪽 패널(흰 판 · 1 px 선) · 본문 · 아래 단추 줄 · [?][<][>] · 머리 줄 없음 · ☰ 서랍", file: "ui_template_overlay.ts",
|
||
demo: FRAME_DEMOS["work-frame"] },
|
||
{ key: "page-frame", name: "일반 화면 틀", group: "frame", kinds: "페이지 머리(제목 · 설명 · 주 단추) · 몸통 · 좁은 폭 720 · 입력 칸 2열 · 끝 단추 줄", file: "ui_template_general_layout.ts",
|
||
demo: FRAME_DEMOS["page-frame"] },
|
||
{ key: "public-frame", name: "공개 화면 틀", group: "frame", kinds: "머리 띠(제목 · 설명 · 주 단추) · 구역 · 본문", file: "ui_template_public_layout.ts",
|
||
demo: FRAME_DEMOS["public-frame"] },
|
||
{ key: "float-panel", name: "떠 있는 판", group: "frame", kinds: "그림 위 판 · 끌기 머리 · 닫기 ✕ · 팝오버", file: "ui_template_float_panel.ts",
|
||
demo: FRAME_DEMOS["float-panel"] },
|
||
{ key: "drawer", name: "오른쪽 서랍", group: "frame", kinds: "상세 · 설정 · 폭 끌기 · 흰 판", file: "ui_template_side_panels.ts",
|
||
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",
|
||
demo: FRAME_DEMOS["panel-head"] },
|
||
{ key: "list-row", name: "목록 줄", group: "frame", kinds: "고르는 줄 · 두 줄 · 끌기 손잡이 · 색 띠 · 번호 원 · 28 · 32 · 36", file: "ui_template_list_row.ts",
|
||
demo: FRAME_DEMOS["list-row"] },
|
||
];
|