mirror of
https://github.com/wavetermdev/docusaurus-og.git
synced 2026-08-05 13:46:35 -07:00
Merge pull request #31 from acid-info/theme/search-components
feat: search components.
This commit is contained in:
+4
-1
@@ -18,5 +18,8 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.yalc
|
||||
yalc.lock
|
||||
.idea
|
||||
|
||||
static/search-index.json
|
||||
|
||||
+14
-2
@@ -10,7 +10,7 @@ const config = {
|
||||
tagline: 'Codex is building a Decentralized Durability Engine',
|
||||
url: 'https://codex.storage',
|
||||
baseUrl: '/',
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenLinks: 'warn',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
@@ -106,7 +106,19 @@ const config = {
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
}),
|
||||
plugins: ['docusaurus-plugin-sass'],
|
||||
plugins: [
|
||||
'docusaurus-plugin-sass',
|
||||
[
|
||||
'@acid-info/docusaurus-search-local',
|
||||
{
|
||||
hashed: true,
|
||||
indexDocs: true,
|
||||
indexBlog: false,
|
||||
indexPages: true,
|
||||
docsRouteBasePath: '/',
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
|
||||
+5
-1
@@ -18,18 +18,22 @@
|
||||
"prettier:staged": "pretty-quick --staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"@acid-info/docusaurus-search-local": "^0.32.1",
|
||||
"@docusaurus/core": "2.1.0",
|
||||
"@docusaurus/preset-classic": "2.1.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-to-clipboard": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"react-dom": "^17.0.2",
|
||||
"react-use": "^17.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.1.0",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"docusaurus-plugin-sass": "^0.2.2",
|
||||
"husky": "^8.0.1",
|
||||
"prettier": "^2.7.1",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
declare module '@docusaurus/theme-common/internal' {
|
||||
export * from '@docusaurus/theme-common/lib/internal'
|
||||
}
|
||||
|
||||
declare module '@docusaurus/plugin-content-docs/client' {
|
||||
export * from '@docusaurus/plugin-content-docs/lib/client'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export const settle = async <R, E = Error>(
|
||||
promise: Promise<R>,
|
||||
): Promise<[R, undefined] | [undefined, E]> => {
|
||||
try {
|
||||
const result: R = await promise
|
||||
return [result, undefined]
|
||||
} catch (error) {
|
||||
return [undefined, error as E]
|
||||
}
|
||||
}
|
||||
|
||||
export const settleSync = <R, E = Error>(func: () => R): [R, E] => {
|
||||
try {
|
||||
return [func(), null]
|
||||
} catch (error) {
|
||||
return [undefined, error]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
|
||||
export const useEventListener = <
|
||||
T = any,
|
||||
E extends { addEventListener: any; removeEventListener: any } = any,
|
||||
>(
|
||||
key: string | (() => string),
|
||||
element: E | (() => E),
|
||||
listener: (event: T) => void,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
deps?: any[],
|
||||
) => {
|
||||
const _key = useMemo(() => (typeof key === 'string' ? key : key()), [])
|
||||
const _element = useMemo(
|
||||
() => (typeof element === 'function' ? element() : element),
|
||||
[element],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!_element?.addEventListener || !_element?.removeEventListener) return
|
||||
|
||||
_element.addEventListener(_key, listener, options)
|
||||
|
||||
return () => {
|
||||
_element.removeEventListener(_key, listener, options)
|
||||
}
|
||||
}, [_key, _element])
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const useOS = (): 'windows' | 'linux' | 'mac' | 'unknown' => {
|
||||
const str =
|
||||
navigator?.appVersion ?? navigator?.platform ?? navigator?.userAgent
|
||||
|
||||
return /mac/gi.test(str)
|
||||
? 'mac'
|
||||
: /win/gi.test(str)
|
||||
? 'windows'
|
||||
: /linux/gi.test(str)
|
||||
? 'linux'
|
||||
: 'unknown'
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useEventListener } from './useEventListener'
|
||||
|
||||
export const useWindowEventListener = <K extends keyof WindowEventMap, T = any>(
|
||||
key: K | string,
|
||||
listener: (
|
||||
ev: Record<K, any> extends Record<K, WindowEventMap[K]>
|
||||
? WindowEventMap[K]
|
||||
: T,
|
||||
) => void,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
deps: any[] = [],
|
||||
) => {
|
||||
useEventListener(
|
||||
key,
|
||||
() => typeof window !== 'undefined' && window,
|
||||
listener,
|
||||
options,
|
||||
deps,
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { globalStore } from '@site/src/containers/GlobalStore'
|
||||
import Layout from '@theme-original/Layout'
|
||||
import React from 'react'
|
||||
import './styles.scss'
|
||||
|
||||
export default function LayoutWrapper(props) {
|
||||
const store = globalStore.useCreateStore({ hiddenSidebar: false })
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// TODO: move styles to css/custom.scss file.
|
||||
|
||||
.hidden-scrollbar {
|
||||
/* Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Firefox, Edge and IE */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export default function NavbarContent(): JSX.Element {
|
||||
<div className={styles.navbarLogoWrapper}>
|
||||
<NavbarLogo />
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.searchContainer}>
|
||||
<NavbarSearch>
|
||||
<SearchBar />
|
||||
</NavbarSearch>
|
||||
|
||||
@@ -105,6 +105,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.searchContainer > div {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@include utils.responsive('lg', 'down') {
|
||||
.root {
|
||||
padding: 0 var(--ifm-spacing-horizontal);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useLocation } from '@docusaurus/router'
|
||||
import clsx from 'clsx'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
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'
|
||||
|
||||
export const SearchBar: React.FC<{}> = ({}) => {
|
||||
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 onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInput(e.target.value)
|
||||
}
|
||||
|
||||
const onInputFocus = () => {
|
||||
setShowResultsContainer(true)
|
||||
}
|
||||
|
||||
const onInputCancel = () => {
|
||||
setShowResultsContainer(false)
|
||||
}
|
||||
|
||||
const onClickOutsideResultsContainer = (event: Event) => {
|
||||
if (!event.composedPath().find((el) => el === ref.current)) onInputCancel()
|
||||
}
|
||||
|
||||
const onClear = () => {
|
||||
setInput('')
|
||||
inputRef.current.focus()
|
||||
}
|
||||
|
||||
const query = async (input: string) => {
|
||||
const { results } = await search.query(input)
|
||||
setResults(results)
|
||||
setShowResultsContainer(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (input.length > 0) query(input)
|
||||
else setResults([])
|
||||
}, [input])
|
||||
|
||||
useEffect(() => {
|
||||
if (showResultsContainer) {
|
||||
setShowResultsContainer(false)
|
||||
}
|
||||
}, [location.key])
|
||||
|
||||
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}
|
||||
>
|
||||
<SearchResults results={results} onClear={onClear} />
|
||||
</SearchResultsContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// TODO: use ifm variables
|
||||
|
||||
.root {
|
||||
width: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.root input {
|
||||
width: 134px;
|
||||
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: var(--ifm-background-surface-color);
|
||||
|
||||
padding: 9px 48px 9px 16px;
|
||||
|
||||
transition: 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.root.expanded input {
|
||||
background: #eeeef0;
|
||||
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: currentColor;
|
||||
}
|
||||
|
||||
.root.expanded input {
|
||||
background-color: var(--ifm-background-color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useOS } from '@site/src/lib/useOS'
|
||||
import { useWindowEventListener } from '@site/src/lib/useWindowEventListener'
|
||||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
import {} from 'react-use'
|
||||
import styles from './SearchInput.module.scss'
|
||||
|
||||
export type SearchInputProps = Omit<
|
||||
React.HTMLProps<HTMLDivElement>,
|
||||
'value' | 'onChange'
|
||||
> &
|
||||
Pick<React.HTMLProps<HTMLInputElement>, 'onChange'> & {
|
||||
inputProps?: Omit<React.HTMLProps<HTMLInputElement>, 'ref'> & {
|
||||
ref: React.MutableRefObject<HTMLInputElement>
|
||||
}
|
||||
value?: string
|
||||
active?: boolean
|
||||
onFocus?: () => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export const SearchInput: React.FC<SearchInputProps> = ({
|
||||
value,
|
||||
active,
|
||||
onChange,
|
||||
onFocus: onFocusCallback,
|
||||
onCancel,
|
||||
className,
|
||||
inputProps: { ref: inputRef, ...inputProps },
|
||||
...props
|
||||
}) => {
|
||||
const os = useOS()
|
||||
|
||||
const expanded = active || value?.length > 0
|
||||
|
||||
const focus = () => {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
|
||||
const blur = () => {
|
||||
inputRef.current.blur()
|
||||
onCancel && onCancel()
|
||||
}
|
||||
|
||||
useWindowEventListener('keydown', (event) => {
|
||||
if (event.ctrlKey && event.code === 'KeyK') {
|
||||
event.preventDefault()
|
||||
focus()
|
||||
} else if (event.code === 'Escape') {
|
||||
blur()
|
||||
}
|
||||
})
|
||||
|
||||
const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
onFocusCallback && onFocusCallback()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.root, expanded && styles.expanded, className)}
|
||||
{...props}
|
||||
>
|
||||
<span className={styles.label}>Search</span>
|
||||
<input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={inputRef}
|
||||
onFocus={onFocus}
|
||||
{...inputProps}
|
||||
placeholder={expanded ? inputProps.placeholder : ''}
|
||||
/>
|
||||
<div className={styles.shortcuts}>
|
||||
{active ? (
|
||||
<kbd>esc</kbd>
|
||||
) : (
|
||||
<>
|
||||
<kbd>{os === 'mac' ? '⌘' : 'ctrl'}</kbd>
|
||||
<kbd>k</kbd>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SearchInput'
|
||||
@@ -0,0 +1,17 @@
|
||||
.root {
|
||||
& > div:first-child {
|
||||
padding: 0 14px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
color: #828285;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
import styles from './SearchResultGroup.module.scss'
|
||||
|
||||
export type SearchResultGroupProps = React.HTMLProps<HTMLDivElement> & {
|
||||
title: string
|
||||
}
|
||||
|
||||
export const SearchResultGroup: React.FC<SearchResultGroupProps> = ({
|
||||
className,
|
||||
title = '',
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={clsx(styles.root, className)} {...props}>
|
||||
<div>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
<ul>{children}</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SearchResultGroup'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user