📝 Docs: 升级新版 NonePress 主题 (#2375)

This commit is contained in:
Ju4tCode
2023-09-27 16:00:26 +08:00
committed by GitHub
parent 7754f6da1d
commit 842c6ff4c6
234 changed files with 8759 additions and 5887 deletions

16
website/src/libs/color.ts Normal file
View File

@ -0,0 +1,16 @@
/**
* Choose fg color by bg color
* @see https://www.npmjs.com/package/colord
* @see https://www.w3.org/TR/AERT/#color-contrast
*/
export function pickTextColor(
bgColor: string,
lightColor: string,
darkColor: string
) {
const color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor;
const r = parseInt(color.substring(0, 2), 16); // hexToR
const g = parseInt(color.substring(2, 4), 16); // hexToG
const b = parseInt(color.substring(4, 6), 16); // hexToB
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor;
}

130
website/src/libs/filter.ts Normal file
View File

@ -0,0 +1,130 @@
import { useCallback, useState } from "react";
import { translate } from "@docusaurus/Translate";
import type { Resource } from "./store";
export type Filter<T extends Resource = Resource> = {
type: string;
id: string;
displayName: string;
filter: (resource: T) => boolean;
};
export const tagFilter = <T extends Resource = Resource>(
tag: string
): Filter<T> => ({
type: "tag",
id: `tag-${tag}`,
displayName: translate(
{
id: "pages.store.filter.tagDisplayName",
description: "The display name of tag filter",
message: "标签: {tag}",
},
{ tag }
),
filter: (resource: Resource): boolean =>
resource.tags.map((tag) => tag.label).includes(tag),
});
export const officialFilter = <T extends Resource = Resource>(
official: boolean = true
): Filter<T> => ({
type: "official",
id: `official-${official}`,
displayName: translate({
id: "pages.store.filter.officialDisplayName",
description: "The display name of official filter",
message: "非官方|官方",
}).split("|")[Number(official)],
filter: (resource: Resource): boolean => resource.is_official === official,
});
export const authorFilter = <T extends Resource = Resource>(
author: string
): Filter<T> => ({
type: "author",
id: `author-${author}`,
displayName: translate(
{
id: "pages.store.filter.authorDisplayName",
description: "The display name of author filter",
message: "作者: {author}",
},
{ author }
),
filter: (resource: Resource): boolean => resource.author === author,
});
export const queryFilter = <T extends Resource = Resource>(
query: string
): Filter<T> => ({
type: "query",
id: `query-${query}`,
displayName: query,
filter: (resource: Resource): boolean => {
if (!query) return true;
const queryLower = query.toLowerCase();
const pluginMatch =
resource.resourceType === "plugin" &&
(resource.module_name?.toLowerCase().includes(queryLower) ||
resource.project_link?.toLowerCase().includes(queryLower));
const commonMatch =
resource.name.toLowerCase().includes(queryLower) ||
resource.desc.toLowerCase().includes(queryLower) ||
resource.author.toLowerCase().includes(queryLower) ||
resource.tags.filter((t) => t.label.toLowerCase().includes(queryLower))
.length > 0;
return pluginMatch || commonMatch;
},
});
export function filterResources<T extends Resource>(
resources: T[],
filters: Filter<T>[]
): T[] {
return resources.filter((resource) =>
filters.every((filter) => filter.filter(resource))
);
}
type useFilteredResourcesReturn<T extends Resource> = {
filters: Filter<T>[];
addFilter: (filter: Filter<T>) => void;
removeFilter: (filter: Filter<T> | string) => void;
filteredResources: T[];
};
export function useFilteredResources<T extends Resource>(
resources: T[]
): useFilteredResourcesReturn<T> {
const [filters, setFilters] = useState<Filter<T>[]>([]);
const addFilter = useCallback(
(filter: Filter<T>) => {
if (filters.some((f) => f.id === filter.id)) return;
setFilters((filters) => [...filters, filter]);
},
[filters, setFilters]
);
const removeFilter = useCallback(
(filter: Filter<T> | string) => {
setFilters((filters) =>
filters.filter((f) =>
typeof filter === "string" ? f.id !== filter : f !== filter
)
);
},
[setFilters]
);
const filteredResources = useCallback(
() => filterResources(resources, filters),
[resources, filters]
);
return {
filters,
addFilter,
removeFilter,
filteredResources: filteredResources(),
};
}

View File

@ -1,67 +0,0 @@
import { useLayoutEffect, useRef } from "react";
import ResizeObserver from "resize-observer-polyfill";
export function useResizeNotifier(
element: HTMLElement | undefined,
callback: () => void
) {
const callBackRef = useRef(callback);
useLayoutEffect(() => {
callBackRef.current = callback;
}, [callback]);
useLayoutEffect(() => {
if (!element) return;
const resizeObserver = new ResizeObserver(
withResizeLoopDetection(() => {
callBackRef.current!();
})
);
resizeObserver.observe(element);
return () => {
resizeObserver.disconnect();
};
}, [element]);
}
function withResizeLoopDetection(callback: () => void) {
return (entries: ResizeObserverEntry[], resizeObserver: ResizeObserver) => {
const elements = entries.map((entry) => entry.target);
const rectsBefore = elements.map((element) =>
element.getBoundingClientRect()
);
callback();
const rectsAfter = elements.map((element) =>
element.getBoundingClientRect()
);
const changedElements = elements.filter(
(_, i) => !areRectSizesEqual(rectsBefore[i], rectsAfter[i])
);
changedElements.forEach((element) =>
unobserveUntilNextFrame(element, resizeObserver)
);
};
}
function unobserveUntilNextFrame(
element: Element,
resizeObserver: ResizeObserver
) {
resizeObserver.unobserve(element);
requestAnimationFrame(() => {
resizeObserver.observe(element);
});
}
function areRectSizesEqual(rect1: DOMRect, rect2: DOMRect) {
return rect1.width === rect2.width && rect1.height === rect2.height;
}

View File

@ -0,0 +1,88 @@
import { useCallback, useEffect, useState } from "react";
import { type Filter, useFilteredResources, queryFilter } from "./filter";
import type { Resource } from "./store";
type useSearchControlReturn<T extends Resource> = {
filteredResources: T[];
searcherTags: string[];
addFilter: (filter: Filter<T>) => void;
onSearchQueryChange: (query: string) => void;
onSearchQuerySubmit: () => void;
onSearchBackspace: () => void;
onSearchClear: () => void;
onSearchTagClick: (index: number) => void;
};
export function useSearchControl<T extends Resource>(
resources: T[]
): useSearchControlReturn<T> {
const [currentFilter, setCurrentFilter] = useState<Filter<T> | null>(null);
const { filters, addFilter, removeFilter, filteredResources } =
useFilteredResources(resources);
// display tags in searcher (except current filter)
const [searcherFilters, setSearcherFilters] = useState<Filter<T>[]>([]);
useEffect(() => {
setSearcherFilters(
filters.filter((f) => !(currentFilter && f === currentFilter))
);
}, [filters, currentFilter]);
const onSearchQueryChange = useCallback(
(newQuery: string) => {
// remove current filter if query is empty
if (newQuery === "") {
currentFilter && removeFilter(currentFilter);
setCurrentFilter(null);
return;
}
const newFilter = queryFilter<T>(newQuery);
// do nothing if filter is not changed
if (currentFilter?.id === newFilter.id) return;
// remove old currentFilter
currentFilter && removeFilter(currentFilter);
// add new filter
setCurrentFilter(newFilter);
addFilter(newFilter);
},
[currentFilter, setCurrentFilter, addFilter, removeFilter]
);
const onSearchQuerySubmit = useCallback(() => {
// set current filter to null to make filter permanent
setCurrentFilter(null);
}, [setCurrentFilter]);
const onSearchBackspace = useCallback(() => {
// remove last filter
removeFilter(searcherFilters[searcherFilters.length - 1]);
}, [removeFilter, searcherFilters]);
const onSearchClear = useCallback(() => {
// remove all filters
searcherFilters.forEach((filter) => removeFilter(filter));
}, [removeFilter, searcherFilters]);
const onSearchTagClick = useCallback(
(index: number) => {
removeFilter(searcherFilters[index]);
},
[removeFilter, searcherFilters]
);
return {
filteredResources,
searcherTags: searcherFilters.map((filter) => filter.displayName),
addFilter,
onSearchQueryChange,
onSearchQuerySubmit,
onSearchBackspace,
onSearchClear,
onSearchTagClick,
};
}

View File

@ -1,48 +1,47 @@
import { useState } from "react";
import { translate } from "@docusaurus/Translate";
export type Tag = {
label: string;
color: string;
import type { Adapter, AdaptersResponse } from "@/types/adapter";
import type { Bot, BotsResponse } from "@/types/bot";
import type { Driver, DriversResponse } from "@/types/driver";
import type { Plugin, PluginsResponse } from "@/types/plugin";
type RegistryDataResponseTypes = {
adapter: AdaptersResponse;
bot: BotsResponse;
driver: DriversResponse;
plugin: PluginsResponse;
};
type RegistryDataType = keyof RegistryDataResponseTypes;
type ResourceTypes = {
adapter: Adapter;
bot: Bot;
driver: Driver;
plugin: Plugin;
};
export type Obj = {
module_name?: string;
project_link?: string;
name: string;
desc: string;
author: string;
homepage: string;
tags: Tag[];
is_official: boolean;
};
export type Resource = Adapter | Bot | Driver | Plugin;
export function filterObjs(filter: string, objs: Obj[]): Obj[] {
let filterLower = filter.toLowerCase();
return objs.filter((o) => {
return (
o.module_name?.toLowerCase().includes(filterLower) ||
o.project_link?.toLowerCase().includes(filterLower) ||
o.name.toLowerCase().includes(filterLower) ||
o.desc.toLowerCase().includes(filterLower) ||
o.author.toLowerCase().includes(filterLower) ||
o.tags.filter((t) => t.label.toLowerCase().includes(filterLower)).length >
0
);
export async function fetchRegistryData<T extends RegistryDataType>(
dataType: T
): Promise<ResourceTypes[T][]> {
const resp = await fetch(
`https://registry.nonebot.dev/${dataType}s.json`
).catch((e) => {
throw new Error(`Failed to fetch ${dataType}s: ${e}`);
});
if (!resp.ok)
throw new Error(
`Failed to fetch ${dataType}s: ${resp.status} ${resp.statusText}`
);
const data = (await resp.json()) as RegistryDataResponseTypes[T];
return data.map(
(resource) => ({ ...resource, resourceType: dataType }) as ResourceTypes[T]
);
}
type useFilteredObjsReturn = {
filter: string;
setFilter: (filter: string) => void;
filteredObjs: Obj[];
};
export function useFilteredObjs(objs: Obj[]): useFilteredObjsReturn {
const [filter, setFilter] = useState<string>("");
const filteredObjs = filterObjs(filter, objs);
return {
filter,
setFilter,
filteredObjs,
};
}
export const loadFailedTitle = translate({
id: "pages.store.loadFailed.title",
message: "加载失败",
description: "Title to display when loading content failed",
});

View File

@ -0,0 +1,44 @@
import { authorFilter, tagFilter, type Filter } from "./filter";
import type { Resource } from "./store";
import type { Filter as FilterTool } from "@/components/Store/Toolbar";
type Props<T extends Resource = Resource> = {
resources: T[];
addFilter: (filter: Filter<T>) => void;
};
type useToolbarReturns = {
filters: FilterTool[];
};
export function useToolbar<T extends Resource = Resource>({
resources,
addFilter,
}: Props<T>): useToolbarReturns {
const authorFilterTool: FilterTool = {
label: "作者",
icon: ["fas", "user"],
choices: Array.from(new Set(resources.map((resource) => resource.author))),
onSubmit: (author: string) => {
addFilter(authorFilter(author));
},
};
const tagFilterTool: FilterTool = {
label: "标签",
icon: ["fas", "tag"],
choices: Array.from(
new Set(
resources.flatMap((resource) => resource.tags.map((tag) => tag.label))
)
),
onSubmit: (tag: string) => {
addFilter(tagFilter(tag));
},
};
return {
filters: [authorFilterTool, tagFilterTool],
};
}

View File

@ -1,64 +0,0 @@
import { useLayoutEffect, useState } from "react";
import { useResizeNotifier } from "./resize";
export function getElementWidth(element: HTMLElement) {
const style = getComputedStyle(element);
return (
styleMetricToInt(style.marginLeft) +
getWidth(element) +
styleMetricToInt(style.marginRight)
);
}
export function getContentWidth(element: HTMLElement) {
const style = getComputedStyle(element);
return (
element.getBoundingClientRect().width -
styleMetricToInt(style.borderLeftWidth) -
styleMetricToInt(style.paddingLeft) -
styleMetricToInt(style.paddingRight) -
styleMetricToInt(style.borderRightWidth)
);
}
export function getNonContentWidth(element: HTMLElement) {
const style = getComputedStyle(element);
return (
styleMetricToInt(style.marginLeft) +
styleMetricToInt(style.borderLeftWidth) +
styleMetricToInt(style.paddingLeft) +
styleMetricToInt(style.paddingRight) +
styleMetricToInt(style.borderRightWidth) +
styleMetricToInt(style.marginRight)
);
}
export function getWidth(element: HTMLElement) {
return element.getBoundingClientRect().width;
}
function styleMetricToInt(styleAttribute: string | null) {
return styleAttribute ? parseInt(styleAttribute) : 0;
}
export function useContentWidth(element: HTMLElement | undefined) {
const [width, setWidth] = useState<number>();
function syncWidth() {
const newWidth = element ? getContentWidth(element) : undefined;
if (width !== newWidth) {
setWidth(newWidth);
}
}
useResizeNotifier(element, syncWidth);
useLayoutEffect(syncWidth);
return width;
}