merge(dev): 552-2 단추 · 552-4 틀 받음 — 부품 모음 목록 = 양쪽 살림(묶음 열다섯 · 틀 아홉) · emptyState = 빈 상태 부품 · 아이콘 = ui_template_icons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,37 +2,23 @@
|
||||
* B01_Dashboard_UI_PageKit.ts
|
||||
* 대시보드 세부 페이지 공용 조각(549) — 페이지 머리 · 표 · 줄 끝 ⋯ · 배지 · 탭 · 빈 상태.
|
||||
* 색 · 간격 = 셸 변수(`--b01-*`) 만 · 모양은 `B01_Dashboard_UI_Style_Pages.css`.
|
||||
* 아이콘 중 `ui_template_icons` 에 없는 것(⋯ · + · 휴지통 · 상자)만 여기 · Tabler Icons(MIT) 선 모양.
|
||||
* 아이콘 = `ui_template_icons`(552-2 에 여섯을 옮김).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createTag, showConfirmDialog } from "@ui/ui_template_elements";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { type IconName } from "@ui/ui_template_icons";
|
||||
import { icon, type IconName } from "@ui/ui_template_icons";
|
||||
import type { BadgeTone } from "./logic/B01_Dashboard_Logic_Pages";
|
||||
import "./B01_Dashboard_UI_Style_Pages.css";
|
||||
|
||||
/** Tabler Icons(MIT · https://tabler.io/icons) — `ui_template_icons` 와 같은 24 × 24 · 선 2 */
|
||||
const KIT_PATHS = {
|
||||
dots: "M5 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0|M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0|M19 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0",
|
||||
plus: "M12 5l0 14|M5 12l14 0",
|
||||
pencil: "M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4|M13.5 6.5l4 4",
|
||||
upload: "M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2|M7 9l5 -5l5 5|M12 4l0 12",
|
||||
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",
|
||||
} as const;
|
||||
export type KitIconName = keyof typeof KIT_PATHS;
|
||||
/** 아이콘 여섯 — 552-2 에 `ui_template_icons` 로 옮김(같은 Tabler 선) · 이름 · 기본 크기 16 그대로 */
|
||||
export type KitIconName = Extract<
|
||||
IconName,
|
||||
"dots" | "plus" | "pencil" | "upload" | "mail" | "inbox"
|
||||
>;
|
||||
|
||||
export function kitIcon(name: KitIconName, size = 16): HTMLSpanElement {
|
||||
const span = document.createElement("span");
|
||||
span.className = "ui-icon";
|
||||
span.setAttribute("aria-hidden", "true");
|
||||
const paths = KIT_PATHS[name]
|
||||
.split("|")
|
||||
.map((d) => `<path d="${d}"/>`)
|
||||
.join("");
|
||||
span.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
|
||||
return span;
|
||||
return icon(name, size);
|
||||
}
|
||||
|
||||
function node<K extends keyof HTMLElementTagNameMap>(
|
||||
@@ -195,11 +181,9 @@ export interface EmptyOptions {
|
||||
onGuide?: () => void;
|
||||
}
|
||||
|
||||
/** 빈 상태 — 부품 「빈 상태」(ui_template_empty.ts) · ⋯ 같은 PageKit 아이콘만 여기서 그림 */
|
||||
/** 빈 상태 — 부품 「빈 상태」(ui_template_empty.ts) */
|
||||
export function emptyState(opts: EmptyOptions): HTMLElement {
|
||||
const glyph =
|
||||
opts.icon in KIT_PATHS ? kitIcon(opts.icon as KitIconName, 32) : (opts.icon as IconName);
|
||||
return createEmpty({ ...opts, icon: glyph });
|
||||
return createEmpty(opts);
|
||||
}
|
||||
|
||||
export interface TabsHandle {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""552-2 부품 「단추」 · 「아이콘 단추」 · 「아이콘」 · 크기 여덟 단 지킴 시험 — 화면 확인은 ORCA.
|
||||
|
||||
· 크기 = data-size 여덟 단(18 ~ 42) · 단마다 높이 · 글자 · 여백 · 아이콘 · 둥글기 · 옛 sm = 18
|
||||
· 단추 높이 = 글 한 줄 + 여백 + 테두리 = 단(높이를 박지 않음 — 페이지 여백 덮어쓰기 그대로)
|
||||
· 상태 = :hover · :active 를 data-state 로도 받음 · 아이콘 단추 = title + aria-label
|
||||
· 색 = 공통 값만(날 색 · --b01-* · --draw-* 없음) · 옛 클래스 이름 그대로 · 부르는 길(ui_template_elements) 그대로
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
UI = ROOT / "ui_template"
|
||||
SIZES = [18, 20, 22, 24, 28, 32, 36, 42]
|
||||
|
||||
|
||||
def _text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _rungs(css: str) -> dict[int, dict[str, str]]:
|
||||
out = {}
|
||||
for size, body in re.findall(r':where\(\[data-size="(\d+)"\]\) \{([^}]*)\}', css):
|
||||
out[int(size)] = dict(re.findall(r"--ui-size-([\w-]+):\s*([^;]+);", body))
|
||||
return out
|
||||
|
||||
|
||||
def test_여덟_단():
|
||||
src = _text(UI / "ui_template_button.ts")
|
||||
assert "UI_SIZES = [18, 20, 22, 24, 28, 32, 36, 42] as const" in src
|
||||
rungs = _rungs(_text(UI / "ui_template_button.css"))
|
||||
assert sorted(rungs) == SIZES
|
||||
for size, v in rungs.items():
|
||||
assert v["h"] == f"{size}px"
|
||||
assert set(v) == {"h", "font", "px", "icon", "radius", "gap"}
|
||||
# 글자 짝 — 18 ~ 24 = 12 · 28 이상 = 14
|
||||
assert v["font"] == ("var(--text-caption)" if size <= 24 else "var(--text-body-sm)")
|
||||
# 아이콘 크기 — TS 짝과 같음
|
||||
px = dict(re.findall(r"(\d+): (\d+)", re.search(r"ICON_PX[^{]*\{([^}]*)\}", src).group(1)))
|
||||
assert {int(k): f"{v}px" for k, v in px.items()} == {s: rungs[s]["icon"] for s in SIZES}
|
||||
|
||||
|
||||
def test_단추_높이_식():
|
||||
css = _text(UI / "ui_template_button.css")
|
||||
rule = re.search(r"\.ui-btn\[data-size\] \{([^}]*)\}", css).group(1)
|
||||
assert "calc((var(--ui-size-h) - 2px - var(--ui-size-font)) / 2) var(--ui-size-px)" in rule
|
||||
assert "height" not in rule # 높이를 박지 않음
|
||||
# 아이콘은 글 높이 안 — 단 높이가 아이콘 때문에 늘지 않음
|
||||
assert "margin-block: calc((var(--ui-size-font) - var(--ui-size-icon)) / 2)" in css
|
||||
# 32 단 = 옛 기본(여백 8 · 글 14) · 18 단 = 옛 sm(여백 2 · 글 12)
|
||||
rungs = _rungs(css)
|
||||
assert (32 - 2 - 14) / 2 == 8 and rungs[32]["px"] == "16px"
|
||||
assert (18 - 2 - 12) / 2 == 2 and rungs[18]["px"] == "8px" and rungs[18]["radius"] == "4px"
|
||||
|
||||
|
||||
def test_옛_이름_부르는_길():
|
||||
src = _text(UI / "ui_template_button.ts")
|
||||
assert 'opts.size === "sm" ? 18' in src
|
||||
assert 'opts.size === "sm" ? " ui-btn--sm" : ""' in src # 옛 클래스 그대로
|
||||
for cls in ("ui-btn ui-btn--", "ui-btn__icon", "ui-btn__label", "ui-icon-btn ui-icon-btn--"):
|
||||
assert cls in src, cls
|
||||
elements = _text(UI / "ui_template_elements.ts")
|
||||
assert 'export * from "./ui_template_button";' in elements
|
||||
assert "export function createButton" not in elements # 한 벌
|
||||
|
||||
|
||||
def test_상태():
|
||||
styles = _text(UI / "ui_template_elements_styles.ts")
|
||||
for variant in ("filled", "ghost", "pill", "danger", "danger-ghost", "glass"):
|
||||
assert f'.ui-btn--{variant}:is(:hover, [data-state="hover"]):not(:disabled)' in styles
|
||||
assert '.ui-btn:is(:active, [data-state="active"]):not(:disabled)' in styles
|
||||
assert "--color-primary-hover" in styles
|
||||
css = _text(UI / "ui_template_button.css")
|
||||
assert '.ui-icon-btn:is(:hover, [data-state="hover"]):not(:disabled)' in css
|
||||
assert '.ui-icon-btn:is(:active, [data-state="active"]):not(:disabled)' in css
|
||||
assert '.ui-icon-btn[aria-pressed="true"]' in css and ".ui-icon-btn:disabled" in css
|
||||
src = _text(UI / "ui_template_button.ts")
|
||||
assert 'title: opts.title, "aria-label": opts.title' in src
|
||||
|
||||
|
||||
def test_날_색_없음():
|
||||
for name in ("ui_template_button.css", "ui_template_icons.css"):
|
||||
css = _text(UI / name)
|
||||
assert not re.search(r"#[0-9a-fA-F]{3,8}\b|rgba?\(", css), name
|
||||
assert "--b01-" not in css and "--draw-" not in css and "font-family" not in css
|
||||
for name in ("ui_template_button.ts", "ui_template_button.css", "ui_template_icons.ts"):
|
||||
assert len(_text(UI / name).splitlines()) <= 700
|
||||
|
||||
|
||||
def test_아이콘():
|
||||
src = _text(UI / "ui_template_icons.ts")
|
||||
names = re.findall(r"^ (\w+):", src[src.index("const PATHS") : src.index("} as const")], re.M)
|
||||
assert len(names) == len(set(names)) >= 60
|
||||
for n in ("settings", "x", "menu", "undo", "redo", "mapPin", "dots", "plus", "inbox"):
|
||||
assert n in names, n
|
||||
assert not re.search(
|
||||
r"[\U0001F300-\U0001FAFF☀-➿]", src[src.index("const PATHS") :]
|
||||
) # 그림 이모지 없음
|
||||
kit = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_UI_PageKit.ts")
|
||||
assert "KIT_PATHS" not in kit and "return icon(name, size);" in kit # 한 벌
|
||||
@@ -13,7 +13,7 @@ def test_empty():
|
||||
)
|
||||
b01 = ROOT / "B01_Dashboard"
|
||||
kit = (b01 / "B01_Dashboard_UI_PageKit.ts").read_text(encoding="utf-8")
|
||||
assert "return createEmpty({ ...opts, icon: glyph });" in kit
|
||||
assert "return createEmpty(opts);" in kit
|
||||
pages_css = (b01 / "B01_Dashboard_UI_Style_Pages.css").read_text(encoding="utf-8")
|
||||
assert ".b01-pg-empty" not in pages_css
|
||||
assert "사용법 보기" in _text("ui_template_empty.ts")
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""552-4 틀 부품 아홉 지킴 시험 — 화면 확인은 부품 모음(`#/b01-account?view=ui-kit`) · ORCA.
|
||||
|
||||
부품마다(이름 · data-size · 상태 규칙 · 날 색 없음):
|
||||
· 작업 화면 틀(overlay) · 일반 화면 틀(general_layout) · 공개 화면 틀(public_layout)
|
||||
· 떠 있는 판 · 오른쪽 서랍(side_panels) · 도구줄 · 단계 줄(workflow_layout) · 패널 머리 · 목록 줄
|
||||
· 부품 모음 목록 = 아홉 모두 `frame` 묶음 · 견본(demo) 있음 · 파일 이름 = 552-1 목록 그대로
|
||||
· 옛 공용 이름(`ui-sidebar-actions` · `ui-sidebar-row` · `ui-workflow-layout__step` …)은 그대로 냄
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
UI = ROOT / "ui_template"
|
||||
|
||||
# 부품 열쇠 → (ts 파일, css 파일, 만들기 함수, 낼 클래스, 높이 단)
|
||||
PARTS = {
|
||||
"work-frame": ("ui_template_overlay", "createWorkflowOverlays", "ui-workflow-overlay", ()),
|
||||
"page-frame": ("ui_template_general_layout", "createPageFrame", "ui-page-frame", ()),
|
||||
"public-frame": ("ui_template_public_layout", "createPublicFrame", "ui-public-frame", ()),
|
||||
"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)),
|
||||
}
|
||||
|
||||
RAW_COLOR = re.compile(r"#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(|var\(--(?:b01|draw)-")
|
||||
|
||||
|
||||
def _text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _strip_comments(css: str) -> str:
|
||||
return re.sub(r"/\*[\s\S]*?\*/", "", css)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", PARTS)
|
||||
def test_머리_이름_크기(key):
|
||||
stem, maker, cls, sizes = PARTS[key]
|
||||
ts = _text(UI / f"{stem}.ts")
|
||||
head = ts.split("*/", 1)[0]
|
||||
# 규칙 8 — 머리에 무엇 · 꼴 · 크기 · 상태 · 쓰는 법 · 대신하는 옛 변종
|
||||
for word in ("꼴", "크기", "상태", "쓰는 법", "대신하는 옛 변종"):
|
||||
assert word in head, f"{stem} 머리에 {word} 없음"
|
||||
assert re.search(rf"export function {maker}\b", ts), maker
|
||||
assert cls in ts
|
||||
css = _strip_comments(_text(UI / f"{stem}.css"))
|
||||
for size in sizes:
|
||||
assert f'[data-size="{size}"]' in css or f"min-height: {size}px" in css, (stem, size)
|
||||
assert len(ts.splitlines()) <= 700 and len(css.splitlines()) <= 700
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", PARTS)
|
||||
def test_날_색_없음(key):
|
||||
stem = PARTS[key][0]
|
||||
css = _strip_comments(_text(UI / f"{stem}.css"))
|
||||
assert not RAW_COLOR.search(css), (stem, RAW_COLOR.search(css))
|
||||
assert "font-family" not in css # 글꼴 = --font-ui 상속
|
||||
|
||||
|
||||
def test_상태_규칙():
|
||||
# 마우스 · 눌림을 data-state 로 굳혀 보일 수 있음
|
||||
for stem in ("ui_template_list_row", "ui_template_workflow_layout"):
|
||||
css = _text(UI / f"{stem}.css")
|
||||
assert ':is(:hover, [data-state="hover"])' in css, stem
|
||||
row = _text(UI / "ui_template_list_row.css")
|
||||
assert ".ui-sidebar-row.is-active" in row and '[aria-disabled="true"]' in row
|
||||
|
||||
|
||||
def test_작업_화면_틀_겉():
|
||||
css = _strip_comments(_text(UI / "ui_template_overlay.css"))
|
||||
title = re.search(r"\.ui-workflow-overlay__panel--title \{([^}]*)\}", css).group(1)
|
||||
# 흰 판 + 1 px 선 · 그림자 없음(요구 375 정함 7)
|
||||
assert "box-shadow: none" in title and "border-right-color: var(--color-border)" in title
|
||||
# 손잡이는 강조색 그대로(2026-08-04 사용자 지시)
|
||||
assert "border: 1px solid var(--panel-handle-border" in css
|
||||
assert "background: var(--color-scrim)" in css
|
||||
# 옛 이름 그대로 — 아래 단추 줄 · [?][<][>]
|
||||
for name in (".ui-sidebar-actions", ".ui-workflow-overlay__handle--side"):
|
||||
assert name in css
|
||||
ts = _text(UI / "ui_template_overlay.ts")
|
||||
assert 'import "./ui_template_list_row.css"' in ts # 옛 목록 줄 겉도 함께
|
||||
|
||||
|
||||
def test_부품_모음_목록():
|
||||
src = _text(UI / "ui_template_ui_kit_parts.ts")
|
||||
rows = dict(re.findall(r'\{ key: "([^"]+)", name: "[^"]+", group: "(\w+)"', src))
|
||||
lines = src.splitlines()
|
||||
for key, (stem, *_rest) in PARTS.items():
|
||||
assert rows.get(key) == "frame", key
|
||||
i = next(i for i, x in enumerate(lines) if f'key: "{key}"' in x)
|
||||
assert f'file: "{stem}.ts"' in lines[i], key
|
||||
assert "demo:" in lines[i] + lines[i + 1], key
|
||||
@@ -0,0 +1,152 @@
|
||||
/* =============================================================================
|
||||
* ui_template_button.css — 크기 여덟 단 · 단추 단 맞춤 · 글 링크 · 아이콘 단추(552-2)
|
||||
* 단 값(--ui-size-*)은 data-size 를 단 요소만 씀 — 입력 · 고르기 · 분할 · 탭도 같은 값을 읽음.
|
||||
* 단추 기본 꼴 · 색 · 마우스는 `ui_template_elements_styles.ts`(옛 자리 · 페이지 덮어쓰기 차례 그대로).
|
||||
* ========================================================================== */
|
||||
|
||||
:where([data-size="18"]) {
|
||||
--ui-size-h: 18px;
|
||||
--ui-size-font: var(--text-caption);
|
||||
--ui-size-px: 8px;
|
||||
--ui-size-icon: 12px;
|
||||
--ui-size-radius: 4px;
|
||||
--ui-size-gap: 4px;
|
||||
}
|
||||
:where([data-size="20"]) {
|
||||
--ui-size-h: 20px;
|
||||
--ui-size-font: var(--text-caption);
|
||||
--ui-size-px: 8px;
|
||||
--ui-size-icon: 14px;
|
||||
--ui-size-radius: 4px;
|
||||
--ui-size-gap: 4px;
|
||||
}
|
||||
:where([data-size="22"]) {
|
||||
--ui-size-h: 22px;
|
||||
--ui-size-font: var(--text-caption);
|
||||
--ui-size-px: 8px;
|
||||
--ui-size-icon: 14px;
|
||||
--ui-size-radius: 4px;
|
||||
--ui-size-gap: 4px;
|
||||
}
|
||||
:where([data-size="24"]) {
|
||||
--ui-size-h: 24px;
|
||||
--ui-size-font: var(--text-caption);
|
||||
--ui-size-px: 10px;
|
||||
--ui-size-icon: 14px;
|
||||
--ui-size-radius: 6px;
|
||||
--ui-size-gap: 4px;
|
||||
}
|
||||
:where([data-size="28"]) {
|
||||
--ui-size-h: 28px;
|
||||
--ui-size-font: var(--text-body-sm);
|
||||
--ui-size-px: 12px;
|
||||
--ui-size-icon: 16px;
|
||||
--ui-size-radius: var(--radius-buttons);
|
||||
--ui-size-gap: 6px;
|
||||
}
|
||||
:where([data-size="32"]) {
|
||||
--ui-size-h: 32px;
|
||||
--ui-size-font: var(--text-body-sm);
|
||||
--ui-size-px: 16px;
|
||||
--ui-size-icon: 16px;
|
||||
--ui-size-radius: var(--radius-buttons);
|
||||
--ui-size-gap: 6px;
|
||||
}
|
||||
:where([data-size="36"]) {
|
||||
--ui-size-h: 36px;
|
||||
--ui-size-font: var(--text-body-sm);
|
||||
--ui-size-px: 16px;
|
||||
--ui-size-icon: 18px;
|
||||
--ui-size-radius: var(--radius-buttons);
|
||||
--ui-size-gap: 8px;
|
||||
}
|
||||
:where([data-size="42"]) {
|
||||
--ui-size-h: 42px;
|
||||
--ui-size-font: var(--text-body-sm);
|
||||
--ui-size-px: 20px;
|
||||
--ui-size-icon: 20px;
|
||||
--ui-size-radius: var(--radius-buttons);
|
||||
--ui-size-gap: 8px;
|
||||
}
|
||||
|
||||
/* 단추 — 높이 = 글 한 줄 + 위아래 여백 + 테두리(아이콘은 글 높이 안에 맞춤) · 높이를 박지 않아 페이지 여백 덮어쓰기가 옛날처럼 먹음 */
|
||||
.ui-btn[data-size] {
|
||||
gap: var(--ui-size-gap);
|
||||
padding: calc((var(--ui-size-h) - 2px - var(--ui-size-font)) / 2) var(--ui-size-px);
|
||||
font-size: var(--ui-size-font);
|
||||
border-radius: var(--ui-size-radius);
|
||||
}
|
||||
.ui-btn[data-size] > .ui-btn__icon {
|
||||
width: var(--ui-size-icon);
|
||||
height: var(--ui-size-icon);
|
||||
margin-block: calc((var(--ui-size-font) - var(--ui-size-icon)) / 2);
|
||||
}
|
||||
.ui-btn[data-size] > .ui-btn__icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 글 링크 — 테두리 · 바탕 없음 · 강조 글 · 밑줄 */
|
||||
.ui-btn.ui-btn--link {
|
||||
padding-inline: 0;
|
||||
background: none;
|
||||
color: var(--color-accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ui-btn--link:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
/* 아이콘 단추 — 정사각 · 단 높이 = 너비 */
|
||||
.ui-icon-btn {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
box-sizing: border-box;
|
||||
width: var(--ui-size-h);
|
||||
height: var(--ui-size-h);
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--ui-size-radius);
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font: inherit;
|
||||
font-size: var(--ui-size-font);
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
border-color var(--transition-fast),
|
||||
color var(--transition-fast);
|
||||
}
|
||||
.ui-icon-btn svg {
|
||||
width: var(--ui-size-icon);
|
||||
height: var(--ui-size-icon);
|
||||
}
|
||||
.ui-icon-btn--outline {
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
.ui-icon-btn--filled {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
.ui-icon-btn:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background: var(--color-hover);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.ui-icon-btn--filled:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background: var(--color-primary-hover);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
.ui-icon-btn[aria-pressed="true"] {
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.ui-icon-btn:is(:active, [data-state="active"]):not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
.ui-icon-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/* =============================================================================
|
||||
* ui_template_button.ts
|
||||
* 부품 「단추」 · 「아이콘 단추」 · 크기 여덟 단(552-2 · 부품 모음 `#/b01-account?view=ui-kit`)
|
||||
*
|
||||
* 꼴 = 단추: 주(filled) · 보조(ghost) · 고스트 글(pill) · 위험(danger) · 위험 고스트(danger-ghost) · 유리(glass · 그림 위) · 글 링크(link)
|
||||
* 아이콘 단추: 고스트(ghost · 기본) · 선(outline) · 채움(filled) · 켜짐 = aria-pressed
|
||||
* 크기 = `data-size="18|20|22|24|28|32|36|42"`(높이 px) — 단추 · 아이콘 단추 · 입력 · 고르기 · 분할 · 탭 같은 값
|
||||
* 옛 `size: "sm"` = 18 · 크기를 안 주면 data-size 없이 옛 꼴(높이 32 · 아이콘 붙으면 34) 그대로
|
||||
* 상태 = 마우스 `:hover` · 눌림 `:active` · 막힘 `disabled` — CSS 는 `[data-state]` 로도 받아 부품 모음이 굳혀 보임
|
||||
* 쓰기 = `createButton({ label: "저장", size: 24, onClick })` · `createIconButton({ icon: "settings", title: "설정", size: 24 })`
|
||||
* 글자 기호 단추(▲ ▼ ✕ ?)는 `glyph: "▲"` — 시험이 글자로 잡음
|
||||
* 대신 = 페이지마다 만든 16 ~ 26 짜리 작은 단추 · `b01-shell__icon-btn` · `b01-pg-menu__btn` · `b05-profile__zoom-btn` …(목록 4장)
|
||||
* 클래스 = `ui-btn ui-btn--<꼴>`(+ 옛 `ui-btn--sm`) · `ui-btn__icon` · `ui-btn__label` · `ui-icon-btn ui-icon-btn--<꼴>` — 시험 · 가이드 촬영이 잡음
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "./ui_template_elements_base";
|
||||
import { icon, type IconName } from "./ui_template_icons";
|
||||
import "./ui_template_button.css";
|
||||
|
||||
/** 크기 여덟 단(높이 px · 요구 377 · 378) */
|
||||
export const UI_SIZES = [18, 20, 22, 24, 28, 32, 36, 42] as const;
|
||||
export type UiSize = (typeof UI_SIZES)[number];
|
||||
|
||||
/** 단마다 아이콘 크기(`ui_template_button.css` 의 `--ui-size-icon` 과 같은 값) */
|
||||
const ICON_PX: Record<UiSize, number> = {
|
||||
18: 12,
|
||||
20: 14,
|
||||
22: 14,
|
||||
24: 14,
|
||||
28: 16,
|
||||
32: 16,
|
||||
36: 18,
|
||||
42: 20,
|
||||
};
|
||||
|
||||
/** 요소에 크기 단을 붙임 */
|
||||
export function setUiSize<T extends HTMLElement>(node: T, size: UiSize): T {
|
||||
node.dataset.size = String(size);
|
||||
return node;
|
||||
}
|
||||
|
||||
export type ButtonVariant =
|
||||
"filled" | "ghost" | "pill" | "danger" | "danger-ghost" | "glass" | "link";
|
||||
/** 크기 — 단(18 ~ 42) · 옛 "sm" = 18 · 옛 "md" = 단 없이 옛 꼴 */
|
||||
export type ButtonSize = "md" | "sm" | UiSize;
|
||||
|
||||
export interface ButtonOptions {
|
||||
/** 표시 텍스트 (i18n 결과 문자열을 전달) */
|
||||
label: string;
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
/** 이벤트 핸들러 (frontend.md §4: on[페이지]_[기능]_[액션] 명명) */
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
disabled?: boolean;
|
||||
type?: "button" | "submit" | "reset";
|
||||
/** 앞쪽 아이콘 요소 (선택) — 이름이면 단 크기 아이콘 */
|
||||
iconStart?: HTMLElement | IconName;
|
||||
}
|
||||
|
||||
export function createButton(opts: ButtonOptions): HTMLButtonElement {
|
||||
const variant = opts.variant ?? "filled";
|
||||
const size: UiSize | null =
|
||||
opts.size === "sm" ? 18 : opts.size === "md" || opts.size === undefined ? null : opts.size;
|
||||
const btn = el("button", {
|
||||
className: `ui-btn ui-btn--${variant}${opts.size === "sm" ? " ui-btn--sm" : ""}`,
|
||||
attrs: { type: opts.type ?? "button" },
|
||||
});
|
||||
if (size) setUiSize(btn, size);
|
||||
if (opts.iconStart) {
|
||||
const mark =
|
||||
typeof opts.iconStart === "string"
|
||||
? icon(opts.iconStart, size ? ICON_PX[size] : 16)
|
||||
: opts.iconStart;
|
||||
mark.classList.add("ui-btn__icon");
|
||||
btn.append(mark);
|
||||
}
|
||||
btn.append(el("span", { className: "ui-btn__label", text: opts.label }));
|
||||
if (opts.disabled) btn.disabled = true;
|
||||
if (opts.onClick) btn.addEventListener("click", opts.onClick);
|
||||
return btn;
|
||||
}
|
||||
|
||||
export type IconButtonVariant = "ghost" | "outline" | "filled";
|
||||
|
||||
export interface IconButtonOptions {
|
||||
/** 아이콘 이름 — 없으면 glyph */
|
||||
icon?: IconName;
|
||||
/** 글자 기호(▲ ▼ ✕ ? …) — 시험이 글자로 잡는 단추 */
|
||||
glyph?: string;
|
||||
/** 마우스 글 · 보조기기 이름(필수) */
|
||||
title: string;
|
||||
variant?: IconButtonVariant;
|
||||
/** 기본 32 */
|
||||
size?: UiSize;
|
||||
/** 켜고 끄는 단추면 지금 값(aria-pressed) */
|
||||
pressed?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export function createIconButton(opts: IconButtonOptions): HTMLButtonElement {
|
||||
const size = opts.size ?? 32;
|
||||
const btn = el("button", {
|
||||
className: `ui-icon-btn ui-icon-btn--${opts.variant ?? "ghost"}`,
|
||||
attrs: { type: "button", title: opts.title, "aria-label": opts.title },
|
||||
});
|
||||
setUiSize(btn, size);
|
||||
btn.append(opts.icon ? icon(opts.icon, ICON_PX[size]) : (opts.glyph ?? ""));
|
||||
if (opts.pressed !== undefined) btn.setAttribute("aria-pressed", String(opts.pressed));
|
||||
if (opts.disabled) btn.disabled = true;
|
||||
if (opts.onClick) btn.addEventListener("click", opts.onClick);
|
||||
return btn;
|
||||
}
|
||||
@@ -10,52 +10,21 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "./ui_template_elements_base";
|
||||
import { createButton } from "./ui_template_button";
|
||||
|
||||
// 차트·기본 스타일 조각은 파일이 700줄을 넘어 떼어냈다(2026-09-04).
|
||||
// 여기서 그대로 다시 내보내 호출부의 import 경로는 불변이다.
|
||||
export * from "./ui_template_elements_chart";
|
||||
export * from "./ui_template_elements_styles";
|
||||
export { el } from "./ui_template_elements_base";
|
||||
export * from "./ui_template_button";
|
||||
|
||||
/** 요소 생성 + 속성/클래스/자식 일괄 설정 헬퍼 */
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 1. 버튼 (Button) — design.md: Filled Brand / Ghost Outlined / Pill Nav
|
||||
* 1. 버튼 — 부품 「단추」 · 「아이콘 단추」는 `ui_template_button.ts`(552-2) · 부르는 길은 그대로
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
export type ButtonVariant = "filled" | "ghost" | "pill" | "danger" | "danger-ghost" | "glass";
|
||||
export type ButtonSize = "md" | "sm";
|
||||
|
||||
export interface ButtonOptions {
|
||||
/** 표시 텍스트 (i18n 결과 문자열을 전달) */
|
||||
label: string;
|
||||
variant?: ButtonVariant;
|
||||
/** 크기 — 기본 md · sm = 표 칸·카드 머리용 작은 단추(글자 캡션 크기) */
|
||||
size?: ButtonSize;
|
||||
/** 이벤트 핸들러 (frontend.md §4: on[페이지]_[기능]_[액션] 명명) */
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
disabled?: boolean;
|
||||
type?: "button" | "submit" | "reset";
|
||||
/** 앞쪽 아이콘 요소 (선택) */
|
||||
iconStart?: HTMLElement;
|
||||
}
|
||||
|
||||
export function createButton(opts: ButtonOptions): HTMLButtonElement {
|
||||
const variant = opts.variant ?? "filled";
|
||||
const btn = el("button", {
|
||||
className: `ui-btn ui-btn--${variant}${opts.size === "sm" ? " ui-btn--sm" : ""}`,
|
||||
attrs: { type: opts.type ?? "button" },
|
||||
});
|
||||
if (opts.iconStart) {
|
||||
opts.iconStart.classList.add("ui-btn__icon");
|
||||
btn.append(opts.iconStart);
|
||||
}
|
||||
btn.append(el("span", { className: "ui-btn__label", text: opts.label }));
|
||||
if (opts.disabled) btn.disabled = true;
|
||||
if (opts.onClick) btn.addEventListener("click", opts.onClick);
|
||||
return btn;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 2. 입력 필드 (Input Field) — design.md: 1px mist border, 8px radius, focus ring
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
@@ -13,7 +13,7 @@ const BASE_STYLE_ID = "ui-template-elements-style";
|
||||
const BASE_CSS = `
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
/* --- Button --- */
|
||||
/* --- Button — 부품 「단추」(552-2 · ui_template_button.ts) 기본 꼴 · 색 · 상태 · 크기 단은 ui_template_button.css --- */
|
||||
.ui-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -31,6 +31,8 @@ const BASE_CSS = `
|
||||
border-color var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
.ui-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
/* 눌림 — 한 칸 내려앉음(꼴마다 같음) · 부품 모음은 data-state 로 굳힘 */
|
||||
.ui-btn:is(:active, [data-state="active"]):not(:disabled) { transform: translateY(1px); }
|
||||
.ui-btn__icon { display: inline-flex; width: 16px; height: 16px; }
|
||||
|
||||
/* 작은 크기 — 다섯 모양 모두에 먹음(padding · 글자 · 모서리) · 낮은 키에 8px 모서리면 알약 꼴이라 반으로 */
|
||||
@@ -45,27 +47,28 @@ const BASE_CSS = `
|
||||
color: var(--color-primary-text);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.ui-btn--filled:hover:not(:disabled) { background-color: var(--color-royal-amethyst); }
|
||||
.ui-btn--filled:is(:hover, [data-state="hover"]):not(:disabled) { background-color: var(--color-primary-hover); }
|
||||
|
||||
.ui-btn--ghost {
|
||||
background-color: transparent;
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.ui-btn--ghost:hover:not(:disabled) { background-color: var(--color-mist-violet); }
|
||||
.ui-btn--ghost:is(:hover, [data-state="hover"]):not(:disabled) { background-color: var(--color-accent-soft); }
|
||||
|
||||
.ui-btn--pill {
|
||||
background-color: transparent;
|
||||
color: var(--color-plum-velvet);
|
||||
border-radius: var(--radius-pills);
|
||||
}
|
||||
.ui-btn--pill:hover:not(:disabled),
|
||||
.ui-btn--pill.is-active { background-color: var(--color-mist-violet); }
|
||||
.ui-btn--pill:is(:hover, [data-state="hover"]):not(:disabled),
|
||||
.ui-btn--pill.is-active { background-color: var(--color-accent-soft); }
|
||||
|
||||
.ui-btn--danger {
|
||||
background-color: var(--color-danger);
|
||||
color: var(--color-canvas);
|
||||
}
|
||||
.ui-btn--danger:is(:hover, [data-state="hover"]):not(:disabled) { filter: brightness(0.92); }
|
||||
|
||||
/* [초기화] 꼴 — 테두리 · 글만 빨강 · 올리면 채움 */
|
||||
.ui-btn--danger-ghost {
|
||||
@@ -73,7 +76,7 @@ const BASE_CSS = `
|
||||
border-color: var(--color-danger);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.ui-btn--danger-ghost:hover:not(:disabled) {
|
||||
.ui-btn--danger-ghost:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background-color: var(--color-danger);
|
||||
color: var(--color-canvas);
|
||||
}
|
||||
@@ -86,7 +89,7 @@ const BASE_CSS = `
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
}
|
||||
.ui-btn--glass:hover:not(:disabled),
|
||||
.ui-btn--glass:is(:hover, [data-state="hover"]):not(:disabled),
|
||||
.ui-btn--glass.is-active {
|
||||
border-color: var(--color-primary);
|
||||
background-color: color-mix(in srgb, var(--color-primary) 82%, transparent);
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
.ui-general-layout__title {
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-heading-sm);
|
||||
}
|
||||
|
||||
@@ -34,6 +33,47 @@
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
|
||||
/* 일반 화면 틀(552-4) — 대시보드 세부 페이지 꼴(제목 20 · 설명 13 = 대시보드 값) */
|
||||
.ui-page-frame {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
max-width: 1120px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ui-page-frame__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
.ui-page-frame__title {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: 20px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.ui-page-frame__desc {
|
||||
margin: var(--spacing-4) 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ui-page-frame__action {
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.ui-page-frame__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ui-general-block__section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/* =============================================================================
|
||||
* ui_template_general_layout.ts
|
||||
* 일반 화면 틀 `page-frame`(552-4 틀) — 프로젝트 밖 화면(대시보드 세부 · 설정 · B02 · 결제) 몸통: 페이지 머리 + 몸.
|
||||
* 꼴 = createPageFrame — 머리(제목 20 · 설명 13 · 오른쪽 주 단추 자리) + 몸(세로 쌓음) · 대시보드 세부 페이지 꼴
|
||||
* createGeneralLayout — 바탕 · 가운데 폭(--page-max-width) · 큰 제목(옛 꼴 그대로)
|
||||
* 크기 = 폭 1120(대시보드 세부) · 머리 · 몸 사이 16 · 몸 안 사이 12
|
||||
* 상태 = 없음
|
||||
* 쓰는 법 = const f = createPageFrame(host, "구성원", "회사 사람 · 역할"); f.setAction(addBtn); f.body.append(table)
|
||||
* 대신하는 옛 변종 = B01 `pageFrame`(`.b01-pg__head`) · `.b01-proj-head` · M01 `m01-head` · `.ui-wf`(안 씀)
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_general_layout.css";
|
||||
|
||||
export interface GeneralLayoutOptions {
|
||||
@@ -43,3 +54,42 @@ export function createGeneralLayout(options: GeneralLayoutOptions): GeneralLayou
|
||||
root.append(inner);
|
||||
return { root, content };
|
||||
}
|
||||
|
||||
export interface PageFrame {
|
||||
root: HTMLElement;
|
||||
body: HTMLElement;
|
||||
/** 머리 오른쪽 주 단추 — null = 비움 */
|
||||
setAction: (action: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
/** 일반 화면 틀 — host 에 붙임(null = 안 붙임) */
|
||||
export function createPageFrame(host: HTMLElement | null, title: string, desc = ""): PageFrame {
|
||||
const root = document.createElement("div");
|
||||
root.className = "ui-page-frame";
|
||||
const head = document.createElement("header");
|
||||
head.className = "ui-page-frame__head";
|
||||
const text = document.createElement("div");
|
||||
text.className = "ui-page-frame__title-wrap";
|
||||
const h = document.createElement("h2");
|
||||
h.className = "ui-page-frame__title";
|
||||
h.textContent = title;
|
||||
text.append(h);
|
||||
if (desc) {
|
||||
const p = document.createElement("p");
|
||||
p.className = "ui-page-frame__desc";
|
||||
p.textContent = desc;
|
||||
text.append(p);
|
||||
}
|
||||
const slot = document.createElement("div");
|
||||
slot.className = "ui-page-frame__action";
|
||||
head.append(text, slot);
|
||||
const body = document.createElement("div");
|
||||
body.className = "ui-page-frame__body";
|
||||
root.append(head, body);
|
||||
host?.append(root);
|
||||
return {
|
||||
root,
|
||||
body,
|
||||
setAction: (action) => slot.replaceChildren(...(action ? [action] : [])),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/* 부품 「아이콘」(552-2) — 글 줄 안에 놓여도 아래로 처지지 않음 · 색 = 둘레 글 색 */
|
||||
.ui-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
}
|
||||
@@ -3,6 +3,12 @@
|
||||
* 선 아이콘 한 묶음 — Tabler Icons(https://tabler.io/icons) 에서 필요한 것만 옮김 · npm 묶음 안 들임.
|
||||
* 쓰기 = `icon("home")` → <span class="ui-icon"><svg …/></span> · 색 = 글 색(currentColor) · 크기 = 인자.
|
||||
*
|
||||
* 부품 「아이콘」(552-2 · 부품 모음 `#/b01-account?view=ui-kit`)
|
||||
* 꼴 = Tabler 선(24 × 24 · 선 2) 한 꼴 · 크기 16 · 18(기본) · 24 · 32 · 단추 안은 단추 크기 단이 정함
|
||||
* 상태 = 없음(색은 둘레 글 색을 따름)
|
||||
* 쓰기 = `icon("settings", 16)` · 아이콘만 단추 = `createIconButton({ icon, title })`(`ui_template_button.ts`)
|
||||
* 대신 = 그림 이모지(⚙ 📍 🗺️ ✅ …) · PageKit `kitIcon` 여섯 · 글자 기호(→ ▾ ▲▼ ✓ ✗)는 시험이 글자로 잡아 그대로
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-2024 Paweł Kuna
|
||||
@@ -26,6 +32,8 @@
|
||||
* SOFTWARE.
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_icons.css";
|
||||
|
||||
/** 아이콘 이름 → <path> 묶음(24 × 24 · 선 2) */
|
||||
const PATHS = {
|
||||
home: "M5 12l-2 0l9 -9l9 9l-2 0|M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7|M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6",
|
||||
@@ -61,10 +69,64 @@ const PATHS = {
|
||||
world:
|
||||
"M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0|M3.6 9h16.8|M3.6 15h16.8|M11.5 3a17 17 0 0 0 0 18|M12.5 3a17 17 0 0 1 0 18",
|
||||
hand: "M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5|M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5|M14 5.5a1.5 1.5 0 0 1 3 0v6.5|M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47",
|
||||
// 552-2 — 이모지 · 손 그림 대신(지도 · 단계 · 도구 · 방향)
|
||||
map: "M3 7l6 -3l6 3l6 -3v13l-6 3l-6 -3l-6 3v-13|M9 4v13|M15 7v13",
|
||||
road: "M4 19l4 -14|M16 5l4 14|M12 8v-2|M12 13v-2|M12 18v-2",
|
||||
ruler:
|
||||
"M17 3l4 4l-14 14l-4 -4z|M16 7l-1.5 -1.5|M13 10l-1.5 -1.5|M10 13l-1.5 -1.5|M7 16l-1.5 -1.5",
|
||||
sum: "M18 16v2a1 1 0 0 1 -1 1h-11l6 -7l-6 -7h11a1 1 0 0 1 1 1v2",
|
||||
crane: "M6 21h6|M9 21v-18l-6 6h18|M9 3l10 6|M17 9v4a2 2 0 1 1 -2 2",
|
||||
mapPin:
|
||||
"M9 11a3 3 0 1 0 6 0a3 3 0 0 0 -6 0|M17.657 16.657l-4.243 4.243a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 1 1 11.314 0z",
|
||||
compass:
|
||||
"M8 16l2 -6l6 -2l-2 6l-6 2|M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0|M12 3l0 2|M12 19l0 2|M3 12l2 0|M19 12l2 0",
|
||||
circleCheck: "M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0|M9 12l2 2l4 -4",
|
||||
mountain:
|
||||
"M3 20h18l-6.921 -14.612a2.3 2.3 0 0 0 -4.158 0l-6.921 14.612z|M7.5 11l2 2.5l2.5 -2.5l2 3l2.5 -2",
|
||||
cube: "M21 16.008v-8.018a1.98 1.98 0 0 0 -1 -1.717l-7 -4.008a2.016 2.016 0 0 0 -2 0l-7 4.008c-.619 .355 -1 1.01 -1 1.718v8.018c0 .709 .381 1.363 1 1.717l7 4.008a2.016 2.016 0 0 0 2 0l7 -4.008c.619 -.355 1 -1.01 1 -1.718z|M12 22v-10|M12 12l8.73 -5.04|M3.27 6.96l8.73 5.04",
|
||||
settings:
|
||||
"M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z|M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0",
|
||||
x: "M18 6l-12 12|M6 6l12 12",
|
||||
chevronLeft: "M15 6l-6 6l6 6",
|
||||
chevronRight: "M9 6l6 6l-6 6",
|
||||
chevronUp: "M6 15l6 -6l6 6",
|
||||
menu: "M4 6l16 0|M4 12l16 0|M4 18l16 0",
|
||||
undo: "M9 14l-4 -4l4 -4|M5 10h11a4 4 0 1 1 0 8h-1",
|
||||
redo: "M15 14l4 -4l-4 -4|M19 10h-11a4 4 0 1 0 0 8h1",
|
||||
alert:
|
||||
"M12 9v4|M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.87l-8.106 -13.536a1.914 1.914 0 0 0 -3.274 0z|M12 16h.01",
|
||||
lock: "M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z|M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0|M8 11v-4a4 4 0 1 1 8 0v4",
|
||||
help: "M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0|M12 16v.01|M12 13a2 2 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483",
|
||||
refresh: "M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4|M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4",
|
||||
trash:
|
||||
"M4 7l16 0|M10 11l0 6|M14 11l0 6|M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12|M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3",
|
||||
check: "M5 12l5 5l10 -10",
|
||||
download: "M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2|M7 11l5 5l5 -5|M12 4l0 12",
|
||||
eye: "M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0|M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6",
|
||||
eyeOff:
|
||||
"M10.585 10.587a2 2 0 0 0 2.829 2.828|M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87|M3 3l18 18",
|
||||
copy: "M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z|M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1",
|
||||
externalLink:
|
||||
"M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6|M11 13l9 -9|M15 4h5v5",
|
||||
info: "M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0|M12 9h.01|M11 12h1v4h1",
|
||||
minus: "M5 12l14 0",
|
||||
filter:
|
||||
"M4 4h16v2.172a2 2 0 0 1 -.586 1.414l-4.414 4.414v7l-6 2v-8.5l-4.48 -4.928a2 2 0 0 1 -.52 -1.345v-2.227z",
|
||||
// 552-2 — 대시보드 PageKit 에서 옮김(글 그대로)
|
||||
dots: "M5 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0|M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0|M19 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0",
|
||||
plus: "M12 5l0 14|M5 12l14 0",
|
||||
pencil: "M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4|M13.5 6.5l4 4",
|
||||
upload: "M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2|M7 9l5 -5l5 5|M12 4l0 12",
|
||||
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",
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof PATHS;
|
||||
|
||||
/** 모든 이름(부품 모음 견본) */
|
||||
export const ICON_NAMES = Object.keys(PATHS) as IconName[];
|
||||
|
||||
export function icon(name: IconName, size = 18): HTMLSpanElement {
|
||||
const span = document.createElement("span");
|
||||
span.className = "ui-icon";
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/* 목록 줄(552-4) — `.ui-sidebar-row` = 옛 이름(M01 · M02 좌측 목록) · 자리 · 크기는 그대로 · 겉만 공통 값
|
||||
새 줄(`.ui-list-row`)은 data-size 높이 단 · 상태 = :is(:hover, [data-state]) 로 부품 모음이 굳혀 보임 */
|
||||
.ui-sidebar-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border: 0;
|
||||
border-radius: var(--radius-buttons);
|
||||
background: transparent;
|
||||
color: var(--color-text-body);
|
||||
font: inherit;
|
||||
font-size: var(--text-body-sm);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-sidebar-row:is(:hover, [data-state="hover"]) {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
|
||||
.ui-sidebar-row:is(:active, [data-state="active"]) {
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.ui-sidebar-row.is-active {
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.ui-sidebar-row:is(:disabled, [aria-disabled="true"]) {
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 새 줄 — 높이 단 · 글 · 덧붙임 */
|
||||
.ui-list-row {
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
.ui-list-row[data-size="28"] {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.ui-list-row[data-size="32"] {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.ui-list-row[data-size="36"] {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.ui-list-row__text {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-list-row__label,
|
||||
.ui-list-row__sub {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-list-row__sub {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-list-row__trailing {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-list-row__no {
|
||||
display: inline-grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: var(--radius-pills);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-list-row.is-active .ui-list-row__no {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
/* 끌기 손잡이 — 점 여섯(2 × 3) */
|
||||
.ui-list-row__grip {
|
||||
flex: none;
|
||||
width: 8px;
|
||||
height: 14px;
|
||||
background: radial-gradient(circle, var(--color-text-muted) 1px, transparent 1.5px) 0 0 / 4px
|
||||
4.6px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.ui-list-row--stripe {
|
||||
box-shadow: inset 3px 0 0 var(--ui-list-row-stripe, var(--color-border));
|
||||
border-radius: 0 var(--radius-buttons) var(--radius-buttons) 0;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/* =============================================================================
|
||||
* ui_template_list_row.ts
|
||||
* 목록 줄 `list-row`(552-4 틀) — 패널 · 목록 안 고르는 줄 하나 · 왼쪽 글 · 오른쪽 덧붙임.
|
||||
* 꼴 = 고르는 줄 · 두 줄(sub) · 끌기 손잡이(grip) · 왼쪽 색 띠(stripe) · 번호 원(no)
|
||||
* 크기 = data-size 28 · 32 · 36 (기본 32)
|
||||
* 상태 = 마우스 · 눌림 · 고름(`is-active` + aria-selected) · 막힘(disabled)
|
||||
* 쓰는 법 = list.append(createListRow({ label: "B1 배수관", active: true, onClick: pick }))
|
||||
* 대신하는 옛 변종 = `.ui-sidebar-row`(이 이름도 그대로 냄) · `.ui-tree__row` · `.ui-find__row` ·
|
||||
* `b07-plan-manage__row` + `__grip` · `b05-drainage__basin` · `d02-points__row` · `m02-pick__item` …
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_list_row.css";
|
||||
|
||||
export interface ListRowOptions {
|
||||
label: string;
|
||||
/** 둘째 줄(흐린 글) */
|
||||
sub?: string;
|
||||
/** 오른쪽 끝 덧붙임(값 · 배지 · 단추) */
|
||||
trailing?: HTMLElement | string;
|
||||
/** 왼쪽 번호 원 */
|
||||
no?: number | string;
|
||||
/** 왼쪽 색 띠 — 공통 값 이름(예: "var(--color-success)") */
|
||||
stripe?: string;
|
||||
/** 끌기 손잡이 — 끌기 동작은 부르는 쪽 몫 */
|
||||
grip?: boolean;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: 28 | 32 | 36;
|
||||
title?: string;
|
||||
onClick?: (event: Event) => void;
|
||||
}
|
||||
|
||||
function span(className: string, text?: string): HTMLSpanElement {
|
||||
const n = document.createElement("span");
|
||||
n.className = className;
|
||||
if (text !== undefined) n.textContent = text;
|
||||
return n;
|
||||
}
|
||||
|
||||
/** 줄 하나 — onClick 이 있으면 <button> · 없으면 <div> */
|
||||
export function createListRow(opts: ListRowOptions): HTMLElement {
|
||||
const row = document.createElement(opts.onClick ? "button" : "div");
|
||||
if (row instanceof HTMLButtonElement) row.type = "button";
|
||||
row.className = "ui-list-row ui-sidebar-row";
|
||||
row.dataset.size = String(opts.size ?? 32);
|
||||
row.setAttribute("role", "option");
|
||||
if (opts.title) row.title = opts.title;
|
||||
if (opts.stripe) {
|
||||
row.classList.add("ui-list-row--stripe");
|
||||
row.style.setProperty("--ui-list-row-stripe", opts.stripe);
|
||||
}
|
||||
if (opts.grip) row.append(span("ui-list-row__grip"));
|
||||
if (opts.no !== undefined) row.append(span("ui-list-row__no", String(opts.no)));
|
||||
const text = span("ui-list-row__text");
|
||||
text.append(span("ui-list-row__label", opts.label));
|
||||
if (opts.sub) text.append(span("ui-list-row__sub", opts.sub));
|
||||
row.append(text);
|
||||
if (opts.trailing !== undefined) {
|
||||
const tail = span("ui-list-row__trailing");
|
||||
tail.append(opts.trailing);
|
||||
row.append(tail);
|
||||
}
|
||||
setListRowActive(row, Boolean(opts.active));
|
||||
if (opts.disabled) {
|
||||
if (row instanceof HTMLButtonElement) row.disabled = true;
|
||||
row.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
if (opts.onClick) row.addEventListener("click", opts.onClick);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 고름 켜고 끄기 — 클래스 · aria 한 번에 */
|
||||
export function setListRowActive(row: HTMLElement, active: boolean): void {
|
||||
row.classList.toggle("is-active", active);
|
||||
row.setAttribute("aria-selected", String(active));
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
/* 작업 화면 틀(552-4) — 왼쪽 패널 = 흰 판 + 1 px 선 · 그림자 없음(대시보드 메뉴 꼴) · 위 머리 줄 없음 · ☰ 서랍 그대로
|
||||
자리 · 폭 · 열고 닫기는 그대로 · 겉(색 · 선 · 둥글기)만 공통 값 · 손잡이 = 강조색 테두리(2026-08-04 사용자 지시) */
|
||||
.ui-workflow-overlay {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -28,14 +30,9 @@
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
border-left: 0;
|
||||
/* 크기 조절은 안 되지만 조절 패널들과 같은 강조색 가장자리(2026-08-04 사용자 지시)
|
||||
— 리사이저 상시 표시선과 같은 색·기조로 경계가 구분되게 한다. */
|
||||
border-right-color: color-mix(
|
||||
in srgb,
|
||||
var(--panel-handle-border, var(--color-border)) 75%,
|
||||
transparent
|
||||
);
|
||||
border-right-color: var(--color-border);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
transition: transform var(--transition-base);
|
||||
}
|
||||
|
||||
@@ -142,6 +139,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 진행 판 접기 [−][+] — 아이콘 단추 32 꼴 */
|
||||
.ui-workflow-overlay__toggle {
|
||||
flex: 0 0 auto;
|
||||
width: var(--spacing-32);
|
||||
@@ -149,10 +147,15 @@
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-buttons);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-text);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-workflow-overlay__toggle:is(:hover, [data-state="hover"]) {
|
||||
background: var(--color-hover);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ui-workflow-overlay__handle {
|
||||
position: absolute;
|
||||
/* 배경과 같은 무채색 테두리는 손잡이가 묻혀 안 보였다(2026-08-04 사용자 지시).
|
||||
@@ -501,29 +504,7 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ui-sidebar-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border: 0;
|
||||
border-radius: var(--radius-buttons);
|
||||
background: transparent;
|
||||
color: var(--color-text-body);
|
||||
font: inherit;
|
||||
font-size: var(--text-body-sm);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-sidebar-row:hover {
|
||||
background: var(--color-paper);
|
||||
}
|
||||
|
||||
.ui-sidebar-row.is-active {
|
||||
background: var(--color-mist-violet);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
/* 목록 줄 `.ui-sidebar-row` = `ui_template_list_row.css`(목록 줄 부품) */
|
||||
|
||||
/* 기기 대응(PLAN_기기대응 2-3) — 폭 840 미만(data-size compact · medium)만.
|
||||
좌측 패널 = 서랍(본문 위에 겹침 · 덮개를 누르면 닫힘). */
|
||||
@@ -538,7 +519,7 @@ html:is([data-size="compact"], [data-size="medium"])
|
||||
position: fixed;
|
||||
inset: var(--app-header-h) 0 0 0;
|
||||
z-index: calc(var(--z-dropdown) - 1);
|
||||
background: rgba(38, 17, 74, 0.35);
|
||||
background: var(--color-scrim);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
/* =============================================================================
|
||||
* ui_template_overlay.ts
|
||||
* 작업 화면 틀 `work-frame`(552-4 틀) — 프로젝트 안 화면(B03 ~ B09 · D · E · M) 겉틀: 왼쪽 패널 · 본문 · 아래 단추 줄.
|
||||
* 꼴 = 왼쪽 패널(흰 판 + 1 px 선 · 그림자 없음 · 제목 줄 = 화면 이름 + 프로젝트 이름) · 손잡이(강조색 테두리)
|
||||
* 아래 단추 줄 `.ui-sidebar-actions`([?][<][>] + 주 단추) · 위 머리 줄 없음 · ☰ 서랍(진행 단계) 그대로
|
||||
* 진행 판(떠 있는 판 · 끌어 옮김 · 로그인 앞 화면만) · 좁은 폭 = 서랍(덮개 `--color-scrim`)
|
||||
* 크기 = 폭 --wf-left-panel-width(좁으면 --wf-left-panel-narrow-width) · 제목 줄 최소 48 · 진행 판 접기 단추 32
|
||||
* 상태 = 펼침 · 접힘(`is-collapsed` · 손잡이 aria-expanded) — 열림 기억 = 화면 캐시
|
||||
* 쓰는 법 = createWorkflowLayout({ title, steps, leftPanel, mainContent }) 이 부름 — 페이지는 왼쪽 패널 끝에 `.ui-sidebar-actions` 만 붙임
|
||||
* 대신하는 옛 변종 = 페이지마다 그린 왼쪽 판 · `design_dock` 아래 줄 · 그림자 판 · 강조색 오른쪽 선
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_overlay.css";
|
||||
// 옛 목록 줄(`.ui-sidebar-row` · M01 · M02 왼쪽 목록) 겉 = 목록 줄 부품
|
||||
import "./ui_template_list_row.css";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { t } from "./ui_template_locale";
|
||||
import { placeHelpButton } from "./ui_template_help";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/* 패널 머리(552-4) — 제목 · 부제 왼쪽 · 단추 오른쪽 · 최소 높이 32 */
|
||||
.ui-panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-8);
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.ui-panel-head--divider {
|
||||
padding-bottom: var(--spacing-8);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ui-panel-head__text {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-panel-head__title {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-body-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--leading-body-sm);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-panel-head__sub {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
line-height: var(--leading-caption);
|
||||
}
|
||||
|
||||
.ui-panel-head__actions {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: var(--btn-group-gap);
|
||||
}
|
||||
|
||||
.ui-panel-head__actions:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/* =============================================================================
|
||||
* ui_template_panel_head.ts
|
||||
* 패널 머리 `panel-head`(552-4 틀) — 판 · 카드 · 구역 맨 위 한 줄: 제목 · 부제 · 오른쪽 단추.
|
||||
* 꼴 = 제목만 · 제목 + 부제(둘째 줄 흐린 글) · 오른쪽 단추 자리(actions) · 밑줄(divider)
|
||||
* 크기 = data-size 32(줄 높이 최소 32) · 제목 = --text-body-sm 600 · 부제 = --text-caption
|
||||
* 상태 = 없음(안 단추가 제 상태를 가짐)
|
||||
* 쓰는 법 = card.prepend(createPanelHead({ title: "배수관", sub: "3 개", actions: [btn] }).root)
|
||||
* 대신하는 옛 변종 = `.ui-general-block__section-header` · `.ui-side-panels__head` ·
|
||||
* `b03-file__group` 제목 · `.b08-hydro__h2` · `.b08-sheets__title` · `e02-card__head` · `e03-card__head` · `m01-head`
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_panel_head.css";
|
||||
|
||||
export interface PanelHeadOptions {
|
||||
title: string;
|
||||
sub?: string;
|
||||
actions?: HTMLElement[];
|
||||
/** 아래 1 px 선 */
|
||||
divider?: boolean;
|
||||
/** 제목 태그 — 기본 h3 */
|
||||
level?: 2 | 3 | 4;
|
||||
}
|
||||
|
||||
export interface PanelHead {
|
||||
root: HTMLElement;
|
||||
title: HTMLElement;
|
||||
actions: HTMLElement;
|
||||
setTitle: (text: string) => void;
|
||||
}
|
||||
|
||||
export function createPanelHead(opts: PanelHeadOptions): PanelHead {
|
||||
const root = document.createElement("header");
|
||||
root.className = "ui-panel-head";
|
||||
root.dataset.size = "32";
|
||||
if (opts.divider) root.classList.add("ui-panel-head--divider");
|
||||
const text = document.createElement("div");
|
||||
text.className = "ui-panel-head__text";
|
||||
const title = document.createElement(`h${opts.level ?? 3}`);
|
||||
title.className = "ui-panel-head__title";
|
||||
title.textContent = opts.title;
|
||||
text.append(title);
|
||||
if (opts.sub) {
|
||||
const sub = document.createElement("p");
|
||||
sub.className = "ui-panel-head__sub";
|
||||
sub.textContent = opts.sub;
|
||||
text.append(sub);
|
||||
}
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "ui-panel-head__actions";
|
||||
actions.append(...(opts.actions ?? []));
|
||||
root.append(text, actions);
|
||||
return { root, title, actions, setTitle: (t) => void (title.textContent = t) };
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/* 공개 화면 틀(552-4) — 머리 띠 · 구역 · 자리 · 글 크기는 A01 hero · section 과 같음 · 색만 공통 값 */
|
||||
.ui-public-frame {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-64);
|
||||
padding-bottom: var(--spacing-64);
|
||||
}
|
||||
|
||||
.ui-public-frame__hero {
|
||||
padding: var(--spacing-96) var(--spacing-32);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: linear-gradient(to bottom, var(--color-accent-soft), var(--color-canvas));
|
||||
}
|
||||
|
||||
.ui-public-frame__hero-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-24);
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-public-frame__hero[data-align="start"] .ui-public-frame__hero-inner {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ui-public-frame__title {
|
||||
max-width: 14ch;
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-heading-lg);
|
||||
line-height: var(--leading-heading-lg);
|
||||
}
|
||||
|
||||
.ui-public-frame__lead {
|
||||
max-width: 52ch;
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body);
|
||||
line-height: var(--leading-body);
|
||||
}
|
||||
|
||||
.ui-public-frame__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-16);
|
||||
margin-top: var(--spacing-8);
|
||||
}
|
||||
|
||||
.ui-public-frame__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-24);
|
||||
width: 100%;
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-32);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ui-public-frame__section-title {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-heading-sm);
|
||||
line-height: var(--leading-heading-sm);
|
||||
}
|
||||
|
||||
.ui-public-frame__section-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.ui-public-frame__title {
|
||||
font-size: var(--text-heading);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/* =============================================================================
|
||||
* ui_template_public_layout.ts
|
||||
* 공개 화면 틀 `public-frame`(552-4 틀) — 로그인 전 공개 페이지(A01 ~ A09) 몸통: 머리 띠(hero) · 구역 · 본문.
|
||||
* 꼴 = 머리 띠(제목 · 설명 · 주 단추 줄 · 가운데 / 왼쪽 맞춤) · 구역(제목 + 몸) 여럿
|
||||
* 공개 머리(로고 · 메뉴)와 바닥글(`.app-footer`)은 앱 셸 몫 — 이 틀은 그 사이 본문만
|
||||
* 크기 = 폭 = --page-max-width · 제목 = --text-heading-lg(좁으면 --text-heading) · 구역 제목 = --text-heading-sm
|
||||
* 상태 = 없음
|
||||
* 쓰는 법 = const f = createPublicFrame({ title: "임도 설계", lead: "…", actions: [btn] }); f.addSection("기능").append(grid)
|
||||
* 대신하는 옛 변종 = A01 ~ A05 `.a0N-hero` · `__inner` · `__title` · `__subtitle` · `__ctas` 다섯 벌 · `.a0N-section(__title)`
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_public_layout.css";
|
||||
|
||||
export interface PublicFrameOptions {
|
||||
title: string;
|
||||
lead?: string;
|
||||
actions?: HTMLElement[];
|
||||
/** 머리 띠 맞춤 — 기본 가운데 */
|
||||
align?: "center" | "start";
|
||||
}
|
||||
|
||||
export interface PublicFrame {
|
||||
root: HTMLElement;
|
||||
hero: HTMLElement;
|
||||
/** 구역 하나 더함 → 그 구역 몸(여기에 내용을 담음) */
|
||||
addSection: (title?: string) => HTMLElement;
|
||||
}
|
||||
|
||||
function node<K extends keyof HTMLElementTagNameMap>(
|
||||
tag: K,
|
||||
className: string,
|
||||
text?: string,
|
||||
): HTMLElementTagNameMap[K] {
|
||||
const n = document.createElement(tag);
|
||||
n.className = className;
|
||||
if (text !== undefined) n.textContent = text;
|
||||
return n;
|
||||
}
|
||||
|
||||
export function createPublicFrame(opts: PublicFrameOptions): PublicFrame {
|
||||
const root = node("div", "ui-public-frame");
|
||||
const hero = node("section", "ui-public-frame__hero");
|
||||
hero.dataset.align = opts.align ?? "center";
|
||||
const inner = node("div", "ui-public-frame__hero-inner");
|
||||
inner.append(node("h1", "ui-public-frame__title", opts.title));
|
||||
if (opts.lead) inner.append(node("p", "ui-public-frame__lead", opts.lead));
|
||||
if (opts.actions?.length) {
|
||||
const ctas = node("div", "ui-public-frame__actions");
|
||||
ctas.append(...opts.actions);
|
||||
inner.append(ctas);
|
||||
}
|
||||
hero.append(inner);
|
||||
root.append(hero);
|
||||
const addSection = (title?: string): HTMLElement => {
|
||||
const section = node("section", "ui-public-frame__section");
|
||||
if (title) section.append(node("h2", "ui-public-frame__section-title", title));
|
||||
const body = node("div", "ui-public-frame__section-body");
|
||||
section.append(body);
|
||||
root.append(section);
|
||||
return body;
|
||||
};
|
||||
return { root, hero, addSection };
|
||||
}
|
||||
@@ -144,3 +144,14 @@
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* 부품 여덟 단 한 줄 */
|
||||
.ui-kit__part-sizes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--color-border);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 부품 모음 화면(552-1) — 대시보드 세부 페이지 `#/b01-account?view=ui-kit` · 시스템 관리자만.
|
||||
* 밝음 · 어두움을 나란히 — 칸마다 iframe(`<html data-theme>`)에 지금 페이지 CSS 를 옮겨 실제 테마 값으로 그림.
|
||||
* 부품 줄 = `ui_template_ui_kit_parts.ts` 목록 · 상태 다섯 칸(기본 · 마우스 · 눌림 · 막힘 · 오류) · 견본이 없으면 빈 자리.
|
||||
* 크기 자 = 단추 · 입력 높이 여덟 단(18 ~ 42).
|
||||
* 크기 자 = 단추 · 입력 높이 여덟 단(18 ~ 42) · 부품마다 `sizes` 가 있으면 여덟 단 한 줄.
|
||||
* ========================================================================== */
|
||||
|
||||
import { pageFrame } from "../B01_Dashboard/B01_Dashboard_UI_PageKit";
|
||||
@@ -68,6 +68,11 @@ function partRow(part: KitPart): HTMLElement {
|
||||
states.append(cell);
|
||||
});
|
||||
item.append(states);
|
||||
if (part.sizes) {
|
||||
const sizes = node("div", "ui-kit__part-sizes");
|
||||
for (const z of KIT_SIZES) sizes.append(part.sizes(z));
|
||||
item.append(sizes);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/* =============================================================================
|
||||
* ui_template_ui_kit_frames.ts
|
||||
* 부품 모음 화면의 틀(552-4) 견본 — `ui_template_ui_kit_parts.ts` 의 frame 줄이 부름.
|
||||
* 견본 = 진짜 부품 함수 · 클래스로 그림 · 화면에 붙는(fixed) 틀은 칸 안에 놓이게 자리만 인라인으로 풂.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "./ui_template_elements";
|
||||
import { createPageFrame } from "./ui_template_general_layout";
|
||||
import { createListRow } from "./ui_template_list_row";
|
||||
import { createWorkflowPanelHandle } from "./ui_template_overlay";
|
||||
import { createPanelHead } from "./ui_template_panel_head";
|
||||
import { createPublicFrame } from "./ui_template_public_layout";
|
||||
import type { KitState } from "./ui_template_ui_kit_parts";
|
||||
import { createStepBar } from "./ui_template_workflow_layout";
|
||||
|
||||
const noop = (): void => {};
|
||||
|
||||
function div(className: string, text?: string): HTMLDivElement {
|
||||
const n = document.createElement("div");
|
||||
n.className = className;
|
||||
if (text !== undefined) n.textContent = text;
|
||||
return n;
|
||||
}
|
||||
|
||||
const wrap = (...children: HTMLElement[]): HTMLElement => {
|
||||
const box = div("ui-kit__demo-row");
|
||||
box.append(...children);
|
||||
return box;
|
||||
};
|
||||
|
||||
/** 마우스 · 눌림 = data-state · 막힘 = disabled */
|
||||
function mark<T extends HTMLElement>(node: T, state: KitState): T {
|
||||
if (state === "disabled") {
|
||||
if (node instanceof HTMLButtonElement) node.disabled = true;
|
||||
node.setAttribute("aria-disabled", "true");
|
||||
} else if (state !== "default") node.dataset.state = state;
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 아래 단추 줄 — [?][<][>] + 주 단추 */
|
||||
function sidebarActions(): HTMLElement {
|
||||
const bar = div("ui-sidebar-actions");
|
||||
const help = createButton({ label: "?", variant: "ghost", onClick: noop });
|
||||
help.classList.add("ui-help-btn");
|
||||
const prev = createButton({ label: "<", variant: "ghost", onClick: noop });
|
||||
const next = createButton({ label: ">", variant: "ghost", onClick: noop });
|
||||
prev.classList.add("ui-step-nav-btn");
|
||||
next.classList.add("ui-step-nav-btn");
|
||||
bar.append(help, prev, next, createButton({ label: "저장", onClick: noop }));
|
||||
return bar;
|
||||
}
|
||||
|
||||
/** 작업 화면 틀 — 왼쪽 패널(제목 줄 · 카드 · 아래 단추 줄 · 손잡이) + 본문 자리 */
|
||||
function workFrame(): HTMLElement {
|
||||
const panel = document.createElement("aside");
|
||||
panel.className = "ui-workflow-overlay__panel ui-workflow-overlay__panel--title";
|
||||
panel.style.cssText = "position:relative;top:auto;bottom:auto;width:280px;height:260px";
|
||||
const header = div("ui-workflow-overlay__header");
|
||||
const title = document.createElement("h2");
|
||||
title.className = "ui-workflow-overlay__title";
|
||||
title.textContent = "횡단설계";
|
||||
const project = document.createElement("span");
|
||||
project.className = "ui-workflow-overlay__project";
|
||||
project.textContent = "검증 프로젝트";
|
||||
header.append(title, project);
|
||||
const handle = createWorkflowPanelHandle("side");
|
||||
handle.setOpen(true);
|
||||
const body = div("ui-workflow-overlay__body");
|
||||
body.style.cssText = "display:flex;flex-direction:column;gap:8px;padding-top:8px";
|
||||
const card = div("ui-sidebar-card ui-sidebar-section");
|
||||
card.append(
|
||||
createPanelHead({ title: "구조물 배치", sub: "측점 3 곳" }).root,
|
||||
createListRow({ label: "No.0+020 배수관", active: true, onClick: noop }),
|
||||
createListRow({ label: "No.0+140 옹벽", onClick: noop }),
|
||||
);
|
||||
const fill = div("");
|
||||
fill.style.flex = "1";
|
||||
body.append(card, fill, sidebarActions());
|
||||
panel.append(header, handle.root, body);
|
||||
const main = div("ui-workflow-layout__main", "본문(지도 · 그림 · 표)");
|
||||
main.style.cssText = "min-height:260px;padding:12px;border:1px dashed var(--color-border)";
|
||||
const box = div("ui-kit__frame-demo");
|
||||
box.style.cssText = "display:grid;grid-template-columns:280px 1fr;width:100%";
|
||||
box.append(panel, main);
|
||||
return box;
|
||||
}
|
||||
|
||||
function pageFrame(): HTMLElement {
|
||||
const f = createPageFrame(null, "구성원", "회사 사람 · 역할 · 초대");
|
||||
f.setAction(createButton({ label: "초대", onClick: noop }));
|
||||
f.body.append(div("ui-general-block__empty", "몸 — 표 · 카드가 들어감"));
|
||||
return f.root;
|
||||
}
|
||||
|
||||
function publicFrame(): HTMLElement {
|
||||
const f = createPublicFrame({
|
||||
title: "임도 설계 자동화",
|
||||
lead: "지형 자료로 노선 · 종단 · 횡단 · 수량을 한 흐름에.",
|
||||
actions: [
|
||||
createButton({ label: "시작하기", onClick: noop }),
|
||||
createButton({ label: "더 알아보기", variant: "ghost", onClick: noop }),
|
||||
],
|
||||
});
|
||||
f.addSection("기능").append(div("ui-general-block__empty", "구역 몸"));
|
||||
f.root.style.width = "100%";
|
||||
return f.root;
|
||||
}
|
||||
|
||||
const STEP_NAMES = ["파일", "전처리", "노선", "횡단"];
|
||||
|
||||
function steps(state: KitState): HTMLElement | null {
|
||||
if (state === "active" || state === "error") return null;
|
||||
const bar = createStepBar(STEP_NAMES, 2, { compact: true });
|
||||
const target = bar.querySelectorAll<HTMLButtonElement>(".ui-workflow-layout__step")[0];
|
||||
mark(target, state);
|
||||
if (state !== "default") return bar;
|
||||
const list = createStepBar(STEP_NAMES, 2, {
|
||||
orientation: "vertical",
|
||||
icons: ["1", "2", "3", "4"],
|
||||
});
|
||||
list.style.width = "180px";
|
||||
return wrap(bar, list);
|
||||
}
|
||||
|
||||
function panelHead(): HTMLElement {
|
||||
const box = div("");
|
||||
box.style.cssText = "display:flex;flex-direction:column;gap:12px;width:320px";
|
||||
box.append(
|
||||
createPanelHead({ title: "배수관", sub: "3 개", divider: true }).root,
|
||||
createPanelHead({
|
||||
title: "표준 횡단면",
|
||||
actions: [createButton({ label: "더함", variant: "ghost", onClick: noop })],
|
||||
}).root,
|
||||
);
|
||||
return box;
|
||||
}
|
||||
|
||||
function listRow(state: KitState): HTMLElement | null {
|
||||
if (state === "error") return null;
|
||||
const box = div("");
|
||||
box.style.cssText = "display:flex;flex-direction:column;gap:2px;width:240px";
|
||||
const one = mark(createListRow({ label: "B1 배수관", sub: "No.0+020", onClick: noop }), state);
|
||||
box.append(one);
|
||||
if (state === "default")
|
||||
box.append(
|
||||
createListRow({ label: "B2 옹벽", active: true, no: 2, size: 28, onClick: noop }),
|
||||
createListRow({ label: "B3 측구", grip: true, trailing: "12 m", size: 36, onClick: noop }),
|
||||
createListRow({ label: "B4 비탈", stripe: "var(--color-success)", onClick: noop }),
|
||||
);
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 열쇠 → 견본(상태별 · null = 그 상태 없음) */
|
||||
export const FRAME_DEMOS: Record<string, (state: KitState) => HTMLElement | null> = {
|
||||
"work-frame": (s) => (s === "default" ? workFrame() : null),
|
||||
"page-frame": (s) => (s === "default" ? pageFrame() : null),
|
||||
"public-frame": (s) => (s === "default" ? publicFrame() : null),
|
||||
steps,
|
||||
"panel-head": (s) => (s === "default" ? panelHead() : null),
|
||||
"list-row": listRow,
|
||||
};
|
||||
@@ -18,8 +18,9 @@
|
||||
* 몫 = 552-2 기본 · 552-3 묶음 · 552-4 틀 — 세 묶음이 떨어져 있어 서로 줄을 안 건드림.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createInputField, createSelectField } from "./ui_template_elements";
|
||||
import { icon } from "./ui_template_icons";
|
||||
import { createButton, createIconButton, type UiSize } from "./ui_template_button";
|
||||
import { createInputField, createSelectField } from "./ui_template_elements";
|
||||
import { ICON_NAMES, icon } from "./ui_template_icons";
|
||||
import {
|
||||
badge,
|
||||
dataTable,
|
||||
@@ -40,6 +41,7 @@ import { createKv } from "./ui_template_kv";
|
||||
import { createProgressBar, createProgressCircle } from "./ui_template_progress";
|
||||
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;
|
||||
@@ -72,6 +74,8 @@ export interface KitPart {
|
||||
file: string;
|
||||
/** 지금 견본 — 상태마다 하나 · null = 그 상태 없음 · 없으면 빈 자리 */
|
||||
demo?: (state: KitState) => HTMLElement | null;
|
||||
/** 크기 단 견본 — 여덟 단 한 줄(크기 없는 부품은 없음) */
|
||||
sizes?: (size: UiSize) => HTMLElement;
|
||||
/** 견본이 옮기기 전 PageKit · 옛 공용 부품인지 */
|
||||
legacy?: boolean;
|
||||
}
|
||||
@@ -111,9 +115,12 @@ const col = (...children: HTMLElement[]): HTMLElement => {
|
||||
// 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: "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)))),
|
||||
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))),
|
||||
sizes: (z) => createIconButton({ icon: "settings", title: `설정 ${z}`, size: z, variant: "outline" }) },
|
||||
{ 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,
|
||||
@@ -126,34 +133,28 @@ export const KIT_PARTS: KitPart[] = [
|
||||
{ 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) },
|
||||
{ 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: "값 하나 · 이름 · 값 글", file: "ui_template_input.ts" },
|
||||
{ key: "text", name: "보조 글 · 안내 줄", group: "basic", kinds: "흐린 글 · 안내 · 상태(오류 · 경고) · 칸 오류", file: "ui_template_text.ts" },
|
||||
// ── 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: "float-panel", name: "떠 있는 판", group: "compound", kinds: "그림 위 판 · 끌기 머리 · 닫기 · 팝오버", file: "ui_template_float_panel.ts" },
|
||||
{ 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("접힌 카드")); } },
|
||||
{ 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: "list-row", name: "목록 줄", group: "compound", kinds: "고르는 줄 · 두 줄 · 끌기 손잡이 · 색 띠", file: "ui_template_list_row.ts" },
|
||||
{ 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",
|
||||
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: "보통 · 넓음 · 확인 · 위험 확인", 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",
|
||||
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: "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",
|
||||
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",
|
||||
@@ -169,7 +170,19 @@ export const KIT_PARTS: KitPart[] = [
|
||||
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" },
|
||||
{ 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: "페이지 머리(제목 · 설명 · 주 단추) · 몸통", 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" },
|
||||
{ key: "drawer", name: "오른쪽 서랍", group: "frame", kinds: "상세 · 설정", file: "ui_template_side_panels.ts" },
|
||||
{ key: "toolbar", name: "도구줄", group: "frame", kinds: "단추 묶음 · 나눔선 · 찾기 · 고르기", file: "ui_template_toolbar.ts" },
|
||||
{ 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"] },
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
/* 단계 줄(552-4) — 가로 막대 · compact(28) · 세로 목록(40 · ☰ 서랍) · 상태 = 마우스 · 고름(지금) · 막힘 · 단계 색 테두리 */
|
||||
.ui-workflow-layout__steps {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
@@ -41,7 +42,7 @@
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
width: 100%;
|
||||
min-height: var(--spacing-40);
|
||||
min-height: 40px;
|
||||
border-radius: var(--radius-buttons);
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
@@ -70,18 +71,20 @@
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.ui-workflow-layout__step.is-enabled:hover {
|
||||
background: var(--color-surface);
|
||||
.ui-workflow-layout__step.is-enabled:is(:hover, [data-state="hover"]) {
|
||||
background: var(--color-hover);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ui-workflow-layout__steps.is-compact .ui-workflow-layout__step {
|
||||
.ui-workflow-layout__steps.is-compact .ui-workflow-layout__step,
|
||||
.ui-workflow-layout__step[data-size="28"] {
|
||||
height: 28px;
|
||||
padding: 0 var(--spacing-12, var(--spacing-8));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-workflow-layout__step.is-active {
|
||||
background: var(--color-mist-violet);
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
@@ -99,7 +102,7 @@
|
||||
}
|
||||
|
||||
.ui-workflow-layout__step.state-stale {
|
||||
border-color: var(--color-muted);
|
||||
border-color: var(--color-text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/* =============================================================================
|
||||
* ui_template_workflow_layout.ts
|
||||
* 단계 줄 `steps`(552-4 틀) + 작업 화면 바탕(createWorkflowLayout · 왼쪽 패널 · 본문 = `ui_template_overlay.ts`).
|
||||
* 꼴 = 가로 막대(알약) · compact(한 줄 28) · 세로 목록(40 · ☰ 서랍 · 맨 위 [대시보드] 줄) · 아이콘 + 이름
|
||||
* 크기 = data-size 28(compact) · 40(세로) · 가로 막대 = 글 + 위아래 4
|
||||
* 상태 = 마우스 · 지금(`is-active`) · 켜짐(`is-enabled`) · 막힘(disabled) · 단계 테두리(state-complete · in_progress · stale · failed)
|
||||
* 쓰는 법 = createStepBar(["파일", "전처리", …], 2, { stages, routes, orientation: "vertical", homeButton: true })
|
||||
* 대신하는 옛 변종 = B01 `.b01-pd-steps` · `.b01-home__step` · A02 `.a02-workflow-row` · B11 `.b11-status__flow`
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_workflow_layout.css";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { t } from "./ui_template_locale";
|
||||
@@ -82,6 +92,7 @@ export function createStepBar(
|
||||
const home = document.createElement("button");
|
||||
home.type = "button";
|
||||
home.className = "ui-workflow-layout__step is-enabled ui-workflow-layout__step--home";
|
||||
if (options.orientation === "vertical") home.dataset.size = "40";
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "ui-workflow-layout__step-icon";
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
@@ -98,6 +109,8 @@ export function createStepBar(
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "ui-workflow-layout__step";
|
||||
if (options?.compact) button.dataset.size = "28";
|
||||
else if (options?.orientation === "vertical") button.dataset.size = "40";
|
||||
if (index === activeStep) button.classList.add("is-active");
|
||||
const iconText = options?.icons?.[index];
|
||||
if (iconText) {
|
||||
|
||||
Reference in New Issue
Block a user