feat: update codex live runtime and restart flow

This commit is contained in:
2026-04-23 18:10:43 +09:00
parent b0b9980a6c
commit 6e863feafd
36 changed files with 1636 additions and 358 deletions

View File

@@ -93,6 +93,25 @@
padding: 4px;
}
.previewer-ui__findbar {
position: sticky;
top: 0;
z-index: 3;
display: flex;
align-items: center;
gap: 8px;
margin: -4px -4px 12px;
padding: 8px;
background: rgba(255, 255, 255, 0.96);
border-bottom: 1px solid rgba(148, 163, 184, 0.2);
backdrop-filter: blur(10px);
}
.previewer-ui__findbar .ant-input-affix-wrapper {
flex: 1 1 auto;
min-width: 0;
}
.previewer-ui__action-button {
display: inline-flex;
align-items: center;
@@ -124,6 +143,12 @@
0 12px 28px rgba(15, 23, 42, 0.16);
}
.previewer-ui--expanded .previewer-ui__header {
position: sticky;
top: 0;
z-index: 4;
}
.previewer-ui--expanded .previewer-ui__body {
height: calc(100vh - 53px) !important;
}
@@ -305,3 +330,24 @@
.previewer-ui__token--option {
color: #c586c0;
}
@media (max-width: 720px) {
.previewer-ui__header {
align-items: flex-start;
flex-direction: column;
}
.previewer-ui__toolbar {
width: 100%;
justify-content: flex-end;
flex-wrap: wrap;
}
.previewer-ui__findbar {
flex-wrap: wrap;
}
.previewer-ui__findbar .ant-btn {
flex: 1 1 calc(50% - 4px);
}
}

View File

@@ -1,7 +1,13 @@
import { CopyOutlined, DownloadOutlined, FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
import { Button, Empty, Select, message } from 'antd';
import {
CopyOutlined,
DownloadOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
SearchOutlined,
} from '@ant-design/icons';
import { Button, Empty, Input, Select, message } from 'antd';
import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { InlineImage } from '../common/InlineImage';
import { CodexDiffBlock } from './CodexDiffBlock';
import type { PreviewerUIProps } from './types';
@@ -283,13 +289,17 @@ export function PreviewerUI({
}: PreviewerUIProps) {
const [messageApi, contextHolder] = message.useMessage();
const [isExpanded, setIsExpanded] = useState(false);
const [isFindOpen, setIsFindOpen] = useState(false);
const [findQuery, setFindQuery] = useState('');
const findInputRef = useRef<HTMLInputElement | null>(null);
const hasLanguageSelector = type === 'code' && languageOptions && languageOptions.length > 0;
const resolvedCopyValue = copyValue ?? resolveCopyValue({ type, value });
const resolvedDownloadValue = resolveDownloadValue({ type, value, downloadValue });
const resolvedDownloadFileName = resolveDownloadFileName({ type, language, downloadFileName });
const canCopy = copyable && resolvedCopyValue.trim().length > 0;
const canDownload = downloadable && (Boolean(downloadUrl) || resolvedDownloadValue.trim().length > 0);
const shouldShowActions = hasLanguageSelector || canCopy || canDownload || maximizable || Boolean(toolbar);
const canFind = type !== 'image' && type !== 'empty';
const shouldShowActions = hasLanguageSelector || canCopy || canDownload || maximizable || canFind || Boolean(toolbar);
useEffect(() => {
if (!isExpanded || typeof document === 'undefined') {
@@ -312,6 +322,18 @@ export function PreviewerUI({
};
}, [isExpanded]);
useEffect(() => {
if (!isExpanded) {
setIsFindOpen(false);
setFindQuery('');
return;
}
if (isFindOpen) {
window.setTimeout(() => findInputRef.current?.focus(), 0);
}
}, [isExpanded, isFindOpen]);
async function handleCopy() {
if (!canCopy) {
return;
@@ -333,6 +355,44 @@ export function PreviewerUI({
setIsExpanded((previous) => !previous);
}
function handleFind(direction: 'forward' | 'backward' = 'forward') {
const keyword = findQuery.trim();
if (!keyword) {
messageApi.info('찾을 단어를 입력해주세요.');
return;
}
const browserWindow = typeof window === 'undefined' ? null : (window as Window & {
find?: (
text: string,
caseSensitive?: boolean,
backwards?: boolean,
wrapAround?: boolean,
wholeWord?: boolean,
searchInFrames?: boolean,
showDialog?: boolean,
) => boolean;
});
if (!browserWindow || typeof browserWindow.find !== 'function') {
messageApi.error('이 브라우저에서는 단어 찾기를 지원하지 않습니다.');
return;
}
const matched = browserWindow.find(keyword, false, direction === 'backward', true, false, false, false);
if (!matched) {
messageApi.info('일치하는 단어를 찾지 못했습니다.');
}
}
function toggleFind() {
if (!canFind) {
return;
}
setIsFindOpen((previous) => !previous);
}
function handleDownload() {
if (!canDownload) {
return;
@@ -405,6 +465,16 @@ export function PreviewerUI({
onClick={() => void toggleFullscreen()}
/>
) : null}
{canFind ? (
<Button
size="small"
type="text"
className="previewer-ui__action-button"
aria-label="단어 찾기"
icon={<SearchOutlined />}
onClick={toggleFind}
/>
) : null}
{toolbar}
</>
);
@@ -433,6 +503,27 @@ export function PreviewerUI({
) : null}
<div className="previewer-ui__body previewer-ui__scroll" style={{ height }}>
{!showHeader && shouldShowActions ? <div className="previewer-ui__floating-toolbar">{actionContent}</div> : null}
{isExpanded && isFindOpen ? (
<div className="previewer-ui__findbar">
<Input
ref={(node) => {
findInputRef.current = node?.input ?? null;
}}
size="small"
value={findQuery}
placeholder="단어 찾기"
allowClear
onChange={(event) => setFindQuery(event.target.value)}
onPressEnter={(event) => handleFind(event.shiftKey ? 'backward' : 'forward')}
/>
<Button size="small" onClick={() => handleFind('backward')}>
</Button>
<Button size="small" type="primary" onClick={() => handleFind('forward')}>
</Button>
</div>
) : null}
{renderContent({
type,
value,

View File

@@ -30,7 +30,6 @@ export function SearchCommandModal({
submitHint,
}: SearchCommandModalProps) {
const inputRef = useRef<InputRef | null>(null);
const selectionLockRef = useRef(false);
const [query, setQuery] = useState('');
const [isMobileViewport, setIsMobileViewport] = useState(() => {
if (typeof window === 'undefined') {
@@ -47,11 +46,10 @@ export function SearchCommandModal({
}, [open]);
const submitOption = (option: SearchKeywordOption | undefined) => {
if (!option || selectionLockRef.current) {
if (!option) {
return;
}
selectionLockRef.current = true;
onSelectOption(option);
onClose();
};
@@ -127,7 +125,6 @@ export function SearchCommandModal({
return;
}
selectionLockRef.current = false;
window.requestAnimationFrame(() => {
inputRef.current?.focus({
cursor: 'all',