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``
}
}