feat: update codex live runtime and restart flow
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user