radix select

This commit is contained in:
Sanster
2022-04-28 13:57:22 +08:00
parent bf1e990f00
commit a297a6d3d0
14 changed files with 499 additions and 303 deletions

View File

@@ -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);
}
}
}

View File

@@ -268,6 +268,7 @@ export default function Editor(props: EditorProps) {
isOriginalLoaded,
windowSize,
initialCentered,
drawOnCurrentRender,
])
// Zoom reset

View File

@@ -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"
/>
)
}

View File

@@ -12,6 +12,7 @@ export enum AIModel {
function ModelSettingBlock() {
const [setting, setSettingState] = useRecoilState(settingState)
console.log(setting.model)
const onModelChange = (value: AIModel) => {
setSettingState(old => {

View File

@@ -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 (
<>

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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