From aaac17db4c0b883d3c0a047e7d75120742f59abf Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 20:49:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui-kit):=20=EB=B6=80=ED=92=88=20=EB=AA=A8?= =?UTF-8?q?=EC=9D=8C=20=ED=99=94=EB=A9=B4=20=ED=8B=80=20=E2=80=94=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EB=A7=8C=20=C2=B7=20=EB=B0=9D=EC=9D=8C=20/=20=EC=96=B4?= =?UTF-8?q?=EB=91=90=EC=9B=80=20=EB=82=98=EB=9E=80=ED=9E=88=20=C2=B7=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=8B=A4=EC=84=AF=20=EC=B9=B8=20=C2=B7=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=20=EC=97=AC=EB=8D=9F=20=EB=8B=A8=20(PLAN=205?= =?UTF-8?q?52-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 대시보드 세부 페이지 `#/b01-account?view=ui-kit` 등록부 한 줄(시스템 관리 묶음 · SYSTEM_ADMIN 만) - ui_template_ui_kit.ts — 테마 칸마다 iframe(data-theme)에 지금 CSS 를 옮겨 실제 값으로 그림 - ui_template_ui_kit_parts.ts — 표준 부품 30 줄(552-2 기본 · 552-3 묶음 · 552-4 틀) · 머리 = 부품 규칙(자리 · 이름 · data-size · data-state · 색 · 아이콘) - 지금 PageKit · 공용 부품 견본(단추 · 입력 · 고르기 · 탭 · 배지 · 표 · ⋯ · 빈 상태 · 설정 줄 · 아이콘) · 나머지 빈 자리 - test_546 시스템 메뉴 기대값에 ui-kit 더함 · 새 지킴 시험 test_552_1_ui_kit Co-Authored-By: Claude Opus 5.5 --- B01_Dashboard/B01_Dashboard_UI_Views.ts | 2 + resources/tester/test_546_b01_shell.py | 2 +- resources/tester/test_552_1_ui_kit.py | 67 +++++++++++ ui_template/ui_template_ui_kit.css | 145 ++++++++++++++++++++++++ ui_template/ui_template_ui_kit.ts | 128 +++++++++++++++++++++ ui_template/ui_template_ui_kit_parts.ts | 137 ++++++++++++++++++++++ 6 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 resources/tester/test_552_1_ui_kit.py create mode 100644 ui_template/ui_template_ui_kit.css create mode 100644 ui_template/ui_template_ui_kit.ts create mode 100644 ui_template/ui_template_ui_kit_parts.ts diff --git a/B01_Dashboard/B01_Dashboard_UI_Views.ts b/B01_Dashboard/B01_Dashboard_UI_Views.ts index 91f034591..82cf3cc62 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Views.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Views.ts @@ -25,6 +25,7 @@ import { mountSysDefaults } from "./B01_Dashboard_UI_View_SysDefaults"; import { mountSysLogs } from "./B01_Dashboard_UI_View_SysLogs"; import { mountSysOverview } from "./B01_Dashboard_UI_View_SysOverview"; import { mountSysUsers } from "./B01_Dashboard_UI_View_SysUsers"; +import { mountUiKit } from "@ui/ui_template_ui_kit"; /** 역할 넷 — 산림청 = 세션 is_forest_service 인 일반 사용자(회사 관리자 · 시스템 관리자가 앞섬) */ export type ViewRole = "USER" | "ADMIN" | "FOREST" | "SYSTEM_ADMIN"; @@ -70,6 +71,7 @@ export const VIEWS: View[] = [ { key: "sys-users", title: "사용자", group: "system", roles: ["SYSTEM_ADMIN"], icon: "user", mount: mountSysUsers }, { key: "sys-defaults", title: "설계 기본값", group: "system", roles: ["SYSTEM_ADMIN"], icon: "database", mount: mountSysDefaults }, { key: "sys-logs", title: "시스템 로그", group: "system", roles: ["SYSTEM_ADMIN"], icon: "fileText", mount: mountSysLogs }, + { key: "ui-kit", title: "부품 모음", group: "system", roles: ["SYSTEM_ADMIN"], icon: "adjustments", mount: mountUiKit }, // 도움 { key: "guide", title: "사용자 가이드", group: "help", roles: ALL, icon: "book", mount: mountGuide }, { key: "chat", title: "지식 챗봇", group: "help", roles: ALL, icon: "chatbot", mount: mountChat }, diff --git a/resources/tester/test_546_b01_shell.py b/resources/tester/test_546_b01_shell.py index 9acde4b6d..9f509ecd9 100644 --- a/resources/tester/test_546_b01_shell.py +++ b/resources/tester/test_546_b01_shell.py @@ -67,7 +67,7 @@ def test_역할별_메뉴(): assert system == [ *work, "company:people", "company:company", "system:sys-overview", "system:sys-companies", "system:sys-users", - "system:sys-defaults", "system:sys-logs", *help_, + "system:sys-defaults", "system:sys-logs", "system:ui-kit", *help_, ] # fmt: skip diff --git a/resources/tester/test_552_1_ui_kit.py b/resources/tester/test_552_1_ui_kit.py new file mode 100644 index 000000000..10f72073b --- /dev/null +++ b/resources/tester/test_552_1_ui_kit.py @@ -0,0 +1,67 @@ +"""552-1 부품 모음 화면 지킴 시험 — 화면 확인은 ORCA. + +· `#/b01-account?view=ui-kit` = 시스템 관리자만(다른 역할은 메뉴에 없고 주소로 와도 홈) +· 부품 목록 = 열쇠 겹침 없음 · 세 묶음(552-2 기본 · 552-3 묶음 · 552-4 틀) · 파일은 ui_template_ 안 +· 크기 여덟 단 · 상태 다섯 칸 · 밝음 / 어두움 두 칸(iframe data-theme) · 날 색 없음 +""" + +from __future__ import annotations + +import re +import shutil +from pathlib import Path + +import pytest + +from test_546_b01_shell import MENU, _run_views + +ROOT = Path(__file__).resolve().parents[2] +UI = ROOT / "ui_template" + + +def _text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") +def test_시스템_관리자만(): + out = _run_views( + MENU + "console.log(JSON.stringify(['USER','ADMIN','FOREST','SYSTEM_ADMIN'].map(" + "(r) => [menu(r).includes('system:ui-kit'), viewFor('ui-kit', r).key])));" + ) + assert out == [[False, "home"], [False, "home"], [False, "home"], [True, "ui-kit"]] + + +def test_부품_목록(): + src = _text(UI / "ui_template_ui_kit_parts.ts") + rows = re.findall( + r'\{ key: "([^"]+)", name: "[^"]+", group: "(\w+)",[^\n]*file: "([^"]+)"', src + ) + keys = [k for k, _, _ in rows] + assert len(keys) == len(set(keys)) >= 25 + assert {g for _, g, _ in rows} == {"basic", "compound", "frame"} + assert all(f.startswith("ui_template_") and f.endswith(".ts") for _, _, f in rows) + # 묶음 차례 그대로 — 창마다 자기 덩어리만 고침 + groups = [g for _, g, _ in rows] + assert groups == sorted(groups, key=["basic", "compound", "frame"].index) + assert "KIT_SIZES = [18, 20, 22, 24, 28, 32, 36, 42] as const" in src + assert 'KIT_STATES = ["default", "hover", "active", "disabled", "error"] as const' in src + # 규칙 머리 — 크기 · 상태 · 색 · 아이콘 + for word in ( + 'data-size="18|20|22|24|28|32|36|42"', + 'data-state="hover"', + "--draw-*", + "icon(name, size)", + ): + assert word in src, word + + +def test_밝음_어두움_두_칸(): + view = _text(UI / "ui_template_ui_kit.ts") + assert 'panes.append(themePane("light"), themePane("dark"))' in view + assert "doc.documentElement.dataset.theme = theme" in view + assert "frame.srcdoc" in view # 표준 모드 문서 + css = _text(UI / "ui_template_ui_kit.css") + assert not re.search(r"#[0-9a-fA-F]{3,8}\b|rgba?\(", css) # 색 = 공통 값만 + for path in (view, _text(UI / "ui_template_ui_kit_parts.ts"), css): + assert len(path.splitlines()) <= 700 diff --git a/ui_template/ui_template_ui_kit.css b/ui_template/ui_template_ui_kit.css new file mode 100644 index 000000000..263dac4e1 --- /dev/null +++ b/ui_template/ui_template_ui_kit.css @@ -0,0 +1,145 @@ +/* 부품 모음 화면(552-1) — 밝음 · 어두움 두 칸 · 칸 안은 iframe 문서 */ +.b01-pg.ui-kit { + max-width: none; +} + +.ui-kit__panes { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(520px, 1fr)); + gap: 16px; + align-items: start; +} + +.ui-kit__pane-title { + margin: 0 0 8px; + font-size: var(--text-body-sm); + font-weight: 600; +} + +.ui-kit__frame { + display: block; + width: 100%; + min-height: 240px; + border: 1px solid var(--color-border); + border-radius: var(--radius-cards, 10px); +} + +/* iframe 문서 */ +.ui-kit__doc { + margin: 0; + background: var(--color-bg); +} + +/* 셸 별칭만 받고 셸 배치(100dvh · grid)는 풂 */ +.b01-shell.ui-kit__body { + display: flex; + flex-direction: column; + gap: 16px; + min-height: 0; + padding: 16px; +} + +.ui-kit__group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.ui-kit__group-title { + margin: 0; + font-size: var(--text-body-sm); + font-weight: 700; + color: var(--color-text); +} + +.ui-kit__part { + padding: 12px; + background: var(--color-surface-raised); + border: 1px solid var(--color-border); + border-radius: var(--radius-cards, 10px); +} + +.ui-kit__part-head { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 4px 8px; + margin-bottom: 8px; + font-size: var(--text-caption); + color: var(--color-text-secondary); +} + +.ui-kit__part-name { + font-size: var(--text-body-sm); + color: var(--color-text); +} + +.ui-kit__part-flag { + padding: 0 6px; + border: 1px solid var(--color-border); + border-radius: 999px; +} + +.ui-kit__states { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 8px; + align-items: start; +} + +.ui-kit__legend { + position: sticky; + top: 0; + z-index: 1; + padding: 4px 12px; + background: var(--color-bg); + font-size: var(--text-caption); + font-weight: 600; + color: var(--color-text-secondary); +} + +.ui-kit__state { + min-width: 0; + overflow: hidden; +} + +/* 상태가 기본 하나뿐인 견본(표 · 배지 …)은 다섯 칸을 다 씀 */ +.ui-kit__state--wide { + grid-column: 1 / -1; + overflow: visible; +} + +.ui-kit__slot { + display: flex; + align-items: center; + justify-content: center; + height: 32px; + border: 1px dashed var(--color-border); + border-radius: 8px; + font-size: var(--text-caption); + color: var(--color-text-secondary); +} + +.ui-kit__demo-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.ui-kit__sizes { + display: flex; + align-items: flex-end; + gap: 8px; +} + +.ui-kit__size { + display: flex; + align-items: center; + justify-content: center; + min-width: 40px; + border: 1px dashed var(--color-accent); + border-radius: 6px; + font-size: 11px; + color: var(--color-text-secondary); +} diff --git a/ui_template/ui_template_ui_kit.ts b/ui_template/ui_template_ui_kit.ts new file mode 100644 index 000000000..6f66ce6fd --- /dev/null +++ b/ui_template/ui_template_ui_kit.ts @@ -0,0 +1,128 @@ +/* ============================================================================= + * ui_template_ui_kit.ts + * 부품 모음 화면(552-1) — 대시보드 세부 페이지 `#/b01-account?view=ui-kit` · 시스템 관리자만. + * 밝음 · 어두움을 나란히 — 칸마다 iframe(``)에 지금 페이지 CSS 를 옮겨 실제 테마 값으로 그림. + * 부품 줄 = `ui_template_ui_kit_parts.ts` 목록 · 상태 다섯 칸(기본 · 마우스 · 눌림 · 막힘 · 오류) · 견본이 없으면 빈 자리. + * 크기 자 = 단추 · 입력 높이 여덟 단(18 ~ 42). + * ========================================================================== */ + +import { pageFrame } from "../B01_Dashboard/B01_Dashboard_UI_PageKit"; +import { + KIT_GROUP_LABEL, + KIT_PARTS, + KIT_SIZES, + KIT_STATES, + KIT_STATE_LABEL, + type KitGroup, + type KitPart, +} from "./ui_template_ui_kit_parts"; +import "./ui_template_ui_kit.css"; + +type Theme = "light" | "dark"; + +function node( + tag: K, + className: string, + text?: string, +): HTMLElementTagNameMap[K] { + const n = document.createElement(tag); + n.className = className; + if (text !== undefined) n.textContent = text; + return n; +} + +/** 크기 자 — 여덟 단 높이 상자 */ +function sizeRuler(): HTMLElement { + const box = node("section", "ui-kit__group"); + box.append(node("h3", "ui-kit__group-title", "크기 여덟 단 (높이 px · data-size)")); + const line = node("div", "ui-kit__sizes"); + for (const h of KIT_SIZES) { + const cell = node("div", "ui-kit__size", String(h)); + cell.style.height = `${h}px`; + cell.dataset.size = String(h); + line.append(cell); + } + box.append(line); + return box; +} + +function partRow(part: KitPart): HTMLElement { + const item = node("article", "ui-kit__part"); + item.dataset.part = part.key; + const head = node("header", "ui-kit__part-head"); + head.append( + node("strong", "ui-kit__part-name", part.name), + node("span", "ui-kit__part-kinds", part.kinds), + node("code", "ui-kit__part-file", part.file), + ); + if (part.legacy) head.append(node("span", "ui-kit__part-flag", "지금 꼴")); + item.append(head); + const states = node("div", "ui-kit__states"); + const shown = KIT_STATES.map((s) => part.demo?.(s)); // null = 그 상태 없음 · undefined = 견본 없음 + const onlyDefault = shown[0] && shown.slice(1).every((x) => x === null); + KIT_STATES.forEach((s, i) => { + if (onlyDefault && i > 0) return; + const cell = node("div", `ui-kit__state${onlyDefault ? " ui-kit__state--wide" : ""}`); + cell.dataset.state = s; + cell.append(shown[i] ?? node("span", "ui-kit__slot", shown[i] === null ? "-" : "빈 자리")); + states.append(cell); + }); + item.append(states); + return item; +} + +/** 한 테마 칸 내용 — 상태 머리 · 크기 자 · 묶음별 부품 줄 */ +export function renderKit(): HTMLElement { + // 셸 별칭(`--b01-*`)을 받으려고 .b01-shell 로 감쌈 — 배치 규칙은 CSS 에서 풂 + const root = node("div", "b01-shell ui-kit__body"); + const legend = node("div", "ui-kit__states ui-kit__legend"); + for (const s of KIT_STATES) legend.append(node("span", "ui-kit__state", KIT_STATE_LABEL[s])); + root.append(legend, sizeRuler()); + for (const group of Object.keys(KIT_GROUP_LABEL) as KitGroup[]) { + const box = node("section", "ui-kit__group"); + box.dataset.group = group; + box.append(node("h3", "ui-kit__group-title", KIT_GROUP_LABEL[group])); + box.append(...KIT_PARTS.filter((p) => p.group === group).map(partRow)); + root.append(box); + } + return root; +} + +/** 테마 칸 — iframe 에 지금 문서의 CSS 를 옮기고 `` 를 박음 */ +function themePane(theme: Theme): HTMLElement { + const pane = node("div", "ui-kit__pane"); + pane.dataset.theme = theme; + pane.append(node("h3", "ui-kit__pane-title", theme === "light" ? "밝음" : "어두움")); + const frame = node("iframe", "ui-kit__frame"); + frame.title = `부품 모음 — ${theme === "light" ? "밝음" : "어두움"}`; + pane.append(frame); + frame.addEventListener("load", () => { + const doc = frame.contentDocument; + if (!doc) return; + doc.documentElement.dataset.theme = theme; + doc.documentElement.lang = "ko"; + for (const css of document.head.querySelectorAll('style, link[rel="stylesheet"]')) + doc.head.append(css.cloneNode(true)); + doc.body.className = "ui-kit__doc"; + doc.body.append(renderKit()); + const fit = (): void => { + frame.style.height = `${doc.documentElement.scrollHeight}px`; + }; + new ResizeObserver(fit).observe(doc.body); + fit(); + }); + frame.srcdoc = ""; + return pane; +} + +export function mountUiKit(host: HTMLElement): void { + const page = pageFrame( + host, + "부품 모음", + "모든 화면이 다시 쓰는 부품 — 밝음 · 어두움 · 상태별 · 빈 자리는 552-2 ~ 552-4 가 채움", + ); + page.root.classList.add("ui-kit"); + const panes = node("div", "ui-kit__panes"); + panes.append(themePane("light"), themePane("dark")); + page.body.append(panes); +} diff --git a/ui_template/ui_template_ui_kit_parts.ts b/ui_template/ui_template_ui_kit_parts.ts new file mode 100644 index 000000000..1c827737c --- /dev/null +++ b/ui_template/ui_template_ui_kit_parts.ts @@ -0,0 +1,137 @@ +/* ============================================================================= + * 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 · 여덟 단) — 단추 · 아이콘 단추 · 입력 · 고르기 · 분할 고르기 · 탭 같은 값 + * 상태 = 마우스 `: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 { createButton, createInputField, createSelectField } from "./ui_template_elements"; +import { icon } from "./ui_template_icons"; +import { + badge, + dataTable, + emptyState, + rowMenu, + settingCard, + settingRow, + tabBar, + valueText, +} from "../B01_Dashboard/B01_Dashboard_UI_PageKit"; + +/** 상태 줄 다섯 칸 */ +export const KIT_STATES = ["default", "hover", "active", "disabled", "error"] as const; +export type KitState = (typeof KIT_STATES)[number]; +export const KIT_STATE_LABEL: Record = { + 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 = { + 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; + /** 견본이 옮기기 전 PageKit · 옛 공용 부품인지 */ + legacy?: boolean; +} + +/** 상태 굳히기 — 마우스 · 눌림 · 오류 = data-state · 막힘 = disabled */ +export function markState(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 => {}; + +// prettier-ignore +export const KIT_PARTS: KitPart[] = [ + // ── 552-2 기본 ───────────────────────────────────────────── + { key: "button", name: "단추", group: "basic", kinds: "주 · 보조 · 고스트 · 위험 · 위험 고스트 · 글 링크 · 여덟 단", file: "ui_template_button.ts", legacy: true, + demo: (s) => row(markState(createButton({ label: "저장", onClick: noop }), s), markState(createButton({ label: "취소", variant: "ghost", onClick: noop }), s), markState(createButton({ label: "삭제", variant: "danger", onClick: noop }), s)) }, + { key: "icon-button", name: "아이콘 단추", group: "basic", kinds: "고스트 · 선 · 채움 · 여덟 단 · title 필수", file: "ui_template_button.ts" }, + { key: "input", name: "입력", group: "basic", kinds: "글 · 수 · 단위 붙음 · 찾기 · 날짜 · 여덟 단", file: "ui_template_input.ts", legacy: true, + demo: (s) => { const f = createInputField({ placeholder: "이름" }); markState(f.input, s); return f.root; } }, + { key: "select", name: "고르기", group: "basic", kinds: "한 개 · 여덟 단", file: "ui_template_input.ts", legacy: true, + demo: (s) => { const f = createSelectField({ options: [{ value: "a", text: "임도" }, { value: "b", text: "예정노선" }] }); markState(f.select, s); return f.root; } }, + { key: "check", name: "체크 · 라디오", group: "basic", kinds: "체크 · 라디오 · 글 붙음", file: "ui_template_checkbox.ts" }, + { key: "toggle", name: "켜기", group: "basic", kinds: "켬 · 끔 · 글 붙음", file: "ui_template_toggle.ts" }, + { key: "segment", name: "분할 고르기", group: "basic", kinds: "글 · 아이콘 · 여덟 단", file: "ui_template_segment.ts" }, + { key: "tabs", name: "탭", group: "basic", kinds: "밑줄 탭(한 꼴) · 수 배지 · 문서 탭은 꼴 그대로", file: "ui_template_tabs.ts", legacy: true, + demo: (s) => (s === "default" ? tabBar([{ key: "a", label: "개요" }, { key: "b", label: "구성원" }, { key: "c", label: "기록" }], "a", noop).root : null) }, + { key: "badge", name: "배지", group: "basic", kinds: "상태 · 역할 · 종류 · 수 · 램프(✓ 같이)", file: "ui_template_badge.ts", legacy: true, + demo: (s) => (s === "default" ? row(badge("진행 중", "accent"), badge("완료", "success"), badge("멈춤", "warning"), badge("오류", "danger"), badge("임시", "neutral")) : null) }, + { key: "tooltip", name: "툴팁", group: "basic", kinds: "마우스 글 · 값 풍선", file: "ui_template_hover_info.ts" }, + { key: "icon", name: "아이콘", group: "basic", kinds: "Tabler 선 · 16 · 18 · 24 · 32", file: "ui_template_icons.ts", legacy: true, + demo: (s) => (s === "default" ? row(icon("home"), icon("folder"), icon("route"), icon("search"), icon("bell"), icon("users")) : null) }, + // ── 552-3 묶음 ───────────────────────────────────────────── + { key: "card", name: "카드", group: "compound", kinds: "선 카드 · 머리 카드 · 고를 수 있는 카드", file: "ui_template_card.ts" }, + { key: "collapsible", name: "접이 카드", group: "compound", kinds: "패널 카드 · 구역 · 캐럿 셰브런", file: "ui_template_collapsible.ts" }, + { key: "panel-head", name: "패널 머리", group: "compound", kinds: "제목 · 부제 · 오른쪽 단추", file: "ui_template_panel_head.ts" }, + { key: "table", name: "표", group: "compound", kinds: "데이터표(28 · 밀도) · 일반표(40) · ⋯ 칸 · 한글 배지 · 빈 줄", file: "ui_template_table.ts", legacy: true, + demo: (s) => (s === "default" ? dataTable([{ label: "이름" }, { label: "상태", width: "90px" }, { label: "연장", width: "80px", align: "end" }, { label: "", width: "44px" }], [["임도 1 노선", badge("진행 중", "accent"), "1,080 m", rowMenu([{ label: "열기", onClick: noop }])], ["임도 2 노선", badge("완료", "success"), "640 m", rowMenu([{ label: "열기", onClick: noop }])]]) : null) }, + { key: "row-menu", name: "⋯ · 오른쪽 메뉴", group: "compound", kinds: "줄 끝 ⋯ · 오른쪽 누름 메뉴 · 위험 항목", file: "ui_template_context_menu.ts", legacy: true, + demo: (s) => (s === "default" ? rowMenu([{ label: "이름 바꾸기", onClick: noop }, { label: "삭제", danger: true, onClick: noop }]) : null) }, + { key: "empty", name: "빈 상태", group: "compound", kinds: "선 아이콘 · 한 줄 · 주 단추 · 사용법", file: "ui_template_empty.ts", legacy: true, + demo: (s) => (s === "default" ? emptyState({ icon: "folder", text: "아직 프로젝트가 없음" }) : null) }, + { key: "modal", name: "모달 · 확인창", group: "compound", kinds: "보통 · 넓음 · 확인 · 위험 확인", file: "ui_template_modal.ts" }, + { key: "drawer", name: "오른쪽 서랍", group: "compound", kinds: "상세 · 설정", file: "ui_template_side_panels.ts" }, + { key: "toast", name: "알림", group: "compound", kinds: "토스트 4꼴 · 띠 알림 · 로딩 가림막", file: "ui_template_toast.ts" }, + { key: "toolbar", name: "도구줄", group: "compound", kinds: "단추 묶음 · 나눔선 · 찾기 · 고르기", file: "ui_template_toolbar.ts" }, + { key: "steps", name: "단계 줄", group: "compound", kinds: "가로 막대 · 세로 목록 · compact", file: "ui_template_workflow_layout.ts" }, + { key: "stat", name: "숫자 카드", group: "compound", kinds: "값 · 단위 · 변화", file: "ui_template_stat.ts" }, + { key: "progress", name: "진행 막대", group: "compound", kinds: "막대 · 원", file: "ui_template_progress.ts" }, + { key: "mini-chart", name: "작은 그래프", group: "compound", kinds: "막대 · 원 · 선", file: "ui_template_elements_chart.ts" }, + { key: "setting-row", name: "설정 줄", group: "compound", kinds: "제목 · 설명 · 오른쪽 값 · 조작", file: "ui_template_setting_row.ts", legacy: true, + demo: (s) => (s === "default" ? settingCard([settingRow("회사 이름", "보고서 머리에 들어감", valueText("산림설계")), settingRow("단위", "", valueText(null))]) : null) }, + { key: "drop", name: "파일 놓기 칸", group: "compound", kinds: "끌어 놓기 · 고르기 단추 · 진행", file: "ui_template_drop.ts" }, + // ── 552-4 틀 ─────────────────────────────────────────────── + { key: "work-frame", name: "작업 화면 틀", group: "frame", kinds: "왼쪽 패널 · 본문 · 아래 단추 줄 · [?][<][>] · 머리 줄 없음 · ☰ 서랍", file: "ui_template_overlay.ts" }, + { key: "page-frame", name: "일반 화면 틀", group: "frame", kinds: "페이지 머리(제목 · 설명 · 주 단추) · 몸통", file: "ui_template_general_layout.ts" }, + { key: "public-frame", name: "공개 화면 틀", group: "frame", kinds: "공개 머리 · 본문 · 바닥글", file: "ui_template_public_layout.ts" }, +];