import { html, css, LitElement, nothing } from 'lit' import { customElement, property } from 'lit/decorators.js' import { resolveInputHint, InputHint, KpiMetricLike } from './metric-input-hint' import { MetricRendererRegistry } from './metric-renderer-registry' /** * metric 정의(meta.input)에 따라 입력 위젯을 **선언적으로 자동 렌더**하는 제네릭 컴포넌트. * 이름 추측(isRating 등) 없이 `resolveInputHint` 로 위젯을 결정한다. * * (v = e.detail.value)}> * * 예외 UI 는 MetricRendererRegistry 로 등록(제네릭 우선순위보다 먼저 적용). */ @customElement('kpi-metric-input') export class KpiMetricInput extends LitElement { static styles = css` :host { display: inline-flex; align-items: baseline; gap: 2px; font: 13px/18px var(--theme-font, sans-serif); } input, select { background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest)); color: var(--input-field-color, var(--md-sys-color-on-surface)); font: inherit; padding: 2px 4px; } input[type='number'] { width: 5em; text-align: right; } .stars { display: inline-flex; cursor: pointer; font-size: 18px; line-height: 1; color: var(--md-sys-color-primary, #2e79be); } .stars[readonly] { cursor: default; opacity: 0.85; } .star { width: 1em; } .unit { color: var(--md-sys-color-on-surface-variant, #888); font-size: 11px; } ` @property({ type: Object }) metric: KpiMetricLike = {} @property() value: any /** 상위에서 강제 비활성화(권한 등). readonly 힌트와 별개. */ @property({ type: Boolean }) disabled = false private _emit(value: any) { this.value = value this.dispatchEvent(new CustomEvent('metric-input-change', { detail: { value }, bubbles: true, composed: true })) } render() { const custom = MetricRendererRegistry.resolve(this.metric) if (custom) return custom(this.metric, this.value, v => this._emit(v)) const hint = resolveInputHint(this.metric) const ro = this.disabled || !!hint.readonly const unit = (this.metric as any)?.unit // 값(우측정렬) 바로 옆에 단위 — 값이 단위에 붙어 자연스럽게. (rating/toggle 등엔 unit 보통 없음) return html` ${this._renderWidget(hint, ro)}${unit ? html`${unit}` : nothing} ` } private _renderWidget(hint: InputHint, ro: boolean) { switch (hint.widget) { case 'rating': return this._renderRating(hint, ro) case 'select': return this._renderSelect(hint, ro) case 'date': return html` this._emit((e.target as HTMLInputElement).value)} />` case 'toggle': return html` this._emit((e.target as HTMLInputElement).checked)} />` case 'text': return html` this._emit((e.target as HTMLInputElement).value)} />` case 'number': default: return html` { const raw = (e.target as HTMLInputElement).value this._emit(raw === '' ? null : Number(raw)) }} />` } } private _renderRating(hint: InputHint, ro: boolean) { const max = hint.max ?? 5 const score = Number(this.value ?? 0) const stars = Array.from({ length: max }, (_, idx) => { const i = idx + 1 const filled = i <= Math.round(score) return html` (ro ? null : this._emit(i))}>${filled ? '★' : '☆'}` }) return html`${stars}` } private _renderSelect(hint: InputHint, ro: boolean) { const options = hint.options ?? [] return html`` } }