web (Vite+React19+TS, Cloudflare Pages 배포): - 디자인토큰(세이지틸/테라코타 SSOT), 앱셸, 공통 UI 프리미티브 - 7화면: 로그인/학습자홈/상담세션/회기리뷰/교수자/관리자/설정 - ClientAvatar: SVG 반구상 흉상 4상태 + RMS 립싱크 + 6파라미터 정서 - 회기리뷰는 외부 레퍼런스 디자인을 Vignette 토큰으로 리스킨 api (FastAPI): - 게이트웨이 /v1/generate·/v1/stream 어댑터(상주풀/EngineSession 보존) - services: 페르소나 L0~L6 빌더 / 결정론 상태머신 / 가드레일 / 턴 오케스트레이터 / 회기간 메모리 / 평가AI / 음성 / RAG - store: DB off 폴백(in-memory), sessions 실구현 검증: - web: node22 tsc+vite build 통과(node23 segfault 회피), Pages 배포 200 - api: app.main import 통과 - 핫픽스: Topbar initials undefined-safe (undefined.trim 크래시) - E2E: 서연(P1) 상담 1턴 — 좋은/나쁜 상담에 차등 반응 실증
57 lines
1.6 KiB
TypeScript
57 lines
1.6 KiB
TypeScript
import { useId } from "react";
|
|
import type { InputHTMLAttributes, ReactNode } from "react";
|
|
|
|
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
invalid?: boolean;
|
|
}
|
|
|
|
/** Input — radius 6px, 포커스 보더색 + ring. 좌측바 금지. §7.2 */
|
|
export function Input({ invalid, className, ...rest }: InputProps) {
|
|
const cls = ["vg-input", className ?? ""].filter(Boolean).join(" ");
|
|
return (
|
|
<input
|
|
className={cls}
|
|
aria-invalid={invalid ? "true" : undefined}
|
|
{...rest}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export interface FieldProps {
|
|
/** 라벨 텍스트 */
|
|
label?: ReactNode;
|
|
/** 보조 안내 */
|
|
hint?: ReactNode;
|
|
/** 에러 메시지 (있으면 input invalid) */
|
|
error?: ReactNode;
|
|
/** input 에 연결할 자식. id 는 자동 연결되지 않으므로 필요 시 htmlFor 직접 사용 */
|
|
children: ReactNode;
|
|
/** label htmlFor 연결용 (없으면 useId) */
|
|
htmlFor?: string;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Field — 라벨 + 입력 + 힌트/에러 묶음.
|
|
* children 으로 <Input/> 등을 받는다(제어는 호출부).
|
|
*/
|
|
export function Field({ label, hint, error, children, htmlFor, className }: FieldProps) {
|
|
const autoId = useId();
|
|
const fieldId = htmlFor ?? autoId;
|
|
const cls = ["vg-field", className ?? ""].filter(Boolean).join(" ");
|
|
return (
|
|
<div className={cls}>
|
|
{label ? (
|
|
<label className="vg-field__label" htmlFor={fieldId}>
|
|
{label}
|
|
</label>
|
|
) : null}
|
|
{children}
|
|
{error ? (
|
|
<span className="vg-field__error">{error}</span>
|
|
) : hint ? (
|
|
<span className="vg-field__hint">{hint}</span>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|