feat(mobile): search page ui (#8012)

feat(mobile): search page ui

fix(core): quick search tags performance issue
This commit is contained in:
CatsJuice
2024-08-29 09:05:23 +00:00
parent 5e8683c9be
commit f1bb1fc9b8
26 changed files with 731 additions and 50 deletions

View File

@@ -1,3 +1,4 @@
export * from './hooks';
export * from './lit-react';
export * from './styles';
export * from './ui/avatar';

View File

@@ -0,0 +1,91 @@
import { Entity, LiveData } from '@toeverything/infra';
import Fuse from 'fuse.js';
import type { TagService } from '../../tag';
import type { QuickSearchSession } from '../providers/quick-search-provider';
import type { QuickSearchGroup } from '../types/group';
import type { QuickSearchItem } from '../types/item';
import { highlighter } from '../utils/highlighter';
import { QuickSearchTagIcon } from '../views/tag-icon';
const group: QuickSearchGroup = {
id: 'tags',
label: {
key: 'com.affine.cmdk.affine.category.affine.tags',
},
score: 10,
};
export class TagsQuickSearchSession
extends Entity
implements QuickSearchSession<'tags', { tagId: string }>
{
constructor(private readonly tagService: TagService) {
super();
}
query$ = new LiveData('');
items$: LiveData<QuickSearchItem<'tags', { tagId: string }>[]> =
LiveData.computed(get => {
const query = get(this.query$);
// has performance issues with `tagList.tagMetas$`
const tags = get(this.tagService.tagList.tags$).map(tag => ({
id: tag.id,
title: get(tag.value$),
color: get(tag.color$),
}));
const fuse = new Fuse(tags, {
keys: ['title'],
includeMatches: true,
includeScore: true,
ignoreLocation: true,
threshold: 0.0,
});
const result = fuse.search(query);
return result.map<QuickSearchItem<'tags', { tagId: string }>>(
({ item, matches, score = 1 }) => {
const normalizedRange = ([start, end]: [number, number]) =>
[
start,
end +
1 /* in fuse, the `end` is different from the `substring` */,
] as [number, number];
const titleMatches = matches
?.filter(match => match.key === 'title')
.flatMap(match => match.indices.map(normalizedRange));
const Icon = () => QuickSearchTagIcon({ color: item.color });
return {
id: 'tag:' + item.id,
source: 'tags',
label: {
title: (highlighter(
item.title,
'<b>',
'</b>',
titleMatches ?? []
) ??
item.title) || {
key: 'Untitled',
},
},
group,
score: 1 - score,
icon: Icon,
matches: titleMatches,
payload: { tagId: item.id },
};
}
);
});
query(query: string) {
this.query$.next(query);
}
}

View File

@@ -9,6 +9,7 @@ import {
import { CollectionService } from '../collection';
import { DocsSearchService } from '../docs-search';
import { WorkspacePropertiesAdapter } from '../properties';
import { TagService } from '../tag';
import { WorkbenchService } from '../workbench';
import { QuickSearch } from './entities/quick-search';
import { CollectionsQuickSearchSession } from './impls/collections';
@@ -16,6 +17,7 @@ import { CommandsQuickSearchSession } from './impls/commands';
import { CreationQuickSearchSession } from './impls/creation';
import { DocsQuickSearchSession } from './impls/docs';
import { RecentDocsQuickSearchSession } from './impls/recent-docs';
import { TagsQuickSearchSession } from './impls/tags';
import { CMDKQuickSearchService } from './services/cmdk';
import { DocDisplayMetaService } from './services/doc-display-meta';
import { QuickSearchService } from './services/quick-search';
@@ -28,8 +30,10 @@ export { CommandsQuickSearchSession } from './impls/commands';
export { CreationQuickSearchSession } from './impls/creation';
export { DocsQuickSearchSession } from './impls/docs';
export { RecentDocsQuickSearchSession } from './impls/recent-docs';
export { TagsQuickSearchSession } from './impls/tags';
export type { QuickSearchItem } from './types/item';
export { QuickSearchContainer } from './views/container';
export { QuickSearchTagIcon } from './views/tag-icon';
export function configureQuickSearchModule(framework: Framework) {
framework
@@ -51,6 +55,7 @@ export function configureQuickSearchModule(framework: Framework) {
])
.entity(CreationQuickSearchSession)
.entity(CollectionsQuickSearchSession, [CollectionService])
.entity(TagsQuickSearchSession, [TagService])
.entity(RecentDocsQuickSearchSession, [
RecentDocsService,
DocDisplayMetaService,

View File

@@ -8,6 +8,7 @@ import { CommandsQuickSearchSession } from '../impls/commands';
import { CreationQuickSearchSession } from '../impls/creation';
import { DocsQuickSearchSession } from '../impls/docs';
import { RecentDocsQuickSearchSession } from '../impls/recent-docs';
import { TagsQuickSearchSession } from '../impls/tags';
import type { QuickSearchService } from './quick-search';
export class CMDKQuickSearchService extends Service {
@@ -30,6 +31,7 @@ export class CMDKQuickSearchService extends Service {
this.framework.createEntity(CommandsQuickSearchSession),
this.framework.createEntity(CreationQuickSearchSession),
this.framework.createEntity(DocsQuickSearchSession),
this.framework.createEntity(TagsQuickSearchSession),
],
result => {
if (!result) {
@@ -60,6 +62,8 @@ export class CMDKQuickSearchService extends Service {
this.workbenchService.workbench.openCollection(
result.payload.collectionId
);
} else if (result.source === 'tags') {
this.workbenchService.workbench.openTag(result.payload.tagId);
} else if (result.source === 'creation') {
if (result.id === 'creation:create-page') {
const newDoc = this.docsService.createDoc({

View File

@@ -0,0 +1,12 @@
export const QuickSearchTagIcon = ({ color }: { color: string }) => {
return (
<svg
width="1em"
height="1em"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" fill={color} r="5" />
</svg>
);
};

View File

@@ -544,6 +544,7 @@
"com.affine.cloudTempDisable.description": "We are upgrading the AFFiNE Cloud service and it is temporarily unavailable on the client side. If you wish to stay updated on the progress and be notified on availability, you can fill out the <1>AFFiNE Cloud Signup</1>.",
"com.affine.cloudTempDisable.title": "AFFiNE Cloud is upgrading now.",
"com.affine.cmdk.affine.category.affine.collections": "Collections",
"com.affine.cmdk.affine.category.affine.tags": "Tags",
"com.affine.cmdk.affine.category.affine.creation": "Create",
"com.affine.cmdk.affine.category.affine.edgeless": "Edgeless",
"com.affine.cmdk.affine.category.affine.general": "General",
@@ -1557,5 +1558,6 @@
"com.affine.import-template.dialog.errorLoad": "Failed to load template, please try again.",
"com.affine.import-template.dialog.createDocToWorkspace": "Create doc to \"{{workspace}}\"",
"com.affine.import-template.dialog.createDocToNewWorkspace": "Create into a New Workspace",
"com.affine.import-template.dialog.createDocWithTemplate": "Create doc with \"{{templateName}}\" template"
"com.affine.import-template.dialog.createDocWithTemplate": "Create doc with \"{{templateName}}\" template",
"com.affine.mobile.search.empty": "No results found"
}

View File

@@ -17,13 +17,16 @@
"@toeverything/theme": "^1.0.7",
"clsx": "^2.1.1",
"core-js": "^3.36.1",
"figma-squircle": "^0.3.1",
"intl-segmenter-polyfill-rs": "^0.1.7",
"lodash-es": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.26.1"
},
"devDependencies": {
"@affine/cli": "workspace:*",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.2.75",
"@types/react-dom": "^18.2.24",
"@vanilla-extract/css": "^1.15.5",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
export * from './search-res-label';
export * from './universal-item';

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
import type { Framework } from '@toeverything/infra';
import { configureMobileSearchModule } from './search';
export function configureMobileModules(framework: Framework) {
configureMobileSearchModule(framework);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -663,6 +663,7 @@ __metadata:
"@blocksuite/icons": "npm:^2.1.64"
"@sentry/react": "npm:^8.0.0"
"@toeverything/theme": "npm:^1.0.7"
"@types/lodash-es": "npm:^4.17.12"
"@types/react": "npm:^18.2.75"
"@types/react-dom": "npm:^18.2.24"
"@vanilla-extract/css": "npm:^1.15.5"
@@ -670,7 +671,9 @@ __metadata:
clsx: "npm:^2.1.1"
core-js: "npm:^3.36.1"
cross-env: "npm:^7.0.3"
figma-squircle: "npm:^0.3.1"
intl-segmenter-polyfill-rs: "npm:^0.1.7"
lodash-es: "npm:^4.17.21"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
react-router-dom: "npm:^6.26.1"