Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -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
|
||||
@@ -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,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 };
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
tabBar,
|
||||
valueText,
|
||||
} from "../B01_Dashboard/B01_Dashboard_UI_PageKit";
|
||||
import { FRAME_DEMOS } from "./ui_template_ui_kit_frames";
|
||||
|
||||
/** 상태 줄 다섯 칸 */
|
||||
export const KIT_STATES = ["default", "hover", "active", "disabled", "error"] as const;
|
||||
@@ -120,22 +121,16 @@ export const KIT_PARTS: KitPart[] = [
|
||||
{ key: "text", name: "보조 글 · 안내 줄", group: "basic", kinds: "흐린 글 · 안내 · 상태(오류 · 경고) · 칸 오류", file: "ui_template_text.ts" },
|
||||
// ── 552-3 묶음 ─────────────────────────────────────────────
|
||||
{ key: "card", name: "카드", group: "compound", kinds: "선 카드 · 머리 카드 · 고를 수 있는 카드", file: "ui_template_card.ts" },
|
||||
{ key: "float-panel", name: "떠 있는 판", group: "compound", kinds: "그림 위 판 · 끌기 머리 · 닫기 · 팝오버", file: "ui_template_float_panel.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: "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", 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: "banner", name: "띠 알림", group: "compound", kinds: "왼쪽 띠 · 정보 · 경고 · 위험 · 펼침 · 안 단추", file: "ui_template_banner.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: "kv", name: "값 목록", group: "compound", kinds: "이름 : 값 줄 · 판정(✓ ✗) · 큰 값", file: "ui_template_kv.ts" },
|
||||
{ key: "progress", name: "진행 막대", group: "compound", kinds: "막대 · 원", file: "ui_template_progress.ts" },
|
||||
@@ -145,7 +140,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