radix select
This commit is contained in:
@@ -52,7 +52,6 @@
|
||||
}
|
||||
|
||||
.editor-toolkit-panel {
|
||||
// width: 100%;
|
||||
position: fixed;
|
||||
bottom: 0.5rem;
|
||||
border-radius: 3rem;
|
||||
@@ -110,80 +109,3 @@
|
||||
border: 1px solid var(--yellow-accent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.editor-size-selector-options {
|
||||
position: fixed;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.editor-size-selector {
|
||||
grid-area: toolkit-size-selector;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-size-selector-main {
|
||||
@include accented-display(var(white));
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
gap: 8px;
|
||||
width: 128px;
|
||||
|
||||
border: 1px solid var(--editor-size-border-color);
|
||||
color: var(--options-text-color);
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-size-options {
|
||||
@include accented-display(var(--btn-primary-bg));
|
||||
width: 128px;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
justify-self: center;
|
||||
position: fixed;
|
||||
bottom: 4rem;
|
||||
cursor: pointer;
|
||||
|
||||
color: var(--options-text-color);
|
||||
background-color: var(--page-bg);
|
||||
border: 1px solid var(--editor-size-border-color);
|
||||
|
||||
border-radius: 0.6rem;
|
||||
|
||||
@include mobile {
|
||||
bottom: 11.5rem;
|
||||
}
|
||||
|
||||
.editor-size-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
user-select: none;
|
||||
padding: 0.2rem 0.8rem;
|
||||
|
||||
&:first-of-type {
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--yellow-accent);
|
||||
color: var(--btn-text-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +268,7 @@ export default function Editor(props: EditorProps) {
|
||||
isOriginalLoaded,
|
||||
windowSize,
|
||||
initialCentered,
|
||||
drawOnCurrentRender,
|
||||
])
|
||||
|
||||
// Zoom reset
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { useClickAway } from 'react-use'
|
||||
import { ChevronUpIcon } from '@heroicons/react/outline'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Selector from '../shared/Selector'
|
||||
|
||||
const sizes = ['720', '1080', '2000', 'Original']
|
||||
|
||||
@@ -12,24 +11,9 @@ type SizeSelectorProps = {
|
||||
|
||||
export default function SizeSelector(props: SizeSelectorProps) {
|
||||
const { originalHeight, originalWidth, onChange } = props
|
||||
const [showOptions, setShowOptions] = useState<boolean>(false)
|
||||
const sizeSelectorRef = useRef(null)
|
||||
const [activeSize, setActiveSize] = useState<string>('Original')
|
||||
const longSide: number = Math.max(originalWidth, originalHeight)
|
||||
|
||||
const getValidSizes = useCallback(() => {
|
||||
const validSizes: string[] = []
|
||||
for (let i = 0; i < sizes.length; i += 1) {
|
||||
if (sizes[i] === 'Original') {
|
||||
validSizes.push(sizes[i])
|
||||
}
|
||||
if (parseInt(sizes[i], 10) < longSide) {
|
||||
validSizes.push(sizes[i])
|
||||
}
|
||||
}
|
||||
return validSizes
|
||||
}, [longSide])
|
||||
|
||||
const getSizeShowName = useCallback(
|
||||
(size: string) => {
|
||||
if (size === 'Original') {
|
||||
@@ -46,57 +30,38 @@ export default function SizeSelector(props: SizeSelectorProps) {
|
||||
[originalWidth, originalHeight, longSide]
|
||||
)
|
||||
|
||||
const showOptionsHandler = () => {
|
||||
setShowOptions(currentShowOptionsState => !currentShowOptionsState)
|
||||
}
|
||||
const getValidSizes = useCallback(() => {
|
||||
const validSizes: string[] = []
|
||||
for (let i = 0; i < sizes.length; i += 1) {
|
||||
if (sizes[i] === 'Original') {
|
||||
validSizes.push(getSizeShowName(sizes[i]))
|
||||
}
|
||||
if (parseInt(sizes[i], 10) < longSide) {
|
||||
validSizes.push(getSizeShowName(sizes[i]))
|
||||
}
|
||||
}
|
||||
return validSizes
|
||||
}, [longSide, getSizeShowName])
|
||||
|
||||
useClickAway(sizeSelectorRef, () => {
|
||||
setShowOptions(false)
|
||||
})
|
||||
|
||||
const sizeChangeHandler = (e: any) => {
|
||||
const currentRes = e.target.textContent.split('x')
|
||||
const sizeChangeHandler = (value: string) => {
|
||||
const currentRes = value.split('x')
|
||||
if (originalWidth > originalHeight) {
|
||||
setActiveSize(currentRes[0])
|
||||
onChange(currentRes[0])
|
||||
onChange(parseInt(currentRes[0], 10))
|
||||
} else {
|
||||
setActiveSize(currentRes[1])
|
||||
onChange(currentRes[1])
|
||||
onChange(parseInt(currentRes[1], 10))
|
||||
}
|
||||
setShowOptions(!showOptions)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="editor-size-selector" ref={sizeSelectorRef}>
|
||||
<div
|
||||
className="editor-size-selector-main"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={showOptionsHandler}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<p>{getSizeShowName(activeSize.toString())}</p>
|
||||
<div className="editor-size-selector-icon">
|
||||
<ChevronUpIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showOptions && (
|
||||
<div className="editor-size-options">
|
||||
{getValidSizes().map(size => (
|
||||
<div
|
||||
className="editor-size-option"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={size}
|
||||
onClick={sizeChangeHandler}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{getSizeShowName(size)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Selector
|
||||
width={100}
|
||||
autoFocusAfterClose={false}
|
||||
value={getSizeShowName(activeSize.toString())}
|
||||
options={getValidSizes()}
|
||||
onChange={sizeChangeHandler}
|
||||
chevronDirection="up"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export enum AIModel {
|
||||
|
||||
function ModelSettingBlock() {
|
||||
const [setting, setSettingState] = useRecoilState(settingState)
|
||||
console.log(setting.model)
|
||||
|
||||
const onModelChange = (value: AIModel) => {
|
||||
setSettingState(old => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import Editor from './Editor/Editor'
|
||||
import ShortcutsModal from './Shortcuts/ShortcutsModal'
|
||||
import SettingModal from './Settings/SettingsModal'
|
||||
import Toast from './shared/Toast'
|
||||
import { Settings, settingState, toastState } from '../store/Atoms'
|
||||
import { settingState, toastState } from '../store/Atoms'
|
||||
import {
|
||||
currentModel,
|
||||
modelDownloaded,
|
||||
@@ -79,7 +79,7 @@ const Workspace = ({ file }: WorkspaceProps) => {
|
||||
return { ...old, model: model as AIModel }
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
}, [setSettingState])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
.modal-mask {
|
||||
z-index: 9999;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
z-index: 9998;
|
||||
inset: 0;
|
||||
background-color: var(--model-mask-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
animation: opacityReveal 150ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes contentShow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -48%) scale(0.96);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: var(--page-bg);
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: grid;
|
||||
grid-auto-rows: max-content;
|
||||
row-gap: 2rem;
|
||||
@@ -19,6 +32,10 @@
|
||||
padding: 2rem;
|
||||
border-radius: 0.95rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
@@ -28,4 +45,6 @@
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { XIcon } from '@heroicons/react/outline'
|
||||
import React, { ReactNode, useRef } from 'react'
|
||||
import { useClickAway, useKey, useKeyPress, useKeyPressEvent } from 'react-use'
|
||||
import React, { ReactNode } from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import Button from './Button'
|
||||
|
||||
export interface ModalProps {
|
||||
@@ -11,34 +11,35 @@ export interface ModalProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function Modal(props: ModalProps) {
|
||||
const Modal = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Root>,
|
||||
ModalProps
|
||||
>((props, forwardedRef) => {
|
||||
const { show, children, onClose, className, title } = props
|
||||
const ref = useRef(null)
|
||||
|
||||
useClickAway(ref, () => {
|
||||
if (show) {
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
onClose?.()
|
||||
}
|
||||
})
|
||||
|
||||
useKeyPressEvent('Escape', e => {
|
||||
if (show) {
|
||||
onClose?.()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-mask"
|
||||
style={{ visibility: show === true ? 'visible' : 'hidden' }}
|
||||
>
|
||||
<div ref={ref} className={`modal ${className}`}>
|
||||
<div className="modal-header">
|
||||
<h2>{title}</h2>
|
||||
<Button icon={<XIcon />} onClick={onClose} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<DialogPrimitive.Root open={show} onOpenChange={onOpenChange}>
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="modal-mask" />
|
||||
<DialogPrimitive.Content
|
||||
ref={forwardedRef}
|
||||
className={`modal ${className}`}
|
||||
>
|
||||
<div className="modal-header">
|
||||
<DialogPrimitive.Title>{title}</DialogPrimitive.Title>
|
||||
<Button icon={<XIcon />} onClick={onClose} />
|
||||
</div>
|
||||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
</DialogPrimitive.Root>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default Modal
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
all: unset;
|
||||
flex: 1 0 auto;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.2rem 0.8rem;
|
||||
line-height: 1;
|
||||
padding: 0.4rem 0.8rem;
|
||||
outline: 1px solid var(--border-color);
|
||||
|
||||
&:focus-visible {
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
@use '../../styles/Mixins' as *;
|
||||
|
||||
.selector {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.select-trigger {
|
||||
all: unset;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.selector-main {
|
||||
@include accented-display(var(white));
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border-radius: 0.5rem;
|
||||
height: 38px;
|
||||
gap: 8px;
|
||||
padding: 0.2rem 0.8rem;
|
||||
|
||||
padding: 0 0.8rem;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--page-bg);
|
||||
color: var(--options-text-color);
|
||||
|
||||
svg {
|
||||
@@ -28,47 +16,52 @@
|
||||
height: 1rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--yellow-accent);
|
||||
}
|
||||
|
||||
// &:focus-visible {
|
||||
// border-color: var(--yellow-accent);
|
||||
// }
|
||||
}
|
||||
|
||||
.selector-options {
|
||||
@include accented-display(var(--btn-primary-bg));
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
justify-self: center;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 3rem;
|
||||
|
||||
color: var(--options-text-color);
|
||||
.select-content {
|
||||
overflow: hidden;
|
||||
background-color: var(--page-bg);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.select-viewport {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
border-radius: 0.6rem;
|
||||
.select-item {
|
||||
all: unset;
|
||||
background-color: var(--page-bg);
|
||||
color: var(--options-text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
@include mobile {
|
||||
bottom: 11.5rem;
|
||||
}
|
||||
padding: 6px 6px 6px 25px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
.selector-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
padding: 0.5rem 0.8rem;
|
||||
|
||||
&:first-of-type {
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--yellow-accent);
|
||||
color: var(--btn-text-hover-color);
|
||||
}
|
||||
&:focus {
|
||||
color: var(--btn-text-hover-color);
|
||||
background-color: var(--yellow-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.select-item-indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 25px;
|
||||
padding-right: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -1,86 +1,85 @@
|
||||
import React, { MutableRefObject, useCallback, useRef, useState } from 'react'
|
||||
import { useClickAway, useKeyPressEvent } from 'react-use'
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/outline'
|
||||
import React, { useRef } from 'react'
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import * as Select from '@radix-ui/react-select'
|
||||
import { relative } from 'path'
|
||||
|
||||
type SelectorChevronDirection = 'up' | 'down'
|
||||
|
||||
type SelectorProps = {
|
||||
minWidth?: number
|
||||
chevronDirection?: SelectorChevronDirection
|
||||
interface Props {
|
||||
width?: number
|
||||
value: string
|
||||
options: string[]
|
||||
chevronDirection?: SelectorChevronDirection
|
||||
autoFocusAfterClose?: boolean
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const selectorDefaultProps = {
|
||||
minWidth: 128,
|
||||
chevronDirection: 'down',
|
||||
}
|
||||
const Selector = (props: Props) => {
|
||||
const {
|
||||
width,
|
||||
value,
|
||||
chevronDirection,
|
||||
options,
|
||||
autoFocusAfterClose,
|
||||
onChange,
|
||||
} = props
|
||||
|
||||
function Selector(props: SelectorProps) {
|
||||
const { minWidth, chevronDirection, value, options, onChange } = props
|
||||
const [showOptions, setShowOptions] = useState<boolean>(false)
|
||||
const selectorRef = useRef<HTMLDivElement | null>(null)
|
||||
const contentRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const showOptionsHandler = () => {
|
||||
// console.log(selectorRef.current?.focus)
|
||||
// selectorRef?.current?.focus()
|
||||
setShowOptions(currentShowOptionsState => !currentShowOptionsState)
|
||||
}
|
||||
|
||||
useClickAway(selectorRef, () => {
|
||||
setShowOptions(false)
|
||||
})
|
||||
|
||||
// TODO: how to prevent Modal close?
|
||||
// useKeyPressEvent('Escape', (e: KeyboardEvent) => {
|
||||
// if (showOptions === true) {
|
||||
// console.log(`selector ${e}`)
|
||||
// e.preventDefault()
|
||||
// e.stopPropagation()
|
||||
// setShowOptions(false)
|
||||
// }
|
||||
// })
|
||||
|
||||
const onOptionClick = (e: any, newIndex: number) => {
|
||||
const currentRes = e.target.textContent.split('x')
|
||||
onChange(currentRes[0])
|
||||
setShowOptions(false)
|
||||
const onOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
if (!autoFocusAfterClose) {
|
||||
// 如果使用 Select.Content 的 onCloseAutoFocus 来取消 focus(防止空格继续打开这个 select)
|
||||
// 会导致其它快捷键失效,原因未知
|
||||
window.setTimeout(() => {
|
||||
contentRef?.current?.blur()
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="selector" ref={selectorRef} style={{ minWidth }}>
|
||||
<div
|
||||
className="selector-main"
|
||||
role="button"
|
||||
onClick={showOptionsHandler}
|
||||
aria-hidden="true"
|
||||
<Select.Root
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<Select.Trigger
|
||||
className="select-trigger"
|
||||
style={{ width }}
|
||||
ref={contentRef}
|
||||
>
|
||||
<p>{value}</p>
|
||||
<div className="selector-icon">
|
||||
<Select.Value />
|
||||
<Select.Icon>
|
||||
{chevronDirection === 'up' ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
</div>
|
||||
</div>
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
|
||||
{showOptions && (
|
||||
<div className="selector-options">
|
||||
{options.map((val, _index) => (
|
||||
<div
|
||||
className="selector-option"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={val}
|
||||
onClick={e => onOptionClick(e, _index)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{val}
|
||||
</div>
|
||||
<Select.Content className="select-content">
|
||||
<Select.Viewport className="select-viewport">
|
||||
{options.map(val => (
|
||||
<Select.Item value={val} className="select-item" key={val}>
|
||||
<Select.ItemText>{val}</Select.ItemText>
|
||||
<Select.ItemIndicator className="select-item-indicator">
|
||||
<CheckIcon />
|
||||
</Select.ItemIndicator>
|
||||
</Select.Item>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const selectorDefaultProps = {
|
||||
chevronDirection: 'down',
|
||||
autoFocusAfterClose: true,
|
||||
}
|
||||
|
||||
Selector.defaultProps = selectorDefaultProps
|
||||
|
||||
export default Selector
|
||||
|
||||
Reference in New Issue
Block a user