feat: add mobile shell and unified search
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Failing after 50s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 20s
ci / .NET API 서버 테스트 (push) Successful in 12s
deploy-site / deploy (push) Successful in 39s
ci / 모바일 린트·타입·Jest (push) Failing after 3h10m31s
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Failing after 50s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 20s
ci / .NET API 서버 테스트 (push) Successful in 12s
deploy-site / deploy (push) Successful in 39s
ci / 모바일 린트·타입·Jest (push) Failing after 3h10m31s
This commit is contained in:
parent
f505a03d34
commit
5ca3709b32
98 changed files with 5168 additions and 2093 deletions
|
|
@ -12,8 +12,9 @@ export interface SearchInputProps {
|
|||
value: string
|
||||
onChange: (value: string) => void
|
||||
placeholder: string
|
||||
/** 포커스 단축키. '/' 만 지원. 빈 문자열이면 끈다 */
|
||||
/** 포커스 단축키. '/' 또는 'Ctrl K'. 빈 문자열이면 끈다 */
|
||||
shortcut?: string
|
||||
autoFocus?: boolean
|
||||
}
|
||||
|
||||
function isTypingTarget(target: EventTarget | null): boolean {
|
||||
|
|
@ -21,17 +22,34 @@ function isTypingTarget(target: EventTarget | null): boolean {
|
|||
return target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)
|
||||
}
|
||||
|
||||
export function SearchInput({ value, onChange, placeholder, shortcut = '/' }: SearchInputProps): React.ReactElement {
|
||||
export function SearchInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
shortcut = '/',
|
||||
autoFocus = false,
|
||||
}: SearchInputProps): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (shortcut !== '/') return undefined
|
||||
const onKeyDown = (e: KeyboardEvent): void => {
|
||||
if (e.key !== '/' || e.ctrlKey || e.metaKey || e.altKey || isTypingTarget(e.target)) return
|
||||
const slash = shortcut === '/'
|
||||
&& e.key === '/'
|
||||
&& !e.ctrlKey
|
||||
&& !e.metaKey
|
||||
&& !e.altKey
|
||||
&& !isTypingTarget(e.target)
|
||||
const commandK = shortcut === 'Ctrl K'
|
||||
&& (e.ctrlKey || e.metaKey)
|
||||
&& !e.altKey
|
||||
&& !e.shiftKey
|
||||
&& e.key.toLocaleLowerCase() === 'k'
|
||||
if (!slash && !commandK) return
|
||||
e.preventDefault()
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
if (shortcut !== '/' && shortcut !== 'Ctrl K') return undefined
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
}, [shortcut])
|
||||
|
|
@ -39,6 +57,7 @@ export function SearchInput({ value, onChange, placeholder, shortcut = '/' }: Se
|
|||
return (
|
||||
<TextField
|
||||
inputRef={inputRef}
|
||||
autoFocus={autoFocus}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue