mirror of
https://github.com/wavetermdev/docusaurus-og.git
synced 2026-08-05 13:46:35 -07:00
feat: implement search component
This commit is contained in:
@@ -5,9 +5,11 @@ import React from 'react'
|
||||
import styles from './style.module.scss'
|
||||
|
||||
import ArrowLCircleSvg from '../../static/icons/arrow-left-circle.svg'
|
||||
import ArrowRCircleSvg from '../../static/icons/arrow-right-circle.svg'
|
||||
import ArrowL from '../../static/icons/arrow-left.svg'
|
||||
import ArrowRCircleSvg from '../../static/icons/arrow-right-circle.svg'
|
||||
import ArrowR from '../../static/icons/arrow-right.svg'
|
||||
import CloseSvg from '../../static/icons/close.svg'
|
||||
import CopySvg from '../../static/icons/copy.svg'
|
||||
import DiscordSvg from '../../static/icons/discord.svg'
|
||||
import DiscourseSvg from '../../static/icons/discourse.svg'
|
||||
import DotSvg from '../../static/icons/dot.svg'
|
||||
@@ -15,12 +17,12 @@ import DropdownSvg from '../../static/icons/dropdown.svg'
|
||||
import FolderSvg from '../../static/icons/folder.svg'
|
||||
import GithubSvg from '../../static/icons/github.svg'
|
||||
import GScholarSvg from '../../static/icons/gscholar.svg'
|
||||
import HistorySvg from '../../static/icons/history.svg'
|
||||
import LinkedinSvg from '../../static/icons/linkedin.svg'
|
||||
import SearchSvg from '../../static/icons/search.svg'
|
||||
import StatusSvg from '../../static/icons/status.svg'
|
||||
import TelegramSvg from '../../static/icons/telegram.svg'
|
||||
import TwitterSvg from '../../static/icons/twitter.svg'
|
||||
import CopySvg from '../../static/icons/copy.svg'
|
||||
|
||||
type TIconProps = {
|
||||
size?: 's' | 'm' | 'l'
|
||||
@@ -157,3 +159,15 @@ export const IconCopy = (props: TIconProps): JSX.Element => (
|
||||
<CopySvg />
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const IconHistory = (props: TIconProps): JSX.Element => (
|
||||
<Icon {...props}>
|
||||
<HistorySvg />
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const IconClose = (props: TIconProps): JSX.Element => (
|
||||
<Icon {...props}>
|
||||
<CloseSvg />
|
||||
</Icon>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
@use '../../css/utils';
|
||||
|
||||
.l-modal {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: 0.3s;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.l-modal__container {
|
||||
max-width: 1376px;
|
||||
margin: 0 auto;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(24, 1fr);
|
||||
gap: 1rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.l-modal--open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.l-modal__content {
|
||||
z-index: 9998;
|
||||
grid-column: 8 / 19;
|
||||
background: rgb(var(--lsd-surface-primary));
|
||||
|
||||
border: 1px solid rgb(var(--lsd-border-primary));
|
||||
padding: 7px;
|
||||
height: fit-content;
|
||||
margin: 64px 0;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.l-modal__backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(var(--lsd-surface-primary), 0.6);
|
||||
z-index: 9997;
|
||||
}
|
||||
|
||||
@include utils.responsive('lg', 'down') {
|
||||
.l-modal {
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.l-modal__container {
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.l-modal__content {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 2;
|
||||
margin: 0;
|
||||
border: none;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.l-modal__backdrop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useLockBodyScroll } from '@docusaurus/theme-common/internal'
|
||||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
import { useKeyPressEvent } from 'react-use'
|
||||
import { useHydrated } from '../../lib/useHydrated'
|
||||
import { Portal } from '../Portal/Portal'
|
||||
import './Modal.scss'
|
||||
|
||||
export type ModalProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
open?: boolean
|
||||
onClose?: () => void
|
||||
keepMounted?: boolean
|
||||
}
|
||||
|
||||
export const Modal: React.FC<ModalProps> = ({
|
||||
open = false,
|
||||
onClose,
|
||||
keepMounted = false,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const hydrated = useHydrated()
|
||||
|
||||
const handleClose = () => {
|
||||
onClose && onClose()
|
||||
}
|
||||
|
||||
useLockBodyScroll(open)
|
||||
useKeyPressEvent(
|
||||
(key) => key.code === 'Escape',
|
||||
(event) => {
|
||||
handleClose()
|
||||
},
|
||||
)
|
||||
|
||||
if (!hydrated) return <></>
|
||||
if (!open && !keepMounted) return <></>
|
||||
|
||||
return (
|
||||
<Portal containerId="lsd-presentation" id={props.id}>
|
||||
<div
|
||||
className={clsx(className, 'l-modal', open && 'l-modal--open')}
|
||||
{...(props as any)}
|
||||
>
|
||||
<div className="l-modal__container">
|
||||
<div className="l-modal__content">{children}</div>
|
||||
<div className="l-modal__backdrop" onClick={handleClose} />
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Modal'
|
||||
@@ -18,5 +18,7 @@ export const Portal: React.FC<React.PropsWithChildren<PortalProps>> = ({
|
||||
|
||||
const portalElement = usePortal({ parentId: containerId })
|
||||
|
||||
return createPortal(children, portalElement, id)
|
||||
const portal = createPortal(children, portalElement, id)
|
||||
|
||||
return portal
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useLocalStorage } from 'react-use'
|
||||
|
||||
export const usePersistedHistory = <T = any>(
|
||||
key: string,
|
||||
options?: {
|
||||
equals?: (a: T, b: T) => boolean
|
||||
unique?: boolean
|
||||
},
|
||||
) => {
|
||||
const unique = options?.unique ?? false
|
||||
const equals = options?.equals ?? ((a, b) => a === b)
|
||||
|
||||
const [history, setHistory] = useLocalStorage<T[]>(
|
||||
'logos-docusaurus-theme-' + key,
|
||||
[],
|
||||
)
|
||||
|
||||
const add = (value: T) => {
|
||||
const arr = history ?? []
|
||||
setHistory([
|
||||
value,
|
||||
...(unique ? arr.filter((item) => !equals(item, value)) : arr),
|
||||
])
|
||||
}
|
||||
|
||||
const remove = (rm: (item: T, index: number) => boolean) => {
|
||||
const arr = history ?? []
|
||||
setHistory(arr.filter((item, index) => !rm(item, index)))
|
||||
}
|
||||
|
||||
const removeByIndex = (index: number) => {
|
||||
remove((item, idx) => idx === index)
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
setHistory([])
|
||||
}
|
||||
|
||||
return {
|
||||
add,
|
||||
clear,
|
||||
remove,
|
||||
removeByIndex,
|
||||
list: history,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.6">
|
||||
<path d="M12.6673 4.27334L11.7273 3.33334L8.00065 7.06001L4.27398 3.33334L3.33398 4.27334L7.06065 8.00001L3.33398 11.7267L4.27398 12.6667L8.00065 8.94001L11.7273 12.6667L12.6673 11.7267L8.94065 8.00001L12.6673 4.27334Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 361 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.52942 18.2352V1.76465H11.1765L16.4706 7.05877V18.2352H3.52942Z" stroke="black" stroke-width="1.2"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.33366 1.33331H4.00033C3.26699 1.33331 2.67366 1.93331 2.67366 2.66665L2.66699 13.3333C2.66699 14.0666 3.26033 14.6666 3.99366 14.6666H12.0003C12.7337 14.6666 13.3337 14.0666 13.3337 13.3333V5.33331L9.33366 1.33331ZM4.00033 13.3333V2.66665H8.66699V5.99998H12.0003V13.3333H4.00033Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 411 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 14C6.46667 14 5.13044 13.4916 3.99133 12.4747C2.85222 11.4578 2.19956 10.1884 2.03333 8.66667H3.4C3.55556 9.82222 4.06956 10.7778 4.942 11.5333C5.81444 12.2889 6.83378 12.6667 8 12.6667C9.3 12.6667 10.4029 12.2138 11.3087 11.308C12.2144 10.4022 12.6671 9.29956 12.6667 8C12.6667 6.7 12.2138 5.59711 11.308 4.69133C10.4022 3.78556 9.29956 3.33289 8 3.33333C7.23333 3.33333 6.51667 3.51111 5.85 3.86667C5.18333 4.22222 4.62222 4.71111 4.16667 5.33333H6V6.66667H2V2.66667H3.33333V4.23333C3.9 3.52222 4.59178 2.97222 5.40867 2.58333C6.22556 2.19444 7.08933 2 8 2C8.83333 2 9.614 2.15844 10.342 2.47533C11.07 2.79222 11.7033 3.21978 12.242 3.758C12.7807 4.29711 13.2084 4.93044 13.5253 5.658C13.8422 6.38556 14.0004 7.16622 14 8C14 8.83333 13.8416 9.614 13.5247 10.342C13.2078 11.07 12.7802 11.7033 12.242 12.242C11.7029 12.7807 11.0696 13.2084 10.342 13.5253C9.61444 13.8422 8.83378 14.0004 8 14ZM9.86667 10.8L7.33333 8.26667V4.66667H8.66667V7.73333L10.8 9.86667L9.86667 10.8Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,11 +1,32 @@
|
||||
@use '../../css/utils';
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
.modal {
|
||||
> div > div:first-child {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include utils.responsive('lg', 'down') {
|
||||
.root {
|
||||
position: unset;
|
||||
.modal {
|
||||
.header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
display: block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import { useLocation } from '@docusaurus/router'
|
||||
import clsx from 'clsx'
|
||||
import { CloseIcon, IconButton, SearchIcon } from '@acid-info/lsd-react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Modal } from '../../components/Modal/Modal'
|
||||
import { usePersistedHistory } from '../../lib/usePersistedHistory'
|
||||
import { useWindowEventListener } from '../../lib/useWindowEventListener'
|
||||
import { useSearch } from './hooks/useSearch'
|
||||
import styles from './SearchBar.module.scss'
|
||||
import { SearchInput } from './SearchInput'
|
||||
import { SearchResults } from './SearchResults'
|
||||
import { SearchResultsContainer } from './SearchResultsContainer'
|
||||
import { SearchResult } from './types'
|
||||
import { SearchHistory } from './SearchHistory/SearchHistory'
|
||||
import { SearchInput } from './SearchInput/SearchInput'
|
||||
import { SearchResults } from './SearchResults/SearchResults'
|
||||
import { SearchResult, SearchResultGroupItem } from './types'
|
||||
|
||||
export const SearchBar: React.FC<{}> = ({}) => {
|
||||
const history = usePersistedHistory<SearchResultGroupItem>('search', {
|
||||
unique: true,
|
||||
equals: (a, b) => a.title === b.title && a.href === b.href,
|
||||
})
|
||||
const search = useSearch()
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
const [input, setInput] = useState('')
|
||||
const [results, setResults] = useState<SearchResult[]>([])
|
||||
const [showResultsContainer, setShowResultsContainer] = useState(false)
|
||||
const [displayModal, setDisplayModal] = useState(false)
|
||||
|
||||
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInput(e.target.value)
|
||||
}
|
||||
|
||||
const onInputFocus = () => {
|
||||
setShowResultsContainer(true)
|
||||
}
|
||||
const focusOnInput = () => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
|
||||
const onInputCancel = () => {
|
||||
setShowResultsContainer(false)
|
||||
}
|
||||
|
||||
const onClickOutsideResultsContainer = (event: Event) => {
|
||||
if (!event.composedPath().find((el) => el === ref.current)) onInputCancel()
|
||||
const inputEl = el.querySelector('input')
|
||||
if (inputEl)
|
||||
setTimeout(() => {
|
||||
inputEl.focus()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const onClear = () => {
|
||||
setInput('')
|
||||
inputRef.current && inputRef.current.focus()
|
||||
}
|
||||
|
||||
const query = async (input: string) => {
|
||||
const { results } = await search.query(input)
|
||||
setResults(results)
|
||||
setShowResultsContainer(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,31 +52,69 @@ export const SearchBar: React.FC<{}> = ({}) => {
|
||||
}, [input])
|
||||
|
||||
useEffect(() => {
|
||||
if (showResultsContainer) {
|
||||
setShowResultsContainer(false)
|
||||
}
|
||||
}, [location.key])
|
||||
displayModal ? focusOnInput() : setInput('')
|
||||
}, [displayModal])
|
||||
|
||||
const onNavigate = (
|
||||
e: React.MouseEvent<HTMLAnchorElement>,
|
||||
item: SearchResultGroupItem,
|
||||
) => {
|
||||
e.preventDefault()
|
||||
setDisplayModal(false)
|
||||
history.add(item)
|
||||
window.location.href = item.href
|
||||
}
|
||||
|
||||
useWindowEventListener(
|
||||
'keydown',
|
||||
(event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyK') {
|
||||
event.preventDefault()
|
||||
setDisplayModal(true)
|
||||
}
|
||||
},
|
||||
{},
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={ref} className={clsx(styles.root)}>
|
||||
<SearchInput
|
||||
value={input}
|
||||
active={showResultsContainer}
|
||||
inputProps={{
|
||||
ref: inputRef,
|
||||
placeholder: 'Enter...',
|
||||
}}
|
||||
onChange={onInputChange}
|
||||
onFocus={onInputFocus}
|
||||
onCancel={onInputCancel}
|
||||
/>
|
||||
<SearchResultsContainer
|
||||
visible={showResultsContainer && input.length > 0}
|
||||
inputRef={inputRef}
|
||||
onClickOutside={onClickOutsideResultsContainer}
|
||||
<>
|
||||
<IconButton onClick={() => setDisplayModal(true)} size="medium">
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
<Modal
|
||||
keepMounted
|
||||
id="search-modal"
|
||||
open={displayModal}
|
||||
onClose={() => setDisplayModal(false)}
|
||||
className={styles.modal}
|
||||
>
|
||||
<SearchResults results={results} onClear={onClear} />
|
||||
</SearchResultsContainer>
|
||||
</div>
|
||||
<div className={styles.header}>
|
||||
<SearchInput
|
||||
containerRef={ref}
|
||||
onClear={onClear}
|
||||
value={input}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.closeButton}
|
||||
size="medium"
|
||||
onClick={() => setDisplayModal(false)}
|
||||
>
|
||||
<CloseIcon color="primary" />
|
||||
</IconButton>
|
||||
</div>
|
||||
{input.length > 0 && (
|
||||
<SearchResults results={results} onNavigate={onNavigate} />
|
||||
)}
|
||||
{input.length === 0 && (
|
||||
<SearchHistory
|
||||
history={history.list ?? []}
|
||||
onRemove={history.removeByIndex}
|
||||
onClose={() => setDisplayModal(false)}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
@use '../../../css/utils';
|
||||
|
||||
.root {
|
||||
padding: 24px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.root.empty {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.itemTitle {
|
||||
flex-grow: 1;
|
||||
padding-left: 1rem;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
mark {
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include utils.responsive('lg', 'down') {
|
||||
.root {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
padding: 24px 24px;
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { Typography } from '@acid-info/lsd-react'
|
||||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
import { IconClose, IconHistory } from '../../../components/Icon/Icon'
|
||||
import { SearchResultMessage } from '../SearchResultMessage/SearchResultMessage'
|
||||
import { SearchResultGroupItem } from '../types'
|
||||
import styles from './SearchHistory.module.scss'
|
||||
|
||||
export type SearchHistoryProps = React.HTMLProps<HTMLDivElement> & {
|
||||
history: SearchResultGroupItem[]
|
||||
onRemove: (index: number) => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export const SearchHistory: React.FC<SearchHistoryProps> = ({
|
||||
history: list = [],
|
||||
onRemove,
|
||||
onClose,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
className,
|
||||
styles.root,
|
||||
list.length === 0 && styles.empty,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{list.length === 0 ? (
|
||||
<SearchResultMessage>No recent searches</SearchResultMessage>
|
||||
) : (
|
||||
<>
|
||||
<Typography
|
||||
className={styles.title}
|
||||
variant="subtitle2"
|
||||
component="div"
|
||||
>
|
||||
Recent
|
||||
</Typography>
|
||||
{list.map((item, index) => (
|
||||
<div key={index} className={styles.item}>
|
||||
<IconHistory />
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
component="a"
|
||||
href={item.href}
|
||||
className={styles.itemTitle}
|
||||
dangerouslySetInnerHTML={{ __html: item.title }}
|
||||
onClick={() => onClose && onClose()}
|
||||
/>
|
||||
<button className="clean-btn" onClick={() => onRemove(index)}>
|
||||
<IconClose />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SearchHistory'
|
||||
+9
-75
@@ -5,96 +5,30 @@
|
||||
.root {
|
||||
width: auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
padding-bottom: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.textField {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.root input {
|
||||
width: 134px;
|
||||
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: rgb(var(--lsd-surface-primary));
|
||||
|
||||
padding: 9px 48px 9px 16px;
|
||||
|
||||
transition: 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.root.expanded input {
|
||||
background: rgb(var(--lsd-surface-primary));
|
||||
width: 334px;
|
||||
}
|
||||
|
||||
.root.expanded .label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
color: #747475;
|
||||
font-size: 0.875rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shortcuts {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
line-height: 0;
|
||||
pointer-events: none;
|
||||
|
||||
& > kbd {
|
||||
margin-left: 0.375rem;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: #bababa;
|
||||
box-shadow: none;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
.root input {
|
||||
color: rgb(var(--lsd-text-primary));
|
||||
}
|
||||
|
||||
.root.expanded input {
|
||||
background-color: rgb(var(--lsd-surface-primary));
|
||||
}
|
||||
}
|
||||
|
||||
@include utils.responsive('lg', 'down') {
|
||||
.shortcuts {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.root input {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
background: #eeeef0;
|
||||
}
|
||||
|
||||
.root.expanded input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
+20
-43
@@ -1,88 +1,65 @@
|
||||
import { useOS } from '@logos-theme/lib/useOS'
|
||||
import { useWindowEventListener } from '@logos-theme/lib/useWindowEventListener'
|
||||
import { SearchIcon, TextField } from '@acid-info/lsd-react'
|
||||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
import { useMedia } from 'react-use'
|
||||
import styles from './SearchInput.module.scss'
|
||||
import { Typography } from '@acid-info/lsd-react'
|
||||
|
||||
export type SearchInputProps = Omit<
|
||||
React.HTMLProps<HTMLDivElement>,
|
||||
'value' | 'onChange'
|
||||
> &
|
||||
Pick<React.HTMLProps<HTMLInputElement>, 'onChange'> & {
|
||||
inputProps?: Omit<React.HTMLProps<HTMLInputElement>, 'ref'> & {
|
||||
ref: React.RefObject<HTMLInputElement>
|
||||
}
|
||||
inputProps?: React.HTMLProps<HTMLInputElement>
|
||||
containerRef?: React.RefObject<HTMLDivElement>
|
||||
value?: string
|
||||
active?: boolean
|
||||
onFocus?: () => void
|
||||
onCancel?: () => void
|
||||
onClear?: () => void
|
||||
}
|
||||
|
||||
export const SearchInput: React.FC<SearchInputProps> = ({
|
||||
value = '',
|
||||
active,
|
||||
onChange,
|
||||
onClear,
|
||||
onFocus: onFocusCallback,
|
||||
onCancel,
|
||||
className,
|
||||
inputProps: { ref: inputRef, ...inputProps } = { placeholder: '' },
|
||||
containerRef,
|
||||
...props
|
||||
}) => {
|
||||
const os = useOS()
|
||||
const isMobile = useMedia('(max-width: 996px)')
|
||||
|
||||
const expanded = active || value?.length > 0
|
||||
|
||||
const focus = () => {
|
||||
inputRef?.current && inputRef.current.focus()
|
||||
}
|
||||
|
||||
const blur = () => {
|
||||
inputRef?.current && inputRef.current.blur()
|
||||
onCancel && onCancel()
|
||||
}
|
||||
|
||||
useWindowEventListener('keydown', (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyK') {
|
||||
event.preventDefault()
|
||||
focus()
|
||||
} else if (event.code === 'Escape') {
|
||||
blur()
|
||||
}
|
||||
})
|
||||
|
||||
const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
onFocusCallback && onFocusCallback()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(styles.root, expanded && styles.expanded, className)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code === 'Escape' && value.length > 0) {
|
||||
e.stopPropagation()
|
||||
onClear && onClear()
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Typography component="span" color="primary">
|
||||
Search
|
||||
</Typography>
|
||||
<input
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={inputRef}
|
||||
onFocus={onFocus}
|
||||
{...inputProps}
|
||||
placeholder={expanded || isMobile ? inputProps.placeholder : ''}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
clearButton
|
||||
icon={<SearchIcon color="primary" />}
|
||||
{...(inputProps as any)}
|
||||
/>
|
||||
<div className={styles.shortcuts}>
|
||||
{active ? (
|
||||
<kbd>esc</kbd>
|
||||
) : (
|
||||
<>
|
||||
<kbd>{os === 'mac' ? '⌘' : 'ctrl'}</kbd>
|
||||
<kbd>k</kbd>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+7
-4
@@ -1,17 +1,20 @@
|
||||
.root {
|
||||
& > div:first-child {
|
||||
padding: 0 14px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
padding: 0 24px;
|
||||
color: #828285;
|
||||
|
||||
&,
|
||||
* {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style: none;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import { Typography } from '@acid-info/lsd-react'
|
||||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
import styles from './SearchResultGroup.module.scss'
|
||||
@@ -15,7 +16,7 @@ export const SearchResultGroup: React.FC<SearchResultGroupProps> = ({
|
||||
return (
|
||||
<div className={clsx(styles.root, className)} {...props}>
|
||||
<div>
|
||||
<span>{title}</span>
|
||||
<Typography variant="subtitle2">{title}</Typography>
|
||||
</div>
|
||||
<ul>{children}</ul>
|
||||
</div>
|
||||
|
||||
+3
-1
@@ -3,11 +3,13 @@
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: auto;
|
||||
|
||||
&.l1 {
|
||||
}
|
||||
|
||||
&.l2 {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.fill {
|
||||
|
||||
+9
-14
@@ -3,26 +3,21 @@
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
padding: 0 24px;
|
||||
|
||||
> span {
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
mark {
|
||||
color: #3165b4;
|
||||
color: inherit;
|
||||
background: none;
|
||||
}
|
||||
|
||||
& > div {
|
||||
margin-left: 26px;
|
||||
|
||||
& > div {
|
||||
color: #373738;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
& > p {
|
||||
color: #828285;
|
||||
font-size: 0.625rem;
|
||||
margin: 0;
|
||||
}
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user