This commit is contained in:
2026-07-10 16:41:22 +09:00
parent bbda38a013
commit 50d75fcfcd
47 changed files with 6309 additions and 350 deletions
+106 -24
View File
@@ -141,6 +141,59 @@ export function createInputField(opts: InputFieldOptions): InputFieldHandle {
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;
}
export interface SelectFieldHandle {
root: HTMLDivElement;
select: HTMLSelectElement;
}
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: "ui-select",
});
for (const opt of opts.options) {
const optEl = el("option", {
attrs: { value: opt.value },
text: opt.text,
});
if (opts.value !== undefined && opt.value === opts.value) {
optEl.selected = true;
}
select.append(optEl);
}
if (opts.disabled) select.disabled = true;
if (opts.onChange) {
select.addEventListener("change", () => opts.onChange!(select.value));
}
root.append(select);
return { root, select };
}
/* -----------------------------------------------------------------------------
* 3. 카드 / 패널 (Card) — design.md: white surface, 8px radius, shadow
* -------------------------------------------------------------------------- */
@@ -395,20 +448,31 @@ export function createLineChart(opts: LineChartOptions): HTMLDivElement {
svg.append(tick);
}
// x축 라벨 (데이터 포인트 개수만큼, X_TICK_STEP 간격으로 표기)
// x축 라벨 (데이터 포인트 개수만큼, X_TICK_STEP 간격으로 표기) + 수직 점선 그리드
if (opts.xLabels && opts.xLabels.length > 0) {
const labels = opts.xLabels;
const baseY = CHART_PAD.top + plotH;
for (let idx = 0; idx < pointCount; idx += X_TICK_STEP) {
const label = labels[idx];
if (label === undefined || label === "") continue;
const tick = document.createElementNS(svgNs, "text");
tick.setAttribute("class", "ui-chart__tick ui-chart__tick--x");
tick.setAttribute("x", String(xAt(idx)));
tick.setAttribute("y", String(baseY + 16));
tick.setAttribute("text-anchor", "middle");
tick.textContent = label;
svg.append(tick);
const x = xAt(idx);
if (label !== undefined && label !== "") {
// 수직 점선 그리드
const vline = document.createElementNS(svgNs, "line");
vline.setAttribute("class", "ui-chart__grid ui-chart__grid--vertical");
vline.setAttribute("x1", String(x));
vline.setAttribute("x2", String(x));
vline.setAttribute("y1", String(CHART_PAD.top));
vline.setAttribute("y2", String(baseY));
svg.append(vline);
// x축 라벨
const tick = document.createElementNS(svgNs, "text");
tick.setAttribute("class", "ui-chart__tick ui-chart__tick--x");
tick.setAttribute("x", String(x));
tick.setAttribute("y", String(baseY + 16));
tick.setAttribute("text-anchor", "middle");
tick.textContent = label;
svg.append(tick);
}
}
}
@@ -432,17 +496,6 @@ export function createLineChart(opts: LineChartOptions): HTMLDivElement {
path.setAttribute("class", `ui-chart__line ui-chart__line--c${s.colorIndex ?? 0}`);
path.setAttribute("d", d.trim());
svg.append(path);
// 각 데이터 포인트에 점(circle) 그리기
s.values.forEach((v, i) => {
if (v === null || v === undefined) return;
const circle = document.createElementNS(svgNs, "circle");
circle.setAttribute("class", `ui-chart__point ui-chart__point--c${s.colorIndex ?? 0}`);
circle.setAttribute("cx", String(xAt(i)));
circle.setAttribute("cy", String(yAt(v)));
circle.setAttribute("r", "3");
svg.append(circle);
});
}
// 범례 (그래프 영역 우상단 오버레이)
@@ -477,12 +530,12 @@ const BASE_CSS = `
justify-content: center;
gap: var(--spacing-8);
font-family: var(--font-body);
font-size: var(--text-body);
font-size: var(--text-body-sm);
font-weight: var(--font-weight-medium);
line-height: 1;
border: 1px solid transparent;
border-radius: var(--radius-buttons);
padding: var(--spacing-16) var(--spacing-24);
padding: var(--spacing-8) var(--spacing-16);
cursor: pointer;
transition: background-color var(--transition-fast),
border-color var(--transition-fast), color var(--transition-fast);
@@ -508,8 +561,6 @@ const BASE_CSS = `
background-color: transparent;
color: var(--color-plum-velvet);
border-radius: var(--radius-pills);
padding: var(--spacing-8) var(--spacing-16);
font-size: var(--text-body-sm);
}
.ui-btn--pill:hover:not(:disabled),
.ui-btn--pill.is-active { background-color: var(--color-mist-violet); }
@@ -551,6 +602,34 @@ const BASE_CSS = `
}
.ui-field__error.is-visible { visibility: visible; }
.ui-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
font-family: var(--font-body);
font-size: var(--text-body-sm);
color: var(--color-text-body);
background-color: var(--color-canvas);
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-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
cursor: pointer;
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.ui-select:focus {
outline: none;
border-color: var(--color-focus-ring);
box-shadow: 0 0 0 1px var(--color-focus-ring);
}
.ui-select:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* --- Card --- */
.ui-card {
background-color: var(--color-surface-raised);
@@ -697,6 +776,9 @@ const BASE_CSS = `
stroke-width: 1;
opacity: 0.5;
}
.ui-chart__grid--vertical {
stroke-dasharray: 4, 4;
}
.ui-chart__tick {
fill: var(--color-text-muted);
font-size: 12px;