mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 22:38:56 +08:00
feat(mobile): search page ui (#8012)
feat(mobile): search page ui fix(core): quick search tags performance issue
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
import { Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { configureMobileModules } from './modules';
|
||||
import { router } from './router';
|
||||
|
||||
if (!environment.isBrowser && environment.isDebug) {
|
||||
@@ -58,6 +59,7 @@ configureBrowserWorkbenchModule(framework);
|
||||
configureLocalStorageStateStorageImpls(framework);
|
||||
configureBrowserWorkspaceFlavours(framework);
|
||||
configureIndexedDBWorkspaceEngineStorageProvider(framework);
|
||||
configureMobileModules(framework);
|
||||
const frameworkProvider = framework.provider();
|
||||
|
||||
// setup application lifecycle events, and emit application start event
|
||||
|
||||
@@ -9,13 +9,16 @@ import {
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
import { forwardRef, type ReactNode, useCallback } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
import { DocCardTags } from './tag';
|
||||
|
||||
export interface DocCardProps extends Omit<WorkbenchLinkProps, 'to'> {
|
||||
meta: DocMeta;
|
||||
meta: {
|
||||
id: DocMeta['id'];
|
||||
title?: ReactNode;
|
||||
} & { [key: string]: any };
|
||||
showTags?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './app-tabs';
|
||||
export * from './doc-card';
|
||||
export * from './page-header';
|
||||
export * from './search-button';
|
||||
export * from './search-input';
|
||||
export * from './search-result';
|
||||
export * from './workspace-selector';
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { SearchIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const SearchButton = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<WorkbenchLink to="/search">
|
||||
<div className={styles.search}>
|
||||
<SearchIcon className={styles.icon} />
|
||||
{t['Quick search']()}
|
||||
</div>
|
||||
</WorkbenchLink>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const search = style({
|
||||
width: '100%',
|
||||
height: 44,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '7px 8px',
|
||||
background: cssVarV2('layer/background/primary'),
|
||||
borderRadius: 10,
|
||||
|
||||
color: cssVarV2('text/secondary'),
|
||||
fontSize: 17,
|
||||
fontWeight: 400,
|
||||
lineHeight: '22px',
|
||||
letterSpacing: -0.43,
|
||||
});
|
||||
|
||||
export const icon = style({
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useAutoFocus } from '@affine/component';
|
||||
import { SearchIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
import { getSvgPath } from 'figma-squircle';
|
||||
import { debounce } from 'lodash-es';
|
||||
import {
|
||||
type FormEventHandler,
|
||||
forwardRef,
|
||||
type HTMLProps,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import * as styles from './style.css';
|
||||
|
||||
export interface SearchInputProps
|
||||
extends Omit<HTMLProps<HTMLInputElement>, 'onInput'> {
|
||||
value?: string;
|
||||
height?: number;
|
||||
cornerRadius?: number;
|
||||
cornerSmoothing?: number;
|
||||
debounce?: number;
|
||||
onInput?: (value: string) => void;
|
||||
}
|
||||
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
||||
function SearchInput(
|
||||
{
|
||||
className,
|
||||
style,
|
||||
placeholder = 'Search',
|
||||
value = '',
|
||||
height = 44,
|
||||
cornerRadius = 10,
|
||||
cornerSmoothing = 0.6,
|
||||
autoFocus,
|
||||
debounce: debounceDuration,
|
||||
onInput,
|
||||
onClick,
|
||||
...attrs
|
||||
},
|
||||
upstreamRef
|
||||
) {
|
||||
const focusRef = useAutoFocus<HTMLInputElement>(autoFocus);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState(window.innerWidth);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
const clipPath = useMemo(
|
||||
() => getSvgPath({ width, height, cornerRadius, cornerSmoothing }),
|
||||
[cornerRadius, cornerSmoothing, height, width]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setWidth(containerRef.current?.offsetWidth ?? 0);
|
||||
}, []);
|
||||
|
||||
const emitValue = useMemo(() => {
|
||||
const cb = (value: string) => onInput?.(value);
|
||||
return debounceDuration ? debounce(cb, debounceDuration) : cb;
|
||||
}, [debounceDuration, onInput]);
|
||||
|
||||
const handleInput: FormEventHandler<HTMLInputElement> = useCallback(
|
||||
e => {
|
||||
const value = e.currentTarget.value;
|
||||
setInputValue(value);
|
||||
emitValue(value);
|
||||
},
|
||||
[emitValue]
|
||||
);
|
||||
|
||||
const inputRef = (el: HTMLInputElement | null) => {
|
||||
focusRef.current = el;
|
||||
if (upstreamRef) {
|
||||
if (typeof upstreamRef === 'function') {
|
||||
upstreamRef(el);
|
||||
} else {
|
||||
upstreamRef.current = el;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
ref={containerRef}
|
||||
className={clsx(styles.wrapper, className)}
|
||||
style={{ ...style, height, clipPath: `path('${clipPath}')` }}
|
||||
>
|
||||
<div className={styles.prefixIcon}>
|
||||
<SearchIcon width="20" height="20" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
{...attrs}
|
||||
value={inputValue}
|
||||
onInput={handleInput}
|
||||
className={styles.input}
|
||||
/>
|
||||
|
||||
{!inputValue ? (
|
||||
<div className={styles.placeholder}>{placeholder}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const wrapper = style({
|
||||
position: 'relative',
|
||||
backgroundColor: cssVarV2('layer/background/primary'),
|
||||
viewTransitionName: 'mobile-search-input',
|
||||
});
|
||||
|
||||
export const prefixIcon = style({
|
||||
position: 'absolute',
|
||||
width: 36,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: cssVarV2('icon/primary'),
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const input = style({
|
||||
padding: '11px 8px 11px 36px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
|
||||
fontWeight: 400,
|
||||
fontSize: 17,
|
||||
lineHeight: '22px',
|
||||
letterSpacing: -0.43,
|
||||
});
|
||||
|
||||
export const placeholder = style([
|
||||
input,
|
||||
{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
pointerEvents: 'none',
|
||||
color: cssVarV2('text/secondary'),
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './search-res-label';
|
||||
export * from './universal-item';
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { QuickSearchItem } from '@affine/core/modules/quicksearch';
|
||||
import { HighlightText } from '@affine/core/modules/quicksearch/views/highlight-text';
|
||||
import { isI18nString, useI18n } from '@affine/i18n';
|
||||
|
||||
export interface SearchResLabelProps {
|
||||
item: QuickSearchItem;
|
||||
}
|
||||
export const SearchResLabel = ({ item }: SearchResLabelProps) => {
|
||||
const i18n = useI18n();
|
||||
|
||||
const text = !isI18nString(item.label)
|
||||
? i18n.t(item.label.title)
|
||||
: i18n.t(item.label);
|
||||
return <HighlightText text={text} start="<b>" end="</b>" />;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const item = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
|
||||
borderBottom: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
height: 44,
|
||||
|
||||
color: 'unset',
|
||||
':visited': { color: 'unset' },
|
||||
':hover': { color: 'unset' },
|
||||
':active': { color: 'unset' },
|
||||
':focus': { color: 'unset' },
|
||||
});
|
||||
export const iconWrapper = style({
|
||||
width: 32,
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
fontSize: 24,
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
export const content = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
fontSize: 17,
|
||||
lineHeight: '22px',
|
||||
fontWeight: 400,
|
||||
letterSpacing: -0.43,
|
||||
color: cssVarV2('text/primary'),
|
||||
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
export const suffixIcon = style({
|
||||
color: cssVarV2('icon/secondary'),
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { QuickSearchItem } from '@affine/core/modules/quicksearch';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { SearchResLabel } from './search-res-label';
|
||||
import * as styles from './universal-item.css';
|
||||
|
||||
export interface UniversalSearchResultItemProps {
|
||||
id: string;
|
||||
item: QuickSearchItem;
|
||||
category: 'tag' | 'collection';
|
||||
}
|
||||
export const UniversalSearchResultItem = ({
|
||||
id,
|
||||
item,
|
||||
category,
|
||||
}: UniversalSearchResultItemProps) => {
|
||||
return (
|
||||
<WorkbenchLink to={`/${category}/${id}`} className={styles.item}>
|
||||
<div className={styles.iconWrapper}>
|
||||
{item.icon &&
|
||||
(typeof item.icon === 'function' ? <item.icon /> : item.icon)}
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<SearchResLabel item={item} />
|
||||
</div>
|
||||
|
||||
<ArrowRightSmallIcon fontSize="16px" className={styles.suffixIcon} />
|
||||
</WorkbenchLink>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { configureMobileSearchModule } from './search';
|
||||
|
||||
export function configureMobileModules(framework: Framework) {
|
||||
configureMobileSearchModule(framework);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { type Framework, WorkspaceScope } from '@toeverything/infra';
|
||||
|
||||
import { MobileSearchService } from './service/search';
|
||||
|
||||
export { MobileSearchService };
|
||||
|
||||
export function configureMobileSearchModule(framework: Framework) {
|
||||
framework.scope(WorkspaceScope).service(MobileSearchService);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
CollectionsQuickSearchSession,
|
||||
DocsQuickSearchSession,
|
||||
RecentDocsQuickSearchSession,
|
||||
TagsQuickSearchSession,
|
||||
} from '@affine/core/modules/quicksearch';
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
export class MobileSearchService extends Service {
|
||||
readonly recentDocs = this.framework.createEntity(
|
||||
RecentDocsQuickSearchSession
|
||||
);
|
||||
readonly collections = this.framework.createEntity(
|
||||
CollectionsQuickSearchSession
|
||||
);
|
||||
readonly docs = this.framework.createEntity(DocsQuickSearchSession);
|
||||
readonly tags = this.framework.createEntity(TagsQuickSearchSession);
|
||||
}
|
||||
@@ -1,9 +1,148 @@
|
||||
import { AppTabs } from '../../components';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import {
|
||||
type QuickSearchItem,
|
||||
QuickSearchTagIcon,
|
||||
} from '@affine/core/modules/quicksearch';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
LiveData,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { AppTabs, SearchInput, SearchResLabel } from '../../components';
|
||||
import { MobileSearchService } from '../../modules/search';
|
||||
import { SearchResults } from '../../views/search/search-results';
|
||||
import * as styles from '../../views/search/style.css';
|
||||
|
||||
const searchInput$ = new LiveData('');
|
||||
|
||||
const RecentList = () => {
|
||||
const { mobileSearchService, collectionService, tagService } = useServices({
|
||||
MobileSearchService,
|
||||
CollectionService,
|
||||
TagService,
|
||||
});
|
||||
const recentDocsList = useLiveData(mobileSearchService.recentDocs.items$);
|
||||
const collections = useLiveData(collectionService.collections$);
|
||||
const tags = useLiveData(
|
||||
LiveData.computed(get =>
|
||||
get(tagService.tagList.tags$).map(tag => ({
|
||||
id: tag.id,
|
||||
title: get(tag.value$),
|
||||
color: get(tag.color$),
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
const docs = useMemo(
|
||||
() =>
|
||||
recentDocsList.map(item => ({
|
||||
id: item.payload.docId,
|
||||
icon: item.icon,
|
||||
title: <SearchResLabel item={item} />,
|
||||
})),
|
||||
[recentDocsList]
|
||||
);
|
||||
|
||||
const collectionList = useMemo(() => {
|
||||
return collections.slice(0, 3).map(item => {
|
||||
return {
|
||||
id: 'collection:' + item.id,
|
||||
source: 'collection',
|
||||
label: { title: item.name },
|
||||
icon: <ViewLayersIcon />,
|
||||
payload: { collectionId: item.id },
|
||||
} satisfies QuickSearchItem<'collection', { collectionId: string }>;
|
||||
});
|
||||
}, [collections]);
|
||||
|
||||
const tagList = useMemo(() => {
|
||||
return tags
|
||||
.reverse()
|
||||
.slice(0, 3)
|
||||
.map(item => {
|
||||
return {
|
||||
id: 'tag:' + item.id,
|
||||
source: 'tag',
|
||||
label: { title: item.title },
|
||||
icon: <QuickSearchTagIcon color={item.color} />,
|
||||
payload: { tagId: item.id },
|
||||
} satisfies QuickSearchItem<'tag', { tagId: string }>;
|
||||
});
|
||||
}, [tags]);
|
||||
|
||||
return (
|
||||
<SearchResults
|
||||
title="Recent"
|
||||
docs={docs}
|
||||
collections={collectionList}
|
||||
tags={tagList}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const WithQueryList = () => {
|
||||
const searchService = useService(MobileSearchService);
|
||||
const collectionList = useLiveData(searchService.collections.items$);
|
||||
const docList = useLiveData(searchService.docs.items$);
|
||||
const tagList = useLiveData(searchService.tags.items$);
|
||||
|
||||
const docs = useMemo(
|
||||
() =>
|
||||
docList.map(item => ({
|
||||
id: item.payload.docId,
|
||||
icon: item.icon,
|
||||
title: <SearchResLabel item={item} />,
|
||||
})),
|
||||
[docList]
|
||||
);
|
||||
|
||||
return (
|
||||
<SearchResults
|
||||
title="Search result"
|
||||
docs={docs}
|
||||
collections={collectionList}
|
||||
tags={tagList}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const searchInput = useLiveData(searchInput$);
|
||||
const searchService = useService(MobileSearchService);
|
||||
|
||||
const onSearch = useCallback(
|
||||
(v: string) => {
|
||||
searchInput$.next(v);
|
||||
searchService.recentDocs.query(v);
|
||||
searchService.collections.query(v);
|
||||
searchService.docs.query(v);
|
||||
searchService.tags.query(v);
|
||||
},
|
||||
[
|
||||
searchService.collections,
|
||||
searchService.docs,
|
||||
searchService.recentDocs,
|
||||
searchService.tags,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
Search
|
||||
<div className={styles.searchHeader}>
|
||||
<SearchInput
|
||||
debounce={300}
|
||||
autoFocus={!searchInput}
|
||||
value={searchInput}
|
||||
onInput={onSearch}
|
||||
placeholder="Search Docs, Collections"
|
||||
/>
|
||||
</div>
|
||||
{searchInput ? <WithQueryList /> : <RecentList />}
|
||||
<AppTabs />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { SettingsIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { SearchButton, WorkspaceSelector } from '../../components';
|
||||
import { SearchInput, WorkspaceSelector } from '../../components';
|
||||
import { useGlobalEvent } from '../../hooks/use-global-events';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
@@ -15,6 +18,9 @@ import * as styles from './styles.css';
|
||||
* - hide Search
|
||||
*/
|
||||
export const HomeHeader = () => {
|
||||
const t = useI18n();
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
const [dense, setDense] = useState(false);
|
||||
|
||||
useGlobalEvent(
|
||||
@@ -24,6 +30,17 @@ export const HomeHeader = () => {
|
||||
}, [])
|
||||
);
|
||||
|
||||
const navSearch = useCallback(() => {
|
||||
if (!document.startViewTransition) {
|
||||
return workbench.open('/search');
|
||||
}
|
||||
|
||||
document.startViewTransition(() => {
|
||||
workbench.open('/search');
|
||||
return new Promise(resolve => setTimeout(resolve, 150));
|
||||
});
|
||||
}, [workbench]);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.root, { dense })}>
|
||||
<div className={styles.float}>
|
||||
@@ -42,7 +59,7 @@ export const HomeHeader = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.searchWrapper}>
|
||||
<SearchButton />
|
||||
<SearchInput placeholder={t['Quick search']()} onClick={navSearch} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.space} />
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useI18n } from '@affine/i18n';
|
||||
|
||||
import { DocCard, type DocCardProps } from '../../components';
|
||||
import {
|
||||
UniversalSearchResultItem,
|
||||
type UniversalSearchResultItemProps,
|
||||
} from '../../components/search-result/universal-item';
|
||||
import * as styles from './style.css';
|
||||
|
||||
export interface SearchResultsProps {
|
||||
title: string;
|
||||
docs?: DocCardProps['meta'][];
|
||||
collections?: UniversalSearchResultItemProps['item'][];
|
||||
tags?: UniversalSearchResultItemProps['item'][];
|
||||
}
|
||||
|
||||
const Empty = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<div className={styles.empty}>{t['com.affine.mobile.search.empty']()}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchResults = ({
|
||||
title,
|
||||
docs,
|
||||
collections,
|
||||
tags,
|
||||
}: SearchResultsProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.resTitle}>{title}</div>
|
||||
|
||||
{!docs?.length && !collections?.length && !tags?.length ? (
|
||||
<Empty />
|
||||
) : null}
|
||||
|
||||
{/* Doc Res */}
|
||||
{docs?.length ? (
|
||||
<div className={styles.resBlock} data-scroll>
|
||||
<div className={styles.resBlockTitle}>Docs</div>
|
||||
<div className={styles.resBlockScrollContent}>
|
||||
<div className={styles.scrollDocsContent}>
|
||||
{docs.map(doc => (
|
||||
<DocCard meta={doc} key={doc.id} className={styles.docCard} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Collection Res */}
|
||||
{collections?.length ? (
|
||||
<div className={styles.resBlock}>
|
||||
<div className={styles.resBlockTitle}>Collections</div>
|
||||
<div className={styles.resBlockListContent}>
|
||||
{collections.map(collection => (
|
||||
<UniversalSearchResultItem
|
||||
category="collection"
|
||||
id={collection.payload.collectionId}
|
||||
key={collection.id}
|
||||
item={collection}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Tag Res */}
|
||||
{tags?.length ? (
|
||||
<div className={styles.resBlock}>
|
||||
<div className={styles.resBlockTitle}>Tags</div>
|
||||
<div className={styles.resBlockListContent}>
|
||||
{tags.map(tag => (
|
||||
<UniversalSearchResultItem
|
||||
category="tag"
|
||||
id={tag.payload.tagId}
|
||||
key={tag.id}
|
||||
item={tag}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const searchHeader = style({
|
||||
padding: 16,
|
||||
});
|
||||
|
||||
export const resTitle = style({
|
||||
padding: '6px 16px',
|
||||
marginBottom: 8,
|
||||
height: 30,
|
||||
|
||||
fontSize: 13,
|
||||
lineHeight: '18px',
|
||||
fontWeight: 400,
|
||||
letterSpacing: -0.08,
|
||||
|
||||
color: cssVarV2('text/secondary'),
|
||||
});
|
||||
|
||||
export const resBlock = style({
|
||||
paddingBottom: 32,
|
||||
selectors: {
|
||||
'&[data-scroll]': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const resBlockTitle = style({
|
||||
padding: '0 16px',
|
||||
fontSize: 20,
|
||||
lineHeight: '25px',
|
||||
fontWeight: 400,
|
||||
letterSpacing: -0.45,
|
||||
color: cssVarV2('text/primary'),
|
||||
});
|
||||
const resBlockContent = style({
|
||||
padding: '12px 0px',
|
||||
});
|
||||
export const resBlockListContent = style([
|
||||
resBlockContent,
|
||||
{
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
},
|
||||
]);
|
||||
export const resBlockScrollContent = style([
|
||||
resBlockContent,
|
||||
{
|
||||
width: '100%',
|
||||
overflowX: 'auto',
|
||||
paddingBottom: 32,
|
||||
},
|
||||
]);
|
||||
export const scrollDocsContent = style({
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
padding: '0 16px',
|
||||
width: 'fit-content',
|
||||
});
|
||||
export const docCard = style({
|
||||
width: 170,
|
||||
height: 210,
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
export const empty = style({
|
||||
padding: '0 16px',
|
||||
fontSize: 20,
|
||||
fontWeight: 400,
|
||||
lineHeight: '25px',
|
||||
letterSpacing: -0.45,
|
||||
color: cssVarV2('text/primary'),
|
||||
});
|
||||
Reference in New Issue
Block a user