merge(sub_laptop_2): 552-2 기본 부품 13 — 부품 모음 목록 충돌 양쪽 살림(552-2 견본 + 552-3 · 552-4 묶음)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""552-2 기본 부품(입력 · 고르기 · 슬라이더 · 체크 · 켜기 · 분할 · 탭 · 배지 · 툴팁 · 보조 글) 지킴 시험 — 화면 확인은 ORCA.
|
||||
|
||||
· 부품마다 파일 머리(꼴 · 크기 · 상태 · 쓰기 · 대신) · 클래스 `ui-<부품>` · 옛 공용 이름 그대로
|
||||
· 크기 = data-size(setUiSize) · 단 값(--ui-size-*)만 읽음 · 상태 = :hover · :active 를 data-state 로도 받음
|
||||
· 색 = 공통 값만 · 부품 모음 기본 묶음 13 줄 모두 새 파일 견본(옛 PageKit 견본 없음)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
UI = ROOT / "ui_template"
|
||||
|
||||
# 파일 · 만들기 함수 · 클래스
|
||||
PARTS = {
|
||||
"input": ("createInputField", "ui-input"),
|
||||
"input ": ("createSelectField", "ui-select"),
|
||||
"input ": ("createSlider", "ui-slider"),
|
||||
"checkbox": ("createCheckbox", "ui-checkbox"),
|
||||
"toggle": ("createToggle", "ui-toggle"),
|
||||
"segment": ("createSegment", "ui-segment"),
|
||||
"tabs": ("createTabs", "ui-tabs"),
|
||||
"badge": ("createBadge", "ui-badge"),
|
||||
"hover_info": ("tooltipBox", "ui-hover-info"),
|
||||
"text": ("createText", "ui-text"),
|
||||
}
|
||||
|
||||
|
||||
def _text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_파일_머리_이름():
|
||||
for name, (fn, cls) in PARTS.items():
|
||||
ts = _text(UI / f"ui_template_{name.strip()}.ts")
|
||||
assert f"export function {fn}(" in ts, fn
|
||||
assert cls in ts, cls
|
||||
head = ts[: ts.index("=====", 100)]
|
||||
for word in ("꼴", "크기", "상태", "쓰기", "대신"):
|
||||
assert word in head, (name, word)
|
||||
|
||||
|
||||
def test_옛_이름_그대로():
|
||||
inp = _text(UI / "ui_template_input.ts")
|
||||
for cls in (
|
||||
'"ui-field"',
|
||||
'"ui-field__label"',
|
||||
"ui-input ui-input--sm",
|
||||
"ui-select ui-select--sm",
|
||||
):
|
||||
assert cls in inp, cls
|
||||
assert '"ui-field__error"' in inp and "ui-input--error" in inp
|
||||
elements = _text(UI / "ui_template_elements.ts")
|
||||
assert 'export * from "./ui_template_input";' in elements
|
||||
assert "export function createInputField" not in elements # 한 벌
|
||||
# 보조 탭 = 옛 .ui-subtabs + 알약 단추 · 배지 = 옛 .ui-tag 같이
|
||||
assert "ui-tabs ui-tabs--pill ui-subtabs" in _text(UI / "ui_template_tabs.ts")
|
||||
assert "ui-tag ui-tag--${tone}" in _text(UI / "ui_template_badge.ts")
|
||||
# 체크 클래스 그대로
|
||||
assert "ui-checkbox__input" in _text(UI / "ui_template_checkbox.ts")
|
||||
|
||||
|
||||
def test_크기_단():
|
||||
for name in ("input", "checkbox", "toggle", "segment", "tabs", "badge"):
|
||||
assert "setUiSize(" in _text(UI / f"ui_template_{name}.ts"), name
|
||||
for name in ("toggle", "segment", "tabs", "badge", "checkbox"):
|
||||
css = _text(UI / f"ui_template_{name}.css")
|
||||
assert "var(--ui-size-h)" in css, name
|
||||
inp = _text(UI / "ui_template_input.css")
|
||||
assert ".ui-input[data-size]:not(textarea)" in inp and "height: var(--ui-size-h)" in inp
|
||||
# 옛 compact 는 단을 안 붙임(옛 30 그대로) — 크기를 준 칸만 단
|
||||
ts = _text(UI / "ui_template_input.ts")
|
||||
assert "if (opts.size) setUiSize(input, opts.size);" in ts
|
||||
|
||||
|
||||
def test_상태():
|
||||
need = {
|
||||
"input": ['[data-state="hover"]', '[aria-invalid="true"]', ":disabled"],
|
||||
"checkbox": ['[data-state="hover"]', ":disabled"],
|
||||
"toggle": [
|
||||
'[data-state="hover"]',
|
||||
'[data-state="active"]',
|
||||
":disabled",
|
||||
'aria-pressed="true"',
|
||||
],
|
||||
"segment": [
|
||||
'[data-state="hover"]',
|
||||
'[data-state="active"]',
|
||||
":disabled",
|
||||
'aria-checked="true"',
|
||||
],
|
||||
"tabs": ['[data-state="hover"]', ":disabled", ".is-active"],
|
||||
}
|
||||
for name, words in need.items():
|
||||
css = _text(UI / f"ui_template_{name}.css")
|
||||
for w in words:
|
||||
assert w in css, (name, w)
|
||||
# 램프 · 상태 글 = 기호 같이(색만으로 안 가름)
|
||||
assert 'success: "✓", danger: "✗"' in _text(UI / "ui_template_badge.ts")
|
||||
assert 'error: "✗", warning: "!", success: "✓"' in _text(UI / "ui_template_text.ts")
|
||||
|
||||
|
||||
def test_날_색_없음():
|
||||
for name in ("input", "checkbox", "toggle", "segment", "tabs", "badge", "text", "hover_info"):
|
||||
css = _text(UI / f"ui_template_{name}.css")
|
||||
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, name
|
||||
assert (
|
||||
len(css.splitlines()) <= 700
|
||||
and len(_text(UI / f"ui_template_{name}.ts").splitlines()) <= 700
|
||||
)
|
||||
# 고르기 화살표 = 보조 글 색 값(그림 파일 속) · 옛 보라 없음
|
||||
styles = _text(UI / "ui_template_elements_styles.ts")
|
||||
assert "%233e0079" not in styles
|
||||
assert "stroke='%236b7280'" in styles and "stroke='%239aa1ab'" in styles
|
||||
theme = _text(UI / "ui_template_theme.css")
|
||||
assert "--color-slate: #6b7280;" in theme and "--color-slate: #9aa1ab;" in theme
|
||||
|
||||
|
||||
def test_부품_모음_기본_13():
|
||||
src = _text(UI / "ui_template_ui_kit_parts.ts")
|
||||
rows = re.findall(r'\{ key: "([^"]+)", name: "[^"]+", group: "basic",([^\n]*)', src)
|
||||
assert len(rows) == 13 # 목록 3장 1 ~ 13(체크 · 라디오 한 줄)
|
||||
for key, rest in rows:
|
||||
assert "legacy" not in rest, key
|
||||
block = src[src.index(f'{{ key: "{key}"') :].split("\n { key:")[0]
|
||||
assert "demo" in block, key
|
||||
assert "tabBar" not in src
|
||||
@@ -0,0 +1,81 @@
|
||||
/* 부품 「배지」(552-2) — 대시보드 알약 꼴 · 색 = 공통 값 · 옛 .ui-tag 꼴 위에 덮음(.ui-badge 붙은 것만) */
|
||||
.ui-badge {
|
||||
--badge-tone: var(--color-text-secondary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
.ui-badge--accent {
|
||||
--badge-tone: var(--color-accent);
|
||||
}
|
||||
.ui-badge--success {
|
||||
--badge-tone: var(--color-success);
|
||||
}
|
||||
.ui-badge--warning {
|
||||
--badge-tone: var(--color-warning);
|
||||
}
|
||||
.ui-badge--danger {
|
||||
--badge-tone: var(--color-danger);
|
||||
}
|
||||
.ui-badge--info {
|
||||
--badge-tone: var(--color-info);
|
||||
}
|
||||
|
||||
/* 알약 — 높이 = 단 · 연한 바탕 + 진한 글(진행 = 선만) */
|
||||
.ui-badge.ui-badge--pill {
|
||||
height: var(--ui-size-h);
|
||||
padding: 0 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-pills);
|
||||
background: color-mix(in srgb, var(--badge-tone) 14%, transparent);
|
||||
color: var(--badge-tone);
|
||||
font-size: var(--ui-size-font);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
.ui-badge.ui-badge--pill.ui-badge--neutral {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
.ui-badge.ui-badge--pill.ui-badge--accent {
|
||||
border-color: var(--color-accent);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 수 — 빨강 동그라미(색은 tone 과 상관없이 위험 채움) */
|
||||
.ui-badge--count {
|
||||
justify-content: center;
|
||||
min-width: var(--ui-size-h);
|
||||
height: var(--ui-size-h);
|
||||
padding: 0 5px;
|
||||
border-radius: var(--radius-pills);
|
||||
background: var(--color-danger-fill);
|
||||
color: var(--color-canvas);
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
/* 램프 — 점 + 글 */
|
||||
.ui-badge--lamp {
|
||||
height: var(--ui-size-h);
|
||||
color: var(--badge-tone);
|
||||
font-size: var(--ui-size-font);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
.ui-badge--lamp::before {
|
||||
content: "";
|
||||
flex: none;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--badge-tone);
|
||||
}
|
||||
|
||||
/* 점 — 글 없음 */
|
||||
.ui-badge--dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--badge-tone);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* =============================================================================
|
||||
* ui_template_badge.ts
|
||||
* 부품 「배지」(552-2 · 부품 모음 `#/b01-account?view=ui-kit`) — 대시보드 `.b01-pg-badge` 꼴
|
||||
*
|
||||
* 꼴 = 알약(pill · 상태 · 역할 · 종류 — 한글 글 + 색) · 수(count · 빨강 동그라미 · 알림) · 램프(lamp · 점 + 글 · 성공 ✓ · 실패 ✗ 같이)
|
||||
* · 점(dot · 글 없음 · title 필수)
|
||||
* 색 = accent(진행 · 선) · success · warning · danger · info · neutral
|
||||
* 크기 = data-size 18 · 20(기본 20) · 점 8
|
||||
* 상태 = 없음(눌리지 않음)
|
||||
* 쓰기 = `createBadge("진행 중", "accent")` · `createBadge("3", "danger", { kind: "count" })` · `createBadge("통과", "success", { kind: "lamp" })`
|
||||
* 대신 = `.ui-tag--*` 손 조립 · B01 `.b01-pg-badge` · `.b01-proj-badge` · 램프 `b08-summary__lamp` · `b09-bill__lamp` · E03 `e03-verdict` ·
|
||||
* M02 알약 여섯 …(목록 4장)
|
||||
* 클래스 = 알약은 옛 `ui-tag ui-tag--<색>` 도 같이 냄(시험 · B09 표 PDF 가 잡음)
|
||||
* ========================================================================== */
|
||||
|
||||
import { setUiSize } from "./ui_template_button";
|
||||
import "./ui_template_badge.css";
|
||||
|
||||
export type BadgeTone = "accent" | "success" | "warning" | "danger" | "info" | "neutral";
|
||||
export type BadgeKind = "pill" | "count" | "lamp" | "dot";
|
||||
|
||||
/** 램프 글자 기호 — 색만으로 안 가름(시험이 글자로 잡음) */
|
||||
const LAMP_MARK: Partial<Record<BadgeTone, string>> = { success: "✓", danger: "✗" };
|
||||
|
||||
export function createBadge(
|
||||
text: string,
|
||||
tone: BadgeTone = "neutral",
|
||||
opts: { kind?: BadgeKind; size?: 18 | 20 } = {},
|
||||
): HTMLSpanElement {
|
||||
const kind = opts.kind ?? "pill";
|
||||
const span = document.createElement("span");
|
||||
span.className = `ui-badge ui-badge--${kind} ui-badge--${tone}${kind === "pill" ? ` ui-tag ui-tag--${tone}` : ""}`;
|
||||
if (kind !== "dot") setUiSize(span, opts.size ?? 20);
|
||||
if (kind === "dot") {
|
||||
span.title = text;
|
||||
span.setAttribute("aria-label", text);
|
||||
} else if (kind === "lamp") {
|
||||
const mark = LAMP_MARK[tone];
|
||||
span.textContent = mark ? `${mark} ${text}` : text;
|
||||
} else {
|
||||
span.textContent = text;
|
||||
}
|
||||
return span;
|
||||
}
|
||||
@@ -32,3 +32,44 @@
|
||||
color: var(--color-text-muted);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 상태 — 마우스(552-2) */
|
||||
.ui-checkbox:is(:hover, [data-state="hover"]):not(:has(.ui-checkbox__input:disabled)) {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.ui-checkbox:is(:hover, [data-state="hover"]) .ui-checkbox__input:not(:disabled) {
|
||||
filter: brightness(0.92);
|
||||
}
|
||||
|
||||
/* 칩 꼴 — 알약 · 켜면 강조 바탕 · 상자는 숨김(키보드 · 보조기기는 그대로) */
|
||||
.ui-checkbox--chip {
|
||||
position: relative;
|
||||
height: var(--ui-size-h);
|
||||
padding-inline: var(--ui-size-px);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-pills);
|
||||
background: var(--color-surface-raised);
|
||||
font-size: var(--ui-size-font);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.ui-checkbox--chip .ui-checkbox__input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ui-checkbox--chip:is(:hover, [data-state="hover"]):not(:has(.ui-checkbox__input:disabled)) {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
.ui-checkbox--chip:has(.ui-checkbox__input:checked) {
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
.ui-checkbox--chip:has(.ui-checkbox__input:focus-visible) {
|
||||
outline: 2px solid var(--color-focus-ring);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.ui-checkbox--chip:has(.ui-checkbox__input:disabled) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
/* =============================================================================
|
||||
* ui_template_checkbox.ts
|
||||
* 공용 체크박스 — [□ 글] 한 줄 · 색은 테마 토큰(밝음 · 어두움 같이). 줄 세우기는 부르는 쪽 몫.
|
||||
* 부품 「체크 · 라디오」(552-2 · 부품 모음 `#/b01-account?view=ui-kit`) — [□ 글] 한 줄 · 줄 세우기는 부르는 쪽 몫.
|
||||
*
|
||||
* const box = createCheckbox("등고선", { checked: true, value: "contour" });
|
||||
* panel.append(box.root); // 읽기 = box.input.checked · box.input.value
|
||||
* 꼴 = 체크 · 라디오(radio + name) · 칩(chip — 켜면 채움 · 테두리 알약)
|
||||
* 크기 = 상자 14 · 칩은 data-size(기본 24)
|
||||
* 상태 = 마우스 `:hover` · 막힘 `disabled` — CSS 는 `[data-state]` 로도 받음
|
||||
* 쓰기 = `const box = createCheckbox("등고선", { checked: true, value: "contour" }); panel.append(box.root);`
|
||||
* 읽기 = box.input.checked · box.input.value · 라디오 = `createCheckbox("밝음", { radio: "theme", value: "light" })`
|
||||
* 대신 = 날 체크(시트 · B01 · A07 · D03 · D04 · E03 `e03-check` · M02 …) · `.app-settings__radios` · `m02-lib__pick` · `b05-route__check`
|
||||
* 클래스 = `ui-checkbox` · `ui-checkbox__input` · `ui-checkbox__text` — 시험 · 가이드 촬영이 잡음
|
||||
* ========================================================================== */
|
||||
|
||||
import { setUiSize, type UiSize } from "./ui_template_button";
|
||||
import "./ui_template_checkbox.css";
|
||||
|
||||
export interface Checkbox {
|
||||
@@ -13,17 +19,31 @@ export interface Checkbox {
|
||||
input: HTMLInputElement;
|
||||
}
|
||||
|
||||
export function createCheckbox(
|
||||
text: string,
|
||||
options: { checked?: boolean; value?: string } = {},
|
||||
): Checkbox {
|
||||
export interface CheckboxOptions {
|
||||
checked?: boolean;
|
||||
value?: string;
|
||||
/** 라디오 묶음 이름 — 주면 라디오 */
|
||||
radio?: string;
|
||||
/** 칩 꼴(켜면 채움) */
|
||||
chip?: boolean;
|
||||
/** 칩 크기 단(기본 24) */
|
||||
size?: UiSize;
|
||||
disabled?: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export function createCheckbox(text: string, options: CheckboxOptions = {}): Checkbox {
|
||||
const root = document.createElement("label");
|
||||
root.className = "ui-checkbox";
|
||||
root.className = `ui-checkbox${options.radio ? " ui-checkbox--radio" : ""}${options.chip ? " ui-checkbox--chip" : ""}`;
|
||||
if (options.chip) setUiSize(root, options.size ?? 24);
|
||||
const input = document.createElement("input");
|
||||
input.type = "checkbox";
|
||||
input.type = options.radio ? "radio" : "checkbox";
|
||||
if (options.radio) input.name = options.radio;
|
||||
input.className = "ui-checkbox__input";
|
||||
input.checked = options.checked ?? false;
|
||||
input.disabled = options.disabled ?? false;
|
||||
if (options.value !== undefined) input.value = options.value;
|
||||
if (options.onChange) input.addEventListener("change", () => options.onChange!(input.checked));
|
||||
const caption = document.createElement("span");
|
||||
caption.className = "ui-checkbox__text";
|
||||
caption.textContent = text;
|
||||
|
||||
@@ -18,6 +18,7 @@ export * from "./ui_template_elements_chart";
|
||||
export * from "./ui_template_elements_styles";
|
||||
export { el } from "./ui_template_elements_base";
|
||||
export * from "./ui_template_button";
|
||||
export * from "./ui_template_input";
|
||||
|
||||
/** 요소 생성 + 속성/클래스/자식 일괄 설정 헬퍼 */
|
||||
|
||||
@@ -26,132 +27,9 @@ export * from "./ui_template_button";
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 2. 입력 필드 (Input Field) — design.md: 1px mist border, 8px radius, focus ring
|
||||
* 2. 입력 · 고르기 — 부품 「입력」 · 「고르기」 · 「슬라이더」는 `ui_template_input.ts`(552-2) · 부르는 길은 그대로
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
export interface InputFieldOptions {
|
||||
/** 라벨 텍스트 (i18n 결과) */
|
||||
label?: string;
|
||||
/** placeholder (i18n 결과) */
|
||||
placeholder?: string;
|
||||
type?: "text" | "password" | "email" | "number" | "search" | "tel" | "date";
|
||||
value?: string;
|
||||
required?: boolean;
|
||||
/** number 타입 범위 (1차 유효성 검사용, frontend.md §4) */
|
||||
min?: number;
|
||||
max?: number;
|
||||
onInput?: (value: string) => void;
|
||||
/** 좁은 자리(표 칸·좌측 패널)용 작은 크기 — 작은 고르기칸(compact)과 높이가 같음 */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export interface InputFieldHandle {
|
||||
/** 필드 컨테이너 (라벨 + 입력 + 에러 슬롯) */
|
||||
root: HTMLDivElement;
|
||||
input: HTMLInputElement;
|
||||
/** 에러 메시지 표시/해제. msg가 없으면 에러 해제. */
|
||||
setError: (msg?: string) => void;
|
||||
}
|
||||
|
||||
export function createInputField(opts: InputFieldOptions): InputFieldHandle {
|
||||
const root = el("div", { className: "ui-field" });
|
||||
|
||||
if (opts.label) {
|
||||
root.append(el("label", { className: "ui-field__label", text: opts.label }));
|
||||
}
|
||||
|
||||
const input = el("input", {
|
||||
className: opts.compact ? "ui-input ui-input--sm" : "ui-input",
|
||||
attrs: {
|
||||
type: opts.type ?? "text",
|
||||
...(opts.placeholder ? { placeholder: opts.placeholder } : {}),
|
||||
...(opts.required ? { required: "true" } : {}),
|
||||
...(opts.min !== undefined ? { min: String(opts.min) } : {}),
|
||||
...(opts.max !== undefined ? { max: String(opts.max) } : {}),
|
||||
},
|
||||
});
|
||||
if (opts.value !== undefined) input.value = opts.value;
|
||||
if (opts.onInput) {
|
||||
input.addEventListener("input", () => opts.onInput!(input.value));
|
||||
}
|
||||
|
||||
// 에러 메시지 슬롯 (입력창 하단 표기, frontend.md §4)
|
||||
const errorSlot = el("span", { className: "ui-field__error", attrs: { role: "alert" } });
|
||||
|
||||
root.append(input, errorSlot);
|
||||
|
||||
const setError = (msg?: string): void => {
|
||||
if (msg) {
|
||||
input.classList.add("ui-input--error");
|
||||
errorSlot.textContent = msg;
|
||||
errorSlot.classList.add("is-visible");
|
||||
} else {
|
||||
input.classList.remove("ui-input--error");
|
||||
errorSlot.textContent = "";
|
||||
errorSlot.classList.remove("is-visible");
|
||||
}
|
||||
};
|
||||
|
||||
return { root, input, setError };
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 2-1. 드롭다운 필드 (Select Field) — design.md: input과 조화되는 8px radius, focus
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
export interface SelectFieldOptions {
|
||||
/** 라벨 텍스트 */
|
||||
label?: string;
|
||||
/** 옵션 항목 목록 */
|
||||
options: { value: string; text: string }[];
|
||||
/** 초기 선택값 */
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
/** 좁은 자리(표 칸·좌측 패널)용 작은 여백 */
|
||||
compact?: boolean;
|
||||
/** 라벨이 없을 때 보조기기용 이름 */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface SelectFieldHandle {
|
||||
root: HTMLDivElement;
|
||||
select: HTMLSelectElement;
|
||||
/** 선택지를 다시 채움 — value 가 목록에 없으면 첫 항목 */
|
||||
setOptions: (options: { value: string; text: string }[], value?: string) => void;
|
||||
}
|
||||
|
||||
export function createSelectField(opts: SelectFieldOptions): SelectFieldHandle {
|
||||
const root = el("div", { className: "ui-field" });
|
||||
|
||||
if (opts.label) {
|
||||
root.append(el("label", { className: "ui-field__label", text: opts.label }));
|
||||
}
|
||||
|
||||
const select = el("select", {
|
||||
className: opts.compact ? "ui-select ui-select--sm" : "ui-select",
|
||||
attrs: opts.ariaLabel ? { "aria-label": opts.ariaLabel } : {},
|
||||
});
|
||||
|
||||
const setOptions: SelectFieldHandle["setOptions"] = (options, value) => {
|
||||
select.replaceChildren(
|
||||
...options.map((opt) => el("option", { attrs: { value: opt.value }, text: opt.text })),
|
||||
);
|
||||
if (value !== undefined && options.some((opt) => opt.value === value)) select.value = value;
|
||||
};
|
||||
setOptions(opts.options, opts.value);
|
||||
|
||||
if (opts.disabled) select.disabled = true;
|
||||
|
||||
if (opts.onChange) {
|
||||
select.addEventListener("change", () => opts.onChange!(select.value));
|
||||
}
|
||||
|
||||
root.append(select);
|
||||
|
||||
return { root, select, setOptions };
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 3. 카드 / 패널 (Card) — design.md: white surface, 8px radius, shadow
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
@@ -143,13 +143,17 @@ const BASE_CSS = `
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-inputs);
|
||||
padding: 10px 36px 10px 14px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%233e0079'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E");
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
/* 화살표 = 셰브런 · 선 색 = --color-text-secondary 값(밝음 #6b7280 · 어두움 #9aa1ab · 그림 파일 속이라 변수를 못 씀) */
|
||||
:root[data-theme="dark"] .ui-select {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%239aa1ab'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E");
|
||||
}
|
||||
.ui-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-focus-ring);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--color-text-body);
|
||||
font-size: 12px;
|
||||
font-size: var(--text-caption);
|
||||
line-height: 1.4;
|
||||
white-space: pre-line;
|
||||
overflow-wrap: anywhere;
|
||||
@@ -23,3 +23,9 @@
|
||||
.ui-hover-info[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 제자리 상자(tooltipBox · 552-2) — 흐름 안 · 그림 위 설명은 부르는 쪽이 absolute 로 놓음 */
|
||||
.ui-hover-info--inline {
|
||||
position: static;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
* 상자 = 화면에 하나(새로 뜨면 앞 것 갈음) · 대상 아래가 기본 · 아래가 모자라면 위로 뒤집고
|
||||
* 좌우는 화면 안으로 당김(`placeHoverInfo`) · 색 = 테마 토큰(밝음 · 어두움 같이)
|
||||
* 옛 title 풍선은 그대로 둠 — 새 자리부터 이 양식(B09 칸 풍선 `tip` 이 첫 사용처)
|
||||
*
|
||||
* 부품 「툴팁」(552-2 · 부품 모음 `#/b01-account?view=ui-kit`)
|
||||
* 꼴 = 마우스 글(attachHoverInfo) · 값 풍선 · 그림 위 설명(tooltipBox — 제자리 상자)
|
||||
* 크기 = 글 12 · 폭 최대 320 · 상태 = 없음
|
||||
* 쓰기 = `attachHoverInfo(cell, "누르면 고침")` · `panel.append(tooltipBox("구간 1 · 높이 2.4 m"))`
|
||||
* 대신 = `title=`(B06 52 · CAD 32 · B07 20) · `.ui-prov-card` · 시트 `.ss-errtip__balloon` · B05 `b05-structure__balloon` ·
|
||||
* E03 `e03-tip` · E04 · E07 `*-map__tip`
|
||||
* ========================================================================== */
|
||||
|
||||
import "./ui_template_hover_info.css";
|
||||
@@ -139,3 +146,12 @@ export function attachHoverInfo(root: HTMLElement, text: HoverText, selector?: s
|
||||
if (owner && root.contains(owner)) hideHoverInfo();
|
||||
};
|
||||
}
|
||||
|
||||
/** 제자리 툴팁 상자 — 그림 위 설명 · 값 풍선(위치는 부르는 쪽 몫 · 마우스를 안 막음) */
|
||||
export function tooltipBox(text: string): HTMLDivElement {
|
||||
const tip = document.createElement("div");
|
||||
tip.className = "ui-hover-info ui-hover-info--inline";
|
||||
tip.setAttribute("role", "tooltip");
|
||||
tip.textContent = text;
|
||||
return tip;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/* =============================================================================
|
||||
* ui_template_input.css — 입력 · 고르기 크기 단 · 상태 · 단위 붙음 · 슬라이더(552-2)
|
||||
* 기본 꼴(42 · 여백 · 화살표)은 `ui_template_elements_styles.ts`(옛 자리 · 덮어쓰기 차례 그대로).
|
||||
* 단 값(--ui-size-*)은 `ui_template_button.css` 한 곳.
|
||||
* ========================================================================== */
|
||||
|
||||
/* 크기 단 — 높이를 박음(칸은 글 줄 높이가 브라우저마다 달라 여백 식으로는 안 맞음) */
|
||||
.ui-input[data-size]:not(textarea),
|
||||
.ui-select[data-size] {
|
||||
height: var(--ui-size-h);
|
||||
padding-block: 0;
|
||||
font-size: var(--ui-size-font);
|
||||
line-height: normal;
|
||||
}
|
||||
.ui-input[data-size] {
|
||||
padding-inline: 8px;
|
||||
}
|
||||
.ui-select[data-size] {
|
||||
padding-inline: 8px 26px;
|
||||
background-position: right 8px center;
|
||||
}
|
||||
.ui-input[data-size]:is([data-size="36"], [data-size="42"]) {
|
||||
padding-inline: 14px;
|
||||
}
|
||||
.ui-select[data-size]:is([data-size="36"], [data-size="42"]) {
|
||||
padding-inline: 14px 36px;
|
||||
background-position: right 12px center;
|
||||
}
|
||||
.ui-input[data-size]:is([data-size="18"], [data-size="20"], [data-size="22"], [data-size="24"]),
|
||||
.ui-select[data-size]:is([data-size="18"], [data-size="20"], [data-size="22"], [data-size="24"]) {
|
||||
border-radius: var(--ui-size-radius);
|
||||
}
|
||||
textarea.ui-input {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* 상태 — 마우스 · 막힘 · 오류(옛 --error 같이) */
|
||||
.ui-input:is(:hover, [data-state="hover"]):not(:disabled, :focus),
|
||||
.ui-select:is(:hover, [data-state="hover"]):not(:disabled, :focus) {
|
||||
border-color: var(--color-border-strong);
|
||||
}
|
||||
.ui-input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ui-input:is(.is-error, [aria-invalid="true"]):not(:focus),
|
||||
.ui-select:is(.is-error, [aria-invalid="true"]):not(:focus) {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
/* 단위 붙음 — 칸 안 오른쪽 글 · 칸 폭은 부르는 쪽 몫 */
|
||||
.ui-input-unit {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
.ui-input-unit > .ui-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: calc(8px + 2.5em);
|
||||
}
|
||||
.ui-input-unit__text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
transform: translateY(-50%);
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-secondary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 슬라이더 — [이름 | 막대 | 값 글] 3열 */
|
||||
.ui-slider {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(40px, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: var(--ui-size-h);
|
||||
font-size: var(--text-caption);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.ui-slider__range {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
accent-color: var(--color-accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-slider__range:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
accent-color: var(--color-primary-hover);
|
||||
}
|
||||
.ui-slider__range:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ui-slider__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ui-slider__value {
|
||||
min-width: 3em;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--color-text);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/* =============================================================================
|
||||
* ui_template_input.ts
|
||||
* 부품 「입력」 · 「고르기」 · 「슬라이더」(552-2 · 부품 모음 `#/b01-account?view=ui-kit`)
|
||||
*
|
||||
* 꼴 = 입력: 글 · 수 · 찾기 · 날짜 · 비밀번호 · 단위 붙음(unit) · 여러 줄(multiline) / 고르기: 한 개 · 화살표 = 셰브런
|
||||
* 슬라이더: 이름 · 막대 · 값 글 3열 한 줄
|
||||
* 크기 = `size: 24 | 28 | 32 | 42 …`(data-size · 여덟 단) · 크기를 안 주면 옛 꼴(42) · 옛 `compact` = 옛 작은 칸(30) 그대로
|
||||
* 상태 = 마우스 `:hover` · 막힘 `disabled` · 오류 `.is-error` / `aria-invalid` / setError — CSS 는 `[data-state]` 로도 받음
|
||||
* 쓰기 = `createInputField({ label: "폭", type: "number", unit: "m", size: 28 })` · `createSelectField({ options, size: 28 })`
|
||||
* `createSlider({ label: "투명도", min: 0, max: 100, value: 60, format: (v) => `${v}%` })`
|
||||
* 대신 = `.ui-panel-form input`(26) · `b01-sys-input` · `m02-df*__input` · `b04-surface__select` · `viewer-option-row` …(목록 4장)
|
||||
* 클래스 = `ui-field` · `ui-field__label` · `ui-input(--sm · --error)` · `ui-select(--sm)` · `ui-field__error` — 시험 · 가이드 촬영이 잡음
|
||||
* ========================================================================== */
|
||||
|
||||
import { setUiSize, type UiSize } from "./ui_template_button";
|
||||
import { el } from "./ui_template_elements_base";
|
||||
import "./ui_template_input.css";
|
||||
|
||||
export interface InputFieldOptions {
|
||||
/** 라벨 텍스트 (i18n 결과) */
|
||||
label?: string;
|
||||
/** placeholder (i18n 결과) */
|
||||
placeholder?: string;
|
||||
type?: "text" | "password" | "email" | "number" | "search" | "tel" | "date";
|
||||
value?: string;
|
||||
required?: boolean;
|
||||
/** number 타입 범위 (1차 유효성 검사용, frontend.md §4) */
|
||||
min?: number;
|
||||
max?: number;
|
||||
onInput?: (value: string) => void;
|
||||
/** 좁은 자리(표 칸·좌측 패널)용 작은 크기 — 작은 고르기칸(compact)과 높이가 같음 */
|
||||
compact?: boolean;
|
||||
/** 크기 단(높이 px) */
|
||||
size?: UiSize;
|
||||
/** 칸 안 오른쪽 단위 글(m · % · m³) */
|
||||
unit?: string;
|
||||
/** 여러 줄 — textarea(줄 수) */
|
||||
multiline?: number;
|
||||
}
|
||||
|
||||
export interface InputFieldHandle {
|
||||
/** 필드 컨테이너 (라벨 + 입력 + 에러 슬롯) */
|
||||
root: HTMLDivElement;
|
||||
input: HTMLInputElement;
|
||||
/** 에러 메시지 표시/해제. msg가 없으면 에러 해제. */
|
||||
setError: (msg?: string) => void;
|
||||
}
|
||||
|
||||
export function createInputField(opts: InputFieldOptions): InputFieldHandle {
|
||||
const root = el("div", { className: "ui-field" });
|
||||
|
||||
if (opts.label) {
|
||||
root.append(el("label", { className: "ui-field__label", text: opts.label }));
|
||||
}
|
||||
|
||||
const attrs: Record<string, string> = {
|
||||
...(opts.placeholder ? { placeholder: opts.placeholder } : {}),
|
||||
...(opts.required ? { required: "true" } : {}),
|
||||
};
|
||||
// 여러 줄은 textarea — 값 · 이벤트 · 오류 손잡이는 input 과 같은 길(value · placeholder)
|
||||
const input = (
|
||||
opts.multiline
|
||||
? el("textarea", { className: "ui-input", attrs: { ...attrs, rows: String(opts.multiline) } })
|
||||
: el("input", {
|
||||
className: opts.compact ? "ui-input ui-input--sm" : "ui-input",
|
||||
attrs: {
|
||||
type: opts.type ?? "text",
|
||||
...attrs,
|
||||
...(opts.min !== undefined ? { min: String(opts.min) } : {}),
|
||||
...(opts.max !== undefined ? { max: String(opts.max) } : {}),
|
||||
},
|
||||
})
|
||||
) as HTMLInputElement;
|
||||
if (opts.size) setUiSize(input, opts.size);
|
||||
if (opts.value !== undefined) input.value = opts.value;
|
||||
if (opts.onInput) {
|
||||
input.addEventListener("input", () => opts.onInput!(input.value));
|
||||
}
|
||||
|
||||
// 에러 메시지 슬롯 (입력창 하단 표기, frontend.md §4)
|
||||
const errorSlot = el("span", { className: "ui-field__error", attrs: { role: "alert" } });
|
||||
|
||||
if (opts.unit) {
|
||||
const wrap = el("div", { className: "ui-input-unit" });
|
||||
wrap.append(input, el("span", { className: "ui-input-unit__text", text: opts.unit }));
|
||||
root.append(wrap, errorSlot);
|
||||
} else {
|
||||
root.append(input, errorSlot);
|
||||
}
|
||||
|
||||
const setError = (msg?: string): void => {
|
||||
if (msg) {
|
||||
input.classList.add("ui-input--error");
|
||||
input.setAttribute("aria-invalid", "true");
|
||||
errorSlot.textContent = msg;
|
||||
errorSlot.classList.add("is-visible");
|
||||
} else {
|
||||
input.classList.remove("ui-input--error");
|
||||
input.removeAttribute("aria-invalid");
|
||||
errorSlot.textContent = "";
|
||||
errorSlot.classList.remove("is-visible");
|
||||
}
|
||||
};
|
||||
|
||||
return { root, input, setError };
|
||||
}
|
||||
|
||||
export interface SelectFieldOptions {
|
||||
/** 라벨 텍스트 */
|
||||
label?: string;
|
||||
/** 옵션 항목 목록 */
|
||||
options: { value: string; text: string }[];
|
||||
/** 초기 선택값 */
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
/** 좁은 자리(표 칸·좌측 패널)용 작은 여백 */
|
||||
compact?: boolean;
|
||||
/** 크기 단(높이 px) */
|
||||
size?: UiSize;
|
||||
/** 라벨이 없을 때 보조기기용 이름 */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export interface SelectFieldHandle {
|
||||
root: HTMLDivElement;
|
||||
select: HTMLSelectElement;
|
||||
/** 선택지를 다시 채움 — value 가 목록에 없으면 첫 항목 */
|
||||
setOptions: (options: { value: string; text: string }[], value?: string) => void;
|
||||
}
|
||||
|
||||
export function createSelectField(opts: SelectFieldOptions): SelectFieldHandle {
|
||||
const root = el("div", { className: "ui-field" });
|
||||
|
||||
if (opts.label) {
|
||||
root.append(el("label", { className: "ui-field__label", text: opts.label }));
|
||||
}
|
||||
|
||||
const select = el("select", {
|
||||
className: opts.compact ? "ui-select ui-select--sm" : "ui-select",
|
||||
attrs: opts.ariaLabel ? { "aria-label": opts.ariaLabel } : {},
|
||||
});
|
||||
if (opts.size) setUiSize(select, opts.size);
|
||||
|
||||
const setOptions: SelectFieldHandle["setOptions"] = (options, value) => {
|
||||
select.replaceChildren(
|
||||
...options.map((opt) => el("option", { attrs: { value: opt.value }, text: opt.text })),
|
||||
);
|
||||
if (value !== undefined && options.some((opt) => opt.value === value)) select.value = value;
|
||||
};
|
||||
setOptions(opts.options, opts.value);
|
||||
|
||||
if (opts.disabled) select.disabled = true;
|
||||
|
||||
if (opts.onChange) {
|
||||
select.addEventListener("change", () => opts.onChange!(select.value));
|
||||
}
|
||||
|
||||
root.append(select);
|
||||
|
||||
return { root, select, setOptions };
|
||||
}
|
||||
|
||||
export interface SliderOptions {
|
||||
label: string;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
value: number;
|
||||
/** 값 글 — 없으면 수 그대로 */
|
||||
format?: (value: number) => string;
|
||||
onInput?: (value: number) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SliderHandle {
|
||||
root: HTMLDivElement;
|
||||
input: HTMLInputElement;
|
||||
/** 값을 바꾸고 값 글을 맞춤(onInput 은 안 부름) */
|
||||
set: (value: number) => void;
|
||||
}
|
||||
|
||||
/** 슬라이더 — [이름 | 막대 | 값 글] 한 줄(높이 24) */
|
||||
export function createSlider(opts: SliderOptions): SliderHandle {
|
||||
const root = el("div", { className: "ui-slider" });
|
||||
setUiSize(root, 24);
|
||||
const input = el("input", {
|
||||
className: "ui-slider__range",
|
||||
attrs: {
|
||||
type: "range",
|
||||
min: String(opts.min),
|
||||
max: String(opts.max),
|
||||
step: String(opts.step ?? 1),
|
||||
"aria-label": opts.label,
|
||||
},
|
||||
});
|
||||
const text = el("span", { className: "ui-slider__value" });
|
||||
const show = (): void => {
|
||||
text.textContent = opts.format ? opts.format(input.valueAsNumber) : input.value;
|
||||
};
|
||||
const set = (value: number): void => {
|
||||
input.value = String(value);
|
||||
show();
|
||||
};
|
||||
set(opts.value);
|
||||
if (opts.disabled) input.disabled = true;
|
||||
input.addEventListener("input", () => {
|
||||
show();
|
||||
opts.onInput?.(input.valueAsNumber);
|
||||
});
|
||||
root.append(el("span", { className: "ui-slider__label", text: opts.label }), input, text);
|
||||
return { root, input, set };
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* 부품 「분할 고르기」(552-2) — 대시보드 `.b01-sys-seg` 꼴 · 단 값은 ui_template_button.css */
|
||||
.ui-segment {
|
||||
display: inline-flex;
|
||||
height: var(--ui-size-h);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--ui-size-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ui-segment__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ui-size-gap);
|
||||
padding: 0 var(--ui-size-px);
|
||||
border: 0;
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text-secondary);
|
||||
font: inherit;
|
||||
font-size: var(--ui-size-font);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
color var(--transition-fast);
|
||||
}
|
||||
.ui-segment__item + .ui-segment__item {
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
.ui-segment__item svg {
|
||||
width: var(--ui-size-icon);
|
||||
height: var(--ui-size-icon);
|
||||
}
|
||||
.ui-segment__item:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background: var(--color-hover);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.ui-segment__item[aria-checked="true"] {
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
.ui-segment__item:is(:active, [data-state="active"]):not(:disabled) {
|
||||
box-shadow: inset 0 1px 2px var(--color-scrim);
|
||||
}
|
||||
.ui-segment__item:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/* =============================================================================
|
||||
* ui_template_segment.ts
|
||||
* 부품 「분할 고르기」(552-2 · 부품 모음 `#/b01-account?view=ui-kit`) — 붙은 칸 둘 ~ 다섯 중 하나
|
||||
*
|
||||
* 꼴 = 글 · 아이콘(+ title) · 글 + 아이콘
|
||||
* 크기 = data-size 20 · 24 · 28 · 32(기본 28)
|
||||
* 상태 = 마우스 `:hover` · 눌림 `:active` · 막힘 `disabled` · 고름 = aria-checked — CSS 는 `[data-state]` 로도 받음
|
||||
* 쓰기 = `createSegment({ options: [{ value: "list", label: "목록" }, { value: "card", label: "카드" }], value: "list", onChange })`
|
||||
* 대신 = B01 `.b01-sys-seg` · `.b01-home__seg` · `.b01-proj-seg` · B04 `b04-surface__sheet-method` · B05 `b05-routerec__pills` ·
|
||||
* B06 `b06-design__btn(--active)` · CAD `.cad-inspector-tabs` · E04 · E06 · E07 `*-toggle` · M02 `m02-dfip__chips`
|
||||
* ========================================================================== */
|
||||
|
||||
import { setUiSize, type UiSize } from "./ui_template_button";
|
||||
import { icon, type IconName } from "./ui_template_icons";
|
||||
import "./ui_template_segment.css";
|
||||
|
||||
export interface SegmentOption {
|
||||
value: string;
|
||||
label?: string;
|
||||
icon?: IconName;
|
||||
/** 아이콘만이면 필수 */
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SegmentHandle {
|
||||
root: HTMLDivElement;
|
||||
get: () => string;
|
||||
/** 고름을 바꿈(onChange 는 안 부름) */
|
||||
set: (value: string) => void;
|
||||
}
|
||||
|
||||
export function createSegment(opts: {
|
||||
options: SegmentOption[];
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
/** 20 · 24 · 28 · 32 권함 */
|
||||
size?: UiSize;
|
||||
ariaLabel?: string;
|
||||
}): SegmentHandle {
|
||||
const root = document.createElement("div");
|
||||
root.className = "ui-segment";
|
||||
root.setAttribute("role", "radiogroup");
|
||||
if (opts.ariaLabel) root.setAttribute("aria-label", opts.ariaLabel);
|
||||
setUiSize(root, opts.size ?? 28);
|
||||
let current = opts.value;
|
||||
const items = opts.options.map((o) => {
|
||||
const b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "ui-segment__item";
|
||||
b.setAttribute("role", "radio");
|
||||
b.dataset.value = o.value;
|
||||
if (o.icon) b.append(icon(o.icon, 16));
|
||||
if (o.label) b.append(o.label);
|
||||
if (o.title) b.title = o.title;
|
||||
if (o.title && !o.label) b.setAttribute("aria-label", o.title);
|
||||
b.disabled = o.disabled ?? false;
|
||||
b.addEventListener("click", () => {
|
||||
if (current === o.value) return;
|
||||
set(o.value);
|
||||
opts.onChange?.(o.value);
|
||||
});
|
||||
return b;
|
||||
});
|
||||
const set = (value: string): void => {
|
||||
current = value;
|
||||
for (const b of items) b.setAttribute("aria-checked", String(b.dataset.value === value));
|
||||
};
|
||||
set(current);
|
||||
root.append(...items);
|
||||
return { root, get: () => current, set };
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/* 부품 「탭」(552-2) — 밑줄 탭 = 대시보드 `.b01-pg-tabs` 꼴 · 보조 탭 = 작은 알약 · 단 값은 ui_template_button.css */
|
||||
.ui-tabs {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.ui-tabs__tab {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: var(--ui-size-h);
|
||||
padding: 0 12px;
|
||||
margin-bottom: -1px;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: none;
|
||||
color: var(--color-text-secondary);
|
||||
font: inherit;
|
||||
font-size: var(--ui-size-font);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-tabs__tab:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.ui-tabs__tab.is-active {
|
||||
color: var(--color-accent);
|
||||
border-bottom-color: var(--color-accent);
|
||||
}
|
||||
.ui-tabs__tab:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 보조 탭 — 밑줄 없음 · 고른 칸 = 강조 바탕 · 글 */
|
||||
.ui-tabs--pill {
|
||||
align-items: center;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.ui-tabs--pill > .ui-btn {
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.ui-tabs--pill > .ui-btn.is-active {
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
/* 수 배지 — 빨강 동그라미 */
|
||||
.ui-tabs__count {
|
||||
min-width: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: var(--color-danger-fill);
|
||||
color: var(--color-canvas);
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
.ui-tabs__count[hidden] {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/* =============================================================================
|
||||
* ui_template_tabs.ts
|
||||
* 부품 「탭」(552-2 · 부품 모음 `#/b01-account?view=ui-kit`)
|
||||
*
|
||||
* 꼴 = 밑줄 탭(line · 한 꼴 · 대시보드 `tabBar` 꼴) · 보조 탭(pill · 작은 알약 · `.ui-subtabs`) · 탭 오른쪽 수 배지(0 = 숨김)
|
||||
* 문서 탭(`cad-tabs` · `ss-tabs`)은 꼴 그대로 · 토큰만(553)
|
||||
* 크기 = data-size 24 · 32 · 36(밑줄 기본 36 · 보조 기본 24)
|
||||
* 상태 = 마우스 `:hover` · 막힘 `disabled` · 고름 = aria-selected + `.is-active` — CSS 는 `[data-state]` 로도 받음
|
||||
* 쓰기 = `const tabs = createTabs({ tabs: [{ key: "a", label: "개요" }], value: "a", onChange }); tabs.setBadge("a", 3)`
|
||||
* 대신 = B01 `.b01-pg-tabs` · 공용 `.ui-subtabs` · B08 `.b08-quantity__tabs` · B09 `.b09-estimation__tabs` · `app-settings__tabs` ·
|
||||
* M02 `m02-sod__tabs` · 시트 `.ss-fmtdlg__tabs`
|
||||
* 클래스 = 보조 탭 = `ui-subtabs` + 칸 `ui-btn ui-btn--pill`(옛 이름 · 시험이 잡음)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, setUiSize, type UiSize } from "./ui_template_button";
|
||||
import "./ui_template_tabs.css";
|
||||
|
||||
export interface TabItem {
|
||||
key: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface TabsHandle {
|
||||
root: HTMLDivElement;
|
||||
/** 고름을 바꿈(onChange 는 안 부름) */
|
||||
select: (key: string) => void;
|
||||
/** 탭 오른쪽 수 — 0 이하면 숨김 */
|
||||
setBadge: (key: string, count: number) => void;
|
||||
}
|
||||
|
||||
export function createTabs(opts: {
|
||||
tabs: TabItem[];
|
||||
value: string;
|
||||
onChange?: (key: string) => void;
|
||||
kind?: "line" | "pill";
|
||||
/** 24 · 32 · 36 권함 */
|
||||
size?: UiSize;
|
||||
}): TabsHandle {
|
||||
const pill = opts.kind === "pill";
|
||||
const root = document.createElement("div");
|
||||
root.className = pill ? "ui-tabs ui-tabs--pill ui-subtabs" : "ui-tabs";
|
||||
root.setAttribute("role", "tablist");
|
||||
setUiSize(root, opts.size ?? (pill ? 24 : 36));
|
||||
const buttons = new Map<string, HTMLButtonElement>();
|
||||
const badges = new Map<string, HTMLSpanElement>();
|
||||
const select = (key: string): void => {
|
||||
for (const [k, b] of buttons) {
|
||||
b.classList.toggle("is-active", k === key);
|
||||
b.setAttribute("aria-selected", String(k === key));
|
||||
}
|
||||
};
|
||||
for (const tab of opts.tabs) {
|
||||
const b = pill
|
||||
? createButton({ label: tab.label, variant: "pill", size: opts.size ?? 24 })
|
||||
: document.createElement("button");
|
||||
if (!pill) {
|
||||
b.type = "button";
|
||||
b.className = "ui-tabs__tab";
|
||||
b.append(tab.label);
|
||||
}
|
||||
b.setAttribute("role", "tab");
|
||||
b.dataset.tab = tab.key;
|
||||
b.disabled = tab.disabled ?? false;
|
||||
const count = document.createElement("span");
|
||||
count.className = "ui-tabs__count";
|
||||
count.hidden = true;
|
||||
b.append(count);
|
||||
b.addEventListener("click", () => {
|
||||
select(tab.key);
|
||||
opts.onChange?.(tab.key);
|
||||
});
|
||||
buttons.set(tab.key, b);
|
||||
badges.set(tab.key, count);
|
||||
root.append(b);
|
||||
}
|
||||
select(opts.value);
|
||||
return {
|
||||
root,
|
||||
select,
|
||||
setBadge: (key, n) => {
|
||||
const c = badges.get(key);
|
||||
if (!c) return;
|
||||
c.textContent = String(n);
|
||||
c.hidden = n <= 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/* 부품 「보조 글 · 안내 줄」(552-2) — 색 = 공통 값 · 글 크기 = --text-* */
|
||||
.ui-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
font-size: var(--text-caption);
|
||||
line-height: 1.4;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.ui-text[data-text="14"] {
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
.ui-text--hint {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.ui-text--error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.ui-text--warning {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
.ui-text--success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
.ui-text__mark {
|
||||
flex: none;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/* =============================================================================
|
||||
* ui_template_text.ts
|
||||
* 부품 「보조 글 · 안내 줄」(552-2 · 부품 모음 `#/b01-account?view=ui-kit`)
|
||||
*
|
||||
* 꼴 = 흐린 글(muted) · 안내(hint · ⓘ 아이콘) · 상태 글(error · warning · success — 글자 기호 ✗ ! ✓ 같이) · 칸 오류(`.ui-field__error`)
|
||||
* 크기 = 글 12(기본) · 14 — data-text 로 고름
|
||||
* 상태 = 오류 = error 꼴(색만으로 안 가름 · 기호 같이)
|
||||
* 쓰기 = `createText("저장 안 한 값이 있음", "warning")` · `createText("단위 m", "muted", 14)`
|
||||
* 대신 = 파일마다 `__muted`(약 30 벌) · `__hint` · `__status` · `__note` · `__warn` · `.b01-sys-error` · `b03-file__error` …(목록 4장)
|
||||
* ========================================================================== */
|
||||
|
||||
import { icon } from "./ui_template_icons";
|
||||
import "./ui_template_text.css";
|
||||
|
||||
export type TextKind = "muted" | "hint" | "error" | "warning" | "success";
|
||||
|
||||
/** 상태 글 앞 기호 — 색만으로 안 가름 */
|
||||
const MARK: Partial<Record<TextKind, string>> = { error: "✗", warning: "!", success: "✓" };
|
||||
|
||||
export function createText(
|
||||
text: string,
|
||||
kind: TextKind = "muted",
|
||||
size: 12 | 14 = 12,
|
||||
): HTMLElement {
|
||||
const p = document.createElement("p");
|
||||
p.className = `ui-text ui-text--${kind}`;
|
||||
p.dataset.text = String(size);
|
||||
if (kind === "hint") p.append(icon("info", size + 2));
|
||||
const mark = MARK[kind];
|
||||
if (mark) {
|
||||
const m = document.createElement("span");
|
||||
m.className = "ui-text__mark";
|
||||
m.textContent = mark;
|
||||
p.append(m);
|
||||
}
|
||||
p.append(text);
|
||||
if (kind === "error") p.setAttribute("role", "alert");
|
||||
return p;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/* 부품 「켜기」(552-2) — 스위치 · 켜기 칩 · 단 값은 ui_template_button.css */
|
||||
.ui-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ui-size-gap);
|
||||
height: var(--ui-size-h);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--color-text-body);
|
||||
font: inherit;
|
||||
font-size: var(--ui-size-font);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 스위치 막대 — 높이 = 단의 0.65 · 너비 = 높이 × 1.8 */
|
||||
.ui-toggle__track {
|
||||
position: relative;
|
||||
flex: none;
|
||||
width: calc(var(--ui-size-h) * 0.65 * 1.8);
|
||||
height: calc(var(--ui-size-h) * 0.65);
|
||||
border-radius: var(--radius-pills);
|
||||
background: var(--color-smoke);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
.ui-toggle__track::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: calc(var(--ui-size-h) * 0.65 - 4px);
|
||||
height: calc(var(--ui-size-h) * 0.65 - 4px);
|
||||
border-radius: 50%;
|
||||
background: var(--color-canvas);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
.ui-toggle[aria-checked="true"] .ui-toggle__track {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
.ui-toggle[aria-checked="true"] .ui-toggle__track::after {
|
||||
transform: translateX(calc(var(--ui-size-h) * 0.65 * 0.8));
|
||||
}
|
||||
.ui-toggle:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.ui-toggle:is(:hover, [data-state="hover"]):not(:disabled) .ui-toggle__track {
|
||||
filter: brightness(0.92);
|
||||
}
|
||||
|
||||
/* 켜기 칩 — 알약 · 켜면 강조 테두리 · 바탕 */
|
||||
.ui-toggle--chip {
|
||||
padding-inline: var(--ui-size-px);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-pills);
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.ui-toggle--chip:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background: var(--color-hover);
|
||||
}
|
||||
.ui-toggle--chip[aria-pressed="true"] {
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-accent-soft);
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
.ui-toggle__dot {
|
||||
flex: none;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.ui-toggle--chip[aria-pressed="false"] .ui-toggle__dot {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.ui-toggle:is(:active, [data-state="active"]):not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
.ui-toggle:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/* =============================================================================
|
||||
* ui_template_toggle.ts
|
||||
* 부품 「켜기」(552-2 · 부품 모음 `#/b01-account?view=ui-kit`)
|
||||
*
|
||||
* 꼴 = 스위치(switch · [글 ○━] 한 줄) · 켜기 칩(chip · 색 점 붙음 · 지도 층 · 보기 켜고 끄기)
|
||||
* 크기 = data-size 20 · 24 · 28 · 32(기본 24)
|
||||
* 상태 = 마우스 `:hover` · 눌림 `:active` · 막힘 `disabled` · 켬 = aria-checked / aria-pressed — CSS 는 `[data-state]` 로도 받음
|
||||
* 쓰기 = `createToggle({ label: "등고선", checked: true, onChange })`
|
||||
* `createToggle({ kind: "chip", label: "유역", dot: "var(--map-basin-1)" })`
|
||||
* 대신 = B03 `b03-file__lasfree` · B04 `b04-map__layer-button` · `.toggle-button` · B05 `b05-drainage__layer-button` ·
|
||||
* `toggleButton()` · B06 `b06-structure-panel__toggle` · `.ui-prov-toggle` · 시트 `.is-on` 단추 · CAD 상태막대 켜기
|
||||
* ========================================================================== */
|
||||
|
||||
import { setUiSize, type UiSize } from "./ui_template_button";
|
||||
import "./ui_template_toggle.css";
|
||||
|
||||
export interface ToggleOptions {
|
||||
label: string;
|
||||
/** switch(기본) · chip */
|
||||
kind?: "switch" | "chip";
|
||||
checked?: boolean;
|
||||
/** 칩 앞 색 점 — CSS 색 값(그림 색은 `var(--map-*)` · `var(--draw-*)`) */
|
||||
dot?: string;
|
||||
/** 20 · 24 · 28 · 32 권함 */
|
||||
size?: UiSize;
|
||||
disabled?: boolean;
|
||||
onChange?: (on: boolean) => void;
|
||||
}
|
||||
|
||||
export interface ToggleHandle {
|
||||
root: HTMLButtonElement;
|
||||
/** 지금 값 */
|
||||
get: () => boolean;
|
||||
/** 값을 바꿈(onChange 는 안 부름) */
|
||||
set: (on: boolean) => void;
|
||||
}
|
||||
|
||||
export function createToggle(opts: ToggleOptions): ToggleHandle {
|
||||
const chip = opts.kind === "chip";
|
||||
const root = document.createElement("button");
|
||||
root.type = "button";
|
||||
root.className = chip ? "ui-toggle ui-toggle--chip" : "ui-toggle";
|
||||
setUiSize(root, opts.size ?? 24);
|
||||
if (!chip) root.setAttribute("role", "switch");
|
||||
const attr = chip ? "aria-pressed" : "aria-checked";
|
||||
if (chip && opts.dot) {
|
||||
const dot = document.createElement("span");
|
||||
dot.className = "ui-toggle__dot";
|
||||
dot.style.background = opts.dot;
|
||||
root.append(dot);
|
||||
}
|
||||
const text = document.createElement("span");
|
||||
text.className = "ui-toggle__text";
|
||||
text.textContent = opts.label;
|
||||
root.append(text);
|
||||
if (!chip) {
|
||||
const track = document.createElement("span");
|
||||
track.className = "ui-toggle__track";
|
||||
root.append(track);
|
||||
}
|
||||
const set = (on: boolean): void => root.setAttribute(attr, String(on));
|
||||
const get = (): boolean => root.getAttribute(attr) === "true";
|
||||
set(opts.checked ?? false);
|
||||
root.disabled = opts.disabled ?? false;
|
||||
root.addEventListener("click", () => {
|
||||
set(!get());
|
||||
opts.onChange?.(get());
|
||||
});
|
||||
return { root, get, set };
|
||||
}
|
||||
@@ -155,3 +155,10 @@
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--color-border);
|
||||
}
|
||||
|
||||
/* 견본 세로 묶음(탭 두 꼴) */
|
||||
.ui-kit__demo-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -18,16 +18,22 @@
|
||||
* 몫 = 552-2 기본 · 552-3 묶음 · 552-4 틀 — 세 묶음이 떨어져 있어 서로 줄을 안 건드림.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "./ui_template_badge";
|
||||
import { createButton, createIconButton, type UiSize } from "./ui_template_button";
|
||||
import { createInputField, createSelectField } from "./ui_template_elements";
|
||||
import { createCheckbox } from "./ui_template_checkbox";
|
||||
import { tooltipBox } from "./ui_template_hover_info";
|
||||
import { ICON_NAMES, icon } from "./ui_template_icons";
|
||||
import { createInputField, createSelectField, createSlider } from "./ui_template_input";
|
||||
import { createSegment } from "./ui_template_segment";
|
||||
import { createTabs } from "./ui_template_tabs";
|
||||
import { createText } from "./ui_template_text";
|
||||
import { createToggle } from "./ui_template_toggle";
|
||||
import {
|
||||
badge,
|
||||
dataTable,
|
||||
rowMenu,
|
||||
settingCard,
|
||||
settingRow,
|
||||
tabBar,
|
||||
valueText,
|
||||
} from "../B01_Dashboard/B01_Dashboard_UI_PageKit";
|
||||
// 552-3 묶음 부품
|
||||
@@ -111,6 +117,79 @@ const col = (...children: HTMLElement[]): HTMLElement => {
|
||||
box.style.alignItems = "stretch";
|
||||
return box;
|
||||
};
|
||||
/** 552-2 견본 조각 — 상태를 안쪽 요소에 굳힘 */
|
||||
const OPTS = [
|
||||
{ value: "a", text: "임도" },
|
||||
{ value: "b", text: "예정노선" },
|
||||
];
|
||||
interface FieldDemo {
|
||||
root: HTMLElement;
|
||||
node: HTMLElement;
|
||||
setError?: (m?: string) => void;
|
||||
}
|
||||
function fieldDemo(s: KitState, make: () => FieldDemo): HTMLElement | null {
|
||||
if (s === "active") return null;
|
||||
const f = make();
|
||||
markState(f.node, s);
|
||||
if (s === "error") f.setError?.("값을 넣어야 함");
|
||||
return f.root;
|
||||
}
|
||||
function checkDemo(s: KitState): HTMLElement | null {
|
||||
if (s === "active" || s === "error") return null;
|
||||
const boxes = [
|
||||
createCheckbox("등고선", { checked: true }),
|
||||
createCheckbox("밝음", { radio: `kit-${s}`, checked: true }),
|
||||
createCheckbox("어두움", { radio: `kit-${s}` }),
|
||||
createCheckbox("지적", { chip: true, checked: true }),
|
||||
createCheckbox("유역", { chip: true }),
|
||||
];
|
||||
for (const b of boxes) {
|
||||
if (s === "disabled") b.input.disabled = true;
|
||||
else markState(b.root, s);
|
||||
}
|
||||
return row(...boxes.map((b) => b.root));
|
||||
}
|
||||
function segmentDemo(s: KitState): HTMLElement | null {
|
||||
if (s === "error") return null;
|
||||
const seg = createSegment({
|
||||
options: [
|
||||
{ value: "list", label: "목록" },
|
||||
{ value: "card", label: "카드" },
|
||||
{ value: "map", icon: "map", title: "지도" },
|
||||
],
|
||||
value: "list",
|
||||
});
|
||||
const items = [...seg.root.children] as HTMLButtonElement[];
|
||||
if (s === "disabled") items.forEach((b) => markState(b, s));
|
||||
else markState(items[1], s);
|
||||
return seg.root;
|
||||
}
|
||||
function tabsDemo(s: KitState): HTMLElement | null {
|
||||
if (s === "active" || s === "error") return null;
|
||||
const line = createTabs({
|
||||
tabs: [
|
||||
{ key: "a", label: "개요" },
|
||||
{ key: "b", label: "구성원" },
|
||||
{ key: "c", label: "기록" },
|
||||
],
|
||||
value: "a",
|
||||
});
|
||||
const pill = createTabs({
|
||||
kind: "pill",
|
||||
tabs: [
|
||||
{ key: "a", label: "토공" },
|
||||
{ key: "b", label: "배수" },
|
||||
{ key: "c", label: "구조물" },
|
||||
],
|
||||
value: "a",
|
||||
});
|
||||
line.setBadge("b", 3);
|
||||
for (const t of [line, pill]) markState(t.root.children[1] as HTMLButtonElement, s);
|
||||
const box = document.createElement("div");
|
||||
box.className = "ui-kit__demo-col";
|
||||
box.append(line.root, pill.root);
|
||||
return box;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
export const KIT_PARTS: KitPart[] = [
|
||||
@@ -121,22 +200,29 @@ export const KIT_PARTS: KitPart[] = [
|
||||
{ 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,
|
||||
demo: (s) => { const f = createSelectField({ options: [{ value: "a", text: "임도" }, { value: "b", text: "예정노선" }] }); markState(f.select, s); return f.root; } },
|
||||
{ key: "check", name: "체크 · 라디오", group: "basic", kinds: "체크 · 라디오 · 글 붙음", file: "ui_template_checkbox.ts" },
|
||||
{ key: "toggle", name: "켜기", group: "basic", kinds: "켬 · 끔 · 글 붙음", file: "ui_template_toggle.ts" },
|
||||
{ key: "segment", name: "분할 고르기", group: "basic", kinds: "글 · 아이콘 · 여덟 단", file: "ui_template_segment.ts" },
|
||||
{ key: "tabs", name: "탭", group: "basic", kinds: "밑줄 탭(한 꼴) · 수 배지 · 문서 탭은 꼴 그대로", file: "ui_template_tabs.ts", legacy: true,
|
||||
demo: (s) => (s === "default" ? tabBar([{ key: "a", label: "개요" }, { key: "b", label: "구성원" }, { key: "c", label: "기록" }], "a", noop).root : null) },
|
||||
{ key: "badge", name: "배지", group: "basic", kinds: "상태 · 역할 · 종류 · 수 · 램프(✓ 같이)", file: "ui_template_badge.ts", legacy: true,
|
||||
demo: (s) => (s === "default" ? row(badge("진행 중", "accent"), badge("완료", "success"), badge("멈춤", "warning"), badge("오류", "danger"), badge("임시", "neutral")) : null) },
|
||||
{ key: "tooltip", name: "툴팁", group: "basic", kinds: "마우스 글 · 값 풍선", file: "ui_template_hover_info.ts" },
|
||||
{ key: "input", name: "입력", group: "basic", kinds: "글 · 수 · 단위 붙음 · 찾기 · 날짜 · 여러 줄 · 여덟 단", file: "ui_template_input.ts",
|
||||
demo: (s) => fieldDemo(s, () => { const f = createInputField({ placeholder: "폭", type: "number", unit: "m", size: 32 }); return { root: f.root, node: f.input, setError: f.setError }; }),
|
||||
sizes: (z) => createInputField({ placeholder: String(z), size: z }).input },
|
||||
{ key: "select", name: "고르기", group: "basic", kinds: "한 개 · 화살표 = 셰브런 · 여덟 단", file: "ui_template_input.ts",
|
||||
demo: (s) => fieldDemo(s, () => { const f = createSelectField({ options: OPTS, size: 32 }); return { root: f.root, node: f.select }; }),
|
||||
sizes: (z) => createSelectField({ options: OPTS, size: z }).select },
|
||||
{ key: "check", name: "체크 · 라디오", group: "basic", kinds: "체크 · 라디오 · 글 붙음 · 칩(켜면 채움)", file: "ui_template_checkbox.ts", demo: checkDemo },
|
||||
{ key: "toggle", name: "켜기", group: "basic", kinds: "스위치 · 켜기 칩(색 점) · 20 · 24 · 28 · 32", file: "ui_template_toggle.ts",
|
||||
demo: (s) => (s === "error" ? null : row(...[createToggle({ label: "등고선", checked: true }), createToggle({ label: "지적" }), createToggle({ kind: "chip", label: "유역", checked: true, dot: "var(--color-info)" })].map((t) => markState(t.root, s)))),
|
||||
sizes: (z) => createToggle({ label: String(z), size: z, checked: true }).root },
|
||||
{ key: "segment", name: "분할 고르기", group: "basic", kinds: "글 · 아이콘 · 둘 ~ 다섯 칸 · 20 · 24 · 28 · 32", file: "ui_template_segment.ts", demo: segmentDemo,
|
||||
sizes: (z) => createSegment({ options: [{ value: "a", label: String(z) }, { value: "b", icon: "map", title: "지도" }], value: "a", size: z }).root },
|
||||
{ key: "tabs", name: "탭", group: "basic", kinds: "밑줄 탭(한 꼴) · 수 배지 · 보조 탭(작은 알약) · 문서 탭은 꼴 그대로", file: "ui_template_tabs.ts", demo: tabsDemo },
|
||||
{ key: "badge", name: "배지", group: "basic", kinds: "상태 · 역할 · 종류 · 수 · 램프(✓ ✗ 같이) · 점", file: "ui_template_badge.ts",
|
||||
demo: (s) => (s === "default" ? row(createBadge("진행 중", "accent"), createBadge("완료", "success"), createBadge("멈춤", "warning"), createBadge("오류", "danger"), createBadge("안정성", "info"), createBadge("임시", "neutral"), createBadge("3", "danger", { kind: "count" }), createBadge("통과", "success", { kind: "lamp" }), createBadge("미달", "danger", { kind: "lamp" }), createBadge("확인 필요", "warning", { kind: "dot" }), createBadge("작게", "neutral", { size: 18 })) : null) },
|
||||
{ key: "tooltip", name: "툴팁", group: "basic", kinds: "마우스 글 · 값 풍선 · 그림 위 설명", file: "ui_template_hover_info.ts",
|
||||
demo: (s) => (s === "default" ? row(tooltipBox("누르면 값을 고침"), tooltipBox("구간 1 · 높이 2.4 m\n기울기 1 : 1.5")) : null) },
|
||||
{ key: "icon", name: "아이콘", group: "basic", kinds: "Tabler 선 · 16 · 18 · 24 · 32 · 이모지 대신", file: "ui_template_icons.ts",
|
||||
demo: (s) => (s === "default" ? row(...ICON_NAMES.map((n) => { const i = icon(n); i.title = n; i.dataset.icon = n; return i; })) : null) },
|
||||
{ key: "slider", name: "슬라이더", group: "basic", kinds: "값 하나 · 이름 · 값 글", file: "ui_template_input.ts" },
|
||||
{ key: "text", name: "보조 글 · 안내 줄", group: "basic", kinds: "흐린 글 · 안내 · 상태(오류 · 경고) · 칸 오류", file: "ui_template_text.ts" },
|
||||
{ key: "slider", name: "슬라이더", group: "basic", kinds: "값 하나 · 이름 · 값 글 · 3열 줄(24)", file: "ui_template_input.ts",
|
||||
demo: (s) => { if (s === "active" || s === "error") return null; const sl = createSlider({ label: "투명도", min: 0, max: 100, value: 60, format: (v) => `${v}%` }); markState(sl.input, s); return sl.root; } },
|
||||
{ key: "text", name: "보조 글 · 안내 줄", group: "basic", kinds: "흐린 글 · 안내 · 상태(오류 · 경고 · 성공 · 기호 같이) · 12 · 14", file: "ui_template_text.ts",
|
||||
demo: (s) => (s === "default" ? row(createText("흐린 글 — 마지막 저장 3 분 전"), createText("안내 — 노선을 먼저 고름", "hint"), createText("저장 안 한 값이 있음", "warning"), createText("저장됨", "success"), createText("본문 곁 14", "muted", 14)) : s === "error" ? createText("값이 범위를 벗어남", "error") : null) },
|
||||
// ── 552-3 묶음 ─────────────────────────────────────────────
|
||||
{ key: "card", name: "카드", group: "compound", kinds: "선 카드 · 머리 카드 · 고르는 카드 · 여백 12 · 16", file: "ui_template_card.ts",
|
||||
demo: (s) => { const pick = (): HTMLElement => markState(createCard({ title: "새 노선", body: ["노선을 그려 시작"], pad: 12, onSelect: noop }), s);
|
||||
|
||||
Reference in New Issue
Block a user