- ui_template_input.ts · .css 새로 — createInputField · createSelectField 를 옮김(ui_template_elements 가 다시 내보냄 · 부르는 길 그대로) · size(data-size 단 높이) · unit(칸 안 단위) · multiline · createSlider(이름 | 막대 | 값 글 3열) - 옛 compact 는 단을 안 붙임 — 옛 작은 칸 30 그대로(32 로 맞추면 페이지 칸이 바뀜 · 553 에서 단으로 옮김) - 입력 · 고르기 상태 = 마우스 선 짙게 · 막힘 흐림 · 오류 빨강(aria-invalid 같이) · 고르기 화살표 = 보라 → 보조 글 색(밝음 · 어두움 따로) - 체크 = 라디오 · 칩 꼴 더함(createCheckbox 부름 그대로) · 켜기(스위치 · 색 점 칩) · 분할 고르기(대시보드 .b01-sys-seg 꼴) · 탭(밑줄 · 보조 알약 = 옛 .ui-subtabs · 수 배지) - 배지(알약 · 수 · 램프 ✓ ✗ · 점 · 옛 .ui-tag 같이) · 툴팁 제자리 상자(tooltipBox) · 보조 글(흐린 · 안내 · 오류 ✗ · 경고 ! · 성공 ✓) - 부품 모음 기본 묶음 13 줄 모두 새 부품 견본 · 상태 다섯 칸 · 여덟 단 한 줄 - 시험 test_552_2_parts 새로(머리 · 옛 이름 · 크기 단 · 상태 · 날 색 없음 · 부품 모음 13) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
214 lines
7.7 KiB
TypeScript
214 lines
7.7 KiB
TypeScript
/* =============================================================================
|
|
* 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 };
|
|
}
|