Files
ai-code-app/src/views/play/LayoutPreviewWidgets.tsx

357 lines
12 KiB
TypeScript

import {
CheckOutlined,
DeleteOutlined,
LeftOutlined,
PlayCircleOutlined,
PlusOutlined,
RightOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import { Button, Empty, Input, Tag, Typography } from 'antd';
import type { SyntheticEvent } from 'react';
import { InputUI } from '../../components/inputs/primitives/input';
import { SelectUI, type SelectOptionItem } from '../../components/inputs/select';
import type {
LayoutPreviewBaseInputState,
LayoutPreviewEmptyPaneState,
LayoutPreviewMemoState,
LayoutPreviewSelectState,
} from './layoutPreviewRuntime';
const { Text } = Typography;
function stopPreviewEvent(event: SyntheticEvent) {
event.stopPropagation();
}
function formatMemoTimestamp(value: string) {
return new Intl.DateTimeFormat('ko-KR', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
}).format(new Date(value));
}
function getMemoPreview(body: string) {
return body.replace(/\s+/g, ' ').trim() || '새 메모';
}
function formatEmptyPaneTimestamp(value: string | null) {
if (!value) {
return '아직 메모가 없습니다.';
}
return new Intl.DateTimeFormat('ko-KR', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
}).format(new Date(value));
}
const EMPTY_PANE_READINESS_META: Record<
LayoutPreviewEmptyPaneState['readiness'],
{ label: string; tone: 'default' | 'processing' | 'success' }
> = {
unassigned: { label: '컴포넌트 대기', tone: 'default' },
drafting: { label: '요구 정리 중', tone: 'processing' },
ready: { label: '준비 완료', tone: 'success' },
};
export function LayoutPreviewTextMemoPane({
state,
skin = 'note',
title = '메모 본문',
onStartDraft,
onToggleList,
onDeleteSelection,
onSaveDraft,
onSelectNote,
onMoveSelection,
onDraftChange,
}: {
state: LayoutPreviewMemoState;
skin?: 'flat' | 'note';
title?: string;
onStartDraft: () => void;
onToggleList: () => void;
onDeleteSelection: () => void;
onSaveDraft: () => void;
onSelectNote: (noteId: string) => void;
onMoveSelection: (direction: -1 | 1) => void;
onDraftChange: (nextValue: string) => void;
}) {
const selectedIndex = state.selectedId ? state.notes.findIndex((note) => note.id === state.selectedId) : -1;
const selectedNote = selectedIndex >= 0 ? state.notes[selectedIndex] : null;
const hasDraft = state.draftBody.trim().length > 0;
const isFlat = skin === 'flat';
return (
<div
className={`layout-playground__memo-widget-preview${
isFlat ? ' layout-playground__memo-widget-preview--flat' : ''
}`}
onClick={stopPreviewEvent}
>
{isFlat ? (
<div className="layout-playground__memo-widget-preview-head">
<Text strong>{title}</Text>
<Text type="secondary">{selectedNote ? '선택 항목 편집 중' : '새 항목 작성 가능'}</Text>
</div>
) : null}
<div className="layout-playground__memo-widget-preview-toolbar" role="toolbar" aria-label="메모 도구">
<div className="layout-playground__memo-widget-preview-toolbar-group">
<Button type="text" shape="circle" htmlType="button" icon={<PlusOutlined />} aria-label="새 메모" onClick={onStartDraft} />
<Button
type="text"
shape="circle"
htmlType="button"
icon={<DeleteOutlined />}
aria-label="메모 삭제"
disabled={!selectedNote && !hasDraft}
onClick={onDeleteSelection}
/>
<Button
type="text"
shape="circle"
htmlType="button"
icon={<UnorderedListOutlined />}
aria-label="메모 목록"
onClick={onToggleList}
/>
</div>
<div className="layout-playground__memo-widget-preview-toolbar-group">
<Button
type="text"
shape="circle"
htmlType="button"
icon={<LeftOutlined />}
disabled={selectedIndex <= 0}
onClick={() => {
onMoveSelection(-1);
}}
/>
<Button
type="text"
shape="circle"
htmlType="button"
icon={<RightOutlined />}
disabled={selectedIndex < 0 || selectedIndex >= state.notes.length - 1}
onClick={() => {
onMoveSelection(1);
}}
/>
<Button
type="text"
shape="circle"
htmlType="button"
icon={<CheckOutlined />}
aria-label="저장"
disabled={!hasDraft && !selectedNote}
onClick={onSaveDraft}
/>
</div>
</div>
<div className={`layout-playground__memo-widget-preview-body${state.isListOpen ? ' layout-playground__memo-widget-preview-body--list' : ''}`}>
{state.isLoading ? (
<div className="layout-playground__memo-widget-preview-empty">
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="메모를 준비하는 중입니다." />
</div>
) : state.isListOpen ? (
<div className="layout-playground__memo-widget-preview-sheet">
{state.notes.length ? (
<div className="layout-playground__memo-widget-preview-list">
{state.notes.map((note) => (
<button
key={note.id}
type="button"
className={`layout-playground__memo-widget-preview-item${note.id === state.selectedId ? ' layout-playground__memo-widget-preview-item--active' : ''}`}
onClick={() => {
onSelectNote(note.id);
}}
>
<span className="layout-playground__memo-widget-preview-item-time">
{formatMemoTimestamp(note.updatedAt)}
</span>
<span className="layout-playground__memo-widget-preview-item-copy">{getMemoPreview(note.body)}</span>
</button>
))}
</div>
) : (
<div className="layout-playground__memo-widget-preview-empty">
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="저장된 메모가 없습니다." />
</div>
)}
</div>
) : (
<div className="layout-playground__memo-widget-preview-editor">
<div className="layout-playground__memo-widget-preview-meta">
<span>{selectedNote ? formatMemoTimestamp(selectedNote.updatedAt) : '새 메모'}</span>
<span>{state.draftBody.length}/1200</span>
</div>
<Input.TextArea
className="layout-playground__memo-widget-preview-input"
value={state.draftBody}
placeholder="본문 메모를 입력하세요."
variant="borderless"
maxLength={1200}
autoSize={false}
onChange={(event) => {
onDraftChange(event.target.value);
}}
/>
</div>
)}
</div>
</div>
);
}
export function LayoutPreviewBaseInputPane({
state,
fillPane = false,
placeholder = '입력 후 Enter 또는 blur',
}: {
state: LayoutPreviewBaseInputState;
fillPane?: boolean;
placeholder?: string;
}) {
return (
<div
className={`layout-playground__base-input-preview${fillPane ? ' layout-playground__base-input-preview--fill' : ''}`}
onClick={stopPreviewEvent}
>
<InputUI
className="layout-playground__base-input-preview-field"
value={state.value}
placeholder={placeholder}
readOnly
/>
</div>
);
}
export function LayoutPreviewActionPane({
label = 'Codex 실행',
description = '선택한 레이아웃과 기능설명 조합 기준으로 실행합니다.',
onClick,
}: {
label?: string;
description?: string;
onClick?: () => void;
}) {
return (
<div className="layout-playground__action-preview" onClick={stopPreviewEvent}>
<div className="layout-playground__action-preview-copy">
<span className="layout-playground__action-preview-kicker"> </span>
<Text strong>{label}</Text>
<Text type="secondary">{description}</Text>
</div>
<Button
type="primary"
size="large"
icon={<PlayCircleOutlined />}
className="layout-playground__action-preview-button"
onClick={onClick}
>
{label}
</Button>
</div>
);
}
export function LayoutPreviewSelectPane({
state,
data,
onChange,
}: {
state: LayoutPreviewSelectState;
data: SelectOptionItem[];
onChange: (nextCode?: string, item?: SelectOptionItem) => void;
}) {
const resolvedSelectedCode =
state.selectedCode && data.some((item) => item.code === state.selectedCode)
? state.selectedCode
: data[0]?.code;
const formatComboLabel = (item: SelectOptionItem) => item.value;
return (
<div className="layout-playground__select-preview" onClick={stopPreviewEvent}>
<SelectUI
data={data}
value={resolvedSelectedCode}
allowClear
placeholder="콤보 값을 선택하세요"
formatLabel={formatComboLabel}
onChange={onChange}
/>
</div>
);
}
export function LayoutPreviewEmptyPane({
paneLabel,
paneDescription,
sizeSummary,
state,
onReadinessChange,
onNoteChange,
}: {
paneLabel: string;
paneDescription: string;
sizeSummary: string;
state: LayoutPreviewEmptyPaneState;
onReadinessChange: (nextValue: LayoutPreviewEmptyPaneState['readiness']) => void;
onNoteChange: (nextValue: string) => void;
}) {
const readinessMeta = EMPTY_PANE_READINESS_META[state.readiness];
return (
<div className="layout-playground__empty-pane-preview" onClick={stopPreviewEvent}>
<div className="layout-playground__empty-pane-preview-head">
<div className="layout-playground__empty-pane-preview-copy">
<Text strong>{paneLabel}</Text>
<Text type="secondary">{paneDescription}</Text>
</div>
<Tag color={readinessMeta.tone}>{readinessMeta.label}</Tag>
</div>
<div className="layout-playground__empty-pane-preview-meta">
<div className="layout-playground__empty-pane-preview-meta-item">
<span> </span>
<strong>{sizeSummary}</strong>
</div>
<div className="layout-playground__empty-pane-preview-meta-item">
<span> </span>
<strong>{formatEmptyPaneTimestamp(state.updatedAt)}</strong>
</div>
</div>
<div className="layout-playground__empty-pane-preview-actions">
<Button type={state.readiness === 'unassigned' ? 'primary' : 'default'} size="small" onClick={() => onReadinessChange('unassigned')}>
</Button>
<Button type={state.readiness === 'drafting' ? 'primary' : 'default'} size="small" onClick={() => onReadinessChange('drafting')}>
</Button>
<Button type={state.readiness === 'ready' ? 'primary' : 'default'} size="small" onClick={() => onReadinessChange('ready')}>
</Button>
</div>
<Input.TextArea
className="layout-playground__empty-pane-preview-note"
value={state.note}
placeholder="이 section에 들어갈 역할이나 메모를 간단히 정리하세요."
autoSize={{ minRows: 4, maxRows: 8 }}
maxLength={400}
onChange={(event) => {
onNoteChange(event.target.value);
}}
/>
</div>
);
}