260710_0
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user