Files
authentik/website/docusaurus-theme/theme/DocCardList/index.tsx
Dominic R b1c9a3c5ab website/glossary: improve (#18969)
* website/glossary: Fix eslint errors

* wip
2026-01-08 00:25:22 +00:00

112 lines
3.9 KiB
TypeScript

// Shared utilities and types
import { type GlossaryItem, isGlossaryItem, isGlossaryPath } from "../utils/glossaryUtils";
import ErrorBoundary from "./ErrorBoundary";
import GlossaryDocCardList from "./GlossaryDocCardList";
import styles from "./styles.module.css";
// Docusaurus core imports
import type { PropSidebarItem } from "@docusaurus/plugin-content-docs";
import {
filterDocCardListItems,
useCurrentSidebarSiblings,
} from "@docusaurus/plugin-content-docs/client";
import { useLocation } from "@docusaurus/router";
import DocCard from "@theme/DocCard";
import type { Props } from "@theme/DocCardList";
import clsx from "clsx";
import React, { ReactNode, useMemo } from "react";
// Constant empty array to avoid creating new array on each render
const EMPTY_SIDEBAR_ITEMS: PropSidebarItem[] = [];
// Type aliases for clarity
type SidebarDocLike = Extract<PropSidebarItem, { type: "link" }>;
/**
* Type-safe property existence checker with proper typing
*/
function hasOwnProperty<T extends object, K extends PropertyKey>(
value: T,
key: K,
): value is T & Record<K, unknown> {
return Object.prototype.hasOwnProperty.call(value, key);
}
/**
* Generates stable React keys from sidebar items, with fallback to index
*/
function getStableKey(item: GlossaryItem | PropSidebarItem, idx: number): string | number {
if (typeof item === "object" && item !== null) {
const match = ["docId", "id", "href", "label"].find(
(name) => hasOwnProperty(item, name) && typeof item[name] === "string",
);
return match ? ((item as Record<string, unknown>)[match] as string) : idx;
}
return idx;
}
/**
* Standard documentation card item for non-glossary content
*/
function DocCardListItem({ item }: { item: React.ComponentProps<typeof DocCard>["item"] }) {
return (
<article className={clsx(styles.docCardListItem, "col col--6")}>
<DocCard item={item} />
</article>
);
}
/**
* Enhanced DocCardList component that delegates to specialized components based on content type.
* Provides both standard documentation card rendering and specialized glossary functionality.
*/
export default function DocCardList(props: Props): ReactNode {
const { items, className } = props;
const pathname = useLocation()?.pathname ?? "";
const isGlossary = isGlossaryPath(pathname);
const sidebarSiblings = useCurrentSidebarSiblings();
const siblings = sidebarSiblings ?? EMPTY_SIDEBAR_ITEMS;
// Extract glossary terms from sidebar structure (always computed, but only used for glossary pages)
const glossaryPool = useMemo<SidebarDocLike[]>(() => {
const terms: SidebarDocLike[] = [];
// Recursively process sidebar items to find glossary terms
const processItem = (item: PropSidebarItem) => {
if (isGlossaryItem(item) && item.type === "link") {
terms.push(item as SidebarDocLike);
} else if (item.type === "category" && item.items) {
item.items.forEach(processItem);
}
};
siblings.forEach(processItem);
return terms;
}, [siblings]);
// Standard documentation card items (always computed, but only used for non-glossary pages)
const baseItems = useMemo(() => filterDocCardListItems(items ?? siblings), [items, siblings]);
// For glossary pages, delegate to specialized GlossaryDocCardList component
if (isGlossary) {
return (
<ErrorBoundary>
<GlossaryDocCardList glossaryPool={glossaryPool} className={className} />
</ErrorBoundary>
);
}
// Standard documentation card rendering for non-glossary pages
return (
<section className={clsx("row", className)}>
{baseItems.map((item, idx) => (
<DocCardListItem key={getStableKey(item, idx)} item={item} />
))}
</section>
);
}
export { DocCardList };