From a2b2cf102ae9dc116195a638f90672064c48d393 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 12 Feb 2026 18:54:02 +0100 Subject: [PATCH 1/7] test simple emoji solution --- .../src/theme/DocCard/index.tsx | 14 ++++++++++++-- website/docs/api/misc/_category_.yml | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx index 076235d68702..a747aa8b5537 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx @@ -25,6 +25,9 @@ import type { import styles from './styles.module.css'; +const startsWithEmoji = (str: string) => + /^\p{Extended_Pictographic}/u.test(str); + function useCategoryItemsPlural() { const {selectMessage} = usePluralForm(); return (count: number) => @@ -101,11 +104,13 @@ function CardCategory({item}: {item: PropSidebarItemCategory}): ReactNode { return null; } + const icon = startsWithEmoji(item.label) ? undefined : '๐Ÿ—ƒ๏ธ'; + return ( @@ -113,7 +118,12 @@ function CardCategory({item}: {item: PropSidebarItemCategory}): ReactNode { } function CardLink({item}: {item: PropSidebarItemLink}): ReactNode { - const icon = isInternalUrl(item.href) ? '๐Ÿ“„๏ธ' : '๐Ÿ”—'; + const icon = startsWithEmoji(item.label) + ? undefined + : isInternalUrl(item.href) + ? '๐Ÿ“„๏ธ' + : '๐Ÿ”—'; + const doc = useDocById(item.docId ?? undefined); return ( Date: Fri, 13 Feb 2026 12:45:04 +0100 Subject: [PATCH 2/7] add extractLeadingEmoji util --- .../docusaurus-theme-common/src/internal.ts | 2 + .../src/utils/__tests__/emojiUtils.test.ts | 66 +++++++++++++++++++ .../src/utils/emojiUtils.ts | 41 ++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 packages/docusaurus-theme-common/src/utils/__tests__/emojiUtils.test.ts create mode 100644 packages/docusaurus-theme-common/src/utils/emojiUtils.ts diff --git a/packages/docusaurus-theme-common/src/internal.ts b/packages/docusaurus-theme-common/src/internal.ts index 9d8b904cd8bd..c89d0edd471d 100644 --- a/packages/docusaurus-theme-common/src/internal.ts +++ b/packages/docusaurus-theme-common/src/internal.ts @@ -91,6 +91,8 @@ export {PluginHtmlClassNameProvider} from './utils/metadataUtils'; export {splitNavbarItems, NavbarProvider} from './utils/navbarUtils'; +export {extractLeadingEmoji} from './utils/emojiUtils'; + export { useTOCHighlight, type TOCHighlightConfig, diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/emojiUtils.test.ts b/packages/docusaurus-theme-common/src/utils/__tests__/emojiUtils.test.ts new file mode 100644 index 000000000000..267702ff8504 --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/__tests__/emojiUtils.test.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {extractLeadingEmoji} from '../emojiUtils'; + +describe('extractLeadingEmoji', () => { + it('extracts simple leading emoji', () => { + expect(extractLeadingEmoji('๐Ÿ˜€ Hello World')).toEqual({ + emoji: '๐Ÿ˜€', + rest: ' Hello World', + }); + }); + + it('extracts only the first emoji', () => { + expect(extractLeadingEmoji('๐Ÿ˜€๐Ÿ˜€ Hello World')).toEqual({ + emoji: '๐Ÿ˜€', + rest: '๐Ÿ˜€ Hello World', + }); + }); + + it('extracts emoji with multiple code points - ๐Ÿ‡ซ๐Ÿ‡ท', () => { + expect(extractLeadingEmoji('๐Ÿ‡ซ๐Ÿ‡ท Hello World')).toEqual({ + emoji: '๐Ÿ‡ซ๐Ÿ‡ท', + rest: ' Hello World', + }); + }); + + it('extracts emoji with multiple code points - ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', () => { + expect(extractLeadingEmoji('๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Hello World')).toEqual({ + emoji: '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', + rest: ' Hello World', + }); + }); + + it('preserves original string', () => { + expect(extractLeadingEmoji('Hello World')).toEqual({ + emoji: null, + rest: 'Hello World', + }); + }); + + it('preserves original string - leading emoji after space', () => { + expect(extractLeadingEmoji(' ๐Ÿ˜€ Hello World')).toEqual({ + emoji: null, + rest: ' ๐Ÿ˜€ Hello World', + }); + }); + + it('preserves original string - middle emoji', () => { + expect(extractLeadingEmoji('Hello ๐Ÿ˜€ World')).toEqual({ + emoji: null, + rest: 'Hello ๐Ÿ˜€ World', + }); + }); + + it('preserves original string - trailing emoji', () => { + expect(extractLeadingEmoji('Hello World ๐Ÿ˜€')).toEqual({ + emoji: null, + rest: 'Hello World ๐Ÿ˜€', + }); + }); +}); diff --git a/packages/docusaurus-theme-common/src/utils/emojiUtils.ts b/packages/docusaurus-theme-common/src/utils/emojiUtils.ts new file mode 100644 index 000000000000..c66cbfb3fa01 --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/emojiUtils.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const segmenter = new Intl.Segmenter(undefined, {granularity: 'grapheme'}); + +/** + * This method splits "โš ๏ธ Hello World" into "โš ๏ธ" + " Hello World". + * It is quite strict and dumb, only useful to handle best-effort heuristics. + * It only extracts a leading emoji if it is the first grapheme of the string. + * It only extracts one emoji, even if multiples are present. + * It doesn't trim the remaining string. + * If you need something more clever, it should be built on top. + * @param input + */ +export function extractLeadingEmoji(input: string): { + emoji: string | null; + rest: string; +} { + const it = segmenter.segment(input)[Symbol.iterator](); + + // const first = segmenter.segment(input).containing(0)?.segment; + const grapheme = it.next().value?.segment; + + if (!grapheme) { + return {emoji: null, rest: input}; + } + + // Leading grapheme contains an emoji (covers flags/ZWJ/skin tones) + if ( + !/\p{Extended_Pictographic}/u.test(grapheme) && + !/\p{Emoji}/u.test(grapheme) + ) { + return {emoji: null, rest: input}; + } + + return {emoji: grapheme, rest: input.slice(grapheme.length)}; +} From 1ddaf7398203ff401d65b30f01b9009eec890ba4 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 13 Feb 2026 13:58:33 +0100 Subject: [PATCH 3/7] Split DocCard + add emoji extraction + customize emoji icon styling --- .../src/theme-classic.d.ts | 41 ++++++++ .../src/theme/DocCard/Description/index.tsx | 22 +++++ .../DocCard/Description/styles.module.css | 10 ++ .../src/theme/DocCard/Heading/index.tsx | 25 +++++ .../theme/DocCard/Heading/styles.module.css | 20 ++++ .../src/theme/DocCard/Layout/index.tsx | 49 ++++++++++ .../DocCard/{ => Layout}/styles.module.css | 14 +-- .../src/theme/DocCard/index.tsx | 93 +++++-------------- 8 files changed, 194 insertions(+), 80 deletions(-) create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCard/Description/index.tsx create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCard/Description/styles.module.css create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCard/Layout/index.tsx rename packages/docusaurus-theme-classic/src/theme/DocCard/{ => Layout}/styles.module.css (81%) diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 03a00f833a76..13644d77530f 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -582,6 +582,47 @@ declare module '@theme/DocCard' { export default function DocCard(props: Props): ReactNode; } +declare module '@theme/DocCard/Heading' { + import type {ReactNode} from 'react'; + import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; + + export interface Props { + readonly item: PropSidebarItem; + readonly icon: ReactNode; + readonly title: string; + } + + export default function DocCardHeading(props: Props): ReactNode; +} + +declare module '@theme/DocCard/Description' { + import type {ReactNode} from 'react'; + import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; + + export interface Props { + readonly item: PropSidebarItem; + readonly description: string; + } + + export default function DocCardDescription(props: Props): ReactNode; +} + +declare module '@theme/DocCard/Layout' { + import type {ReactNode} from 'react'; + import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; + + export interface Props { + readonly item: PropSidebarItem; + readonly className?: string; + readonly href: string; + readonly icon: ReactNode; + readonly title: string; + readonly description?: string; + } + + export default function DocCardLayout(props: Props): ReactNode; +} + declare module '@theme/DocCardList' { import type {ReactNode} from 'react'; import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Description/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Description/index.tsx new file mode 100644 index 000000000000..a227eb41e2eb --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Description/index.tsx @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import type {Props} from '@theme/DocCard/Description'; + +import styles from './styles.module.css'; + +export default function DocCardDescription({description}: Props): ReactNode { + return ( +

+ {description} +

+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Description/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCard/Description/styles.module.css new file mode 100644 index 000000000000..e5f6bc2e960b --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Description/styles.module.css @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.description { + font-size: 0.8rem; +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx new file mode 100644 index 000000000000..7e267e5688df --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx @@ -0,0 +1,25 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Heading from '@theme/Heading'; +import type {Props} from '@theme/DocCard/Heading'; + +import styles from './styles.module.css'; + +export default function DocCardHeading({title, icon}: Props): ReactNode { + return ( + + {icon && {icon}} + {title} + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css new file mode 100644 index 000000000000..973e7b4fc0d9 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.heading { + display: inline-flex; + align-items: center; +} + +.headingIcon { + font-size: 1.6rem; + margin-right: 0.6rem; +} + +.headingText { + font-size: 1.2rem; +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/index.tsx new file mode 100644 index 000000000000..6081367fcb22 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/index.tsx @@ -0,0 +1,49 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import Heading from '@theme/DocCard/Heading'; +import Description from '@theme/DocCard/Description'; +import type {Props} from '@theme/DocCard/Layout'; + +import styles from './styles.module.css'; + +function Container({ + className, + href, + children, +}: { + className?: string; + href: string; + children: ReactNode; +}): ReactNode { + return ( + + {children} + + ); +} + +export default function DocCardLayout({ + item, + className, + href, + icon, + title, + description, +}: Props): ReactNode { + return ( + + + {description && } + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/styles.module.css similarity index 81% rename from packages/docusaurus-theme-classic/src/theme/DocCard/styles.module.css rename to packages/docusaurus-theme-classic/src/theme/DocCard/Layout/styles.module.css index 63c3d9856b70..2144b70f8d1b 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/styles.module.css @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -.cardContainer { +.container { --ifm-link-color: var(--ifm-color-emphasis-800); --ifm-link-hover-color: var(--ifm-color-emphasis-700); --ifm-link-hover-decoration: none; @@ -16,19 +16,11 @@ transition-property: border, box-shadow; } -.cardContainer:hover { +.container:hover { border-color: var(--ifm-color-primary); box-shadow: 0 3px 6px 0 rgb(0 0 0 / 20%); } -.cardContainer *:last-child { +.container *:last-child { margin-bottom: 0; } - -.cardTitle { - font-size: 1.2rem; -} - -.cardDescription { - font-size: 0.8rem; -} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx index a747aa8b5537..4aa078111b32 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx @@ -6,28 +6,22 @@ */ import React, {type ReactNode} from 'react'; -import clsx from 'clsx'; -import Link from '@docusaurus/Link'; import { useDocById, findFirstSidebarItemLink, } from '@docusaurus/plugin-content-docs/client'; import {usePluralForm} from '@docusaurus/theme-common'; +import {extractLeadingEmoji} from '@docusaurus/theme-common/internal'; import isInternalUrl from '@docusaurus/isInternalUrl'; import {translate} from '@docusaurus/Translate'; +import Layout from '@theme/DocCard/Layout'; import type {Props} from '@theme/DocCard'; -import Heading from '@theme/Heading'; import type { PropSidebarItemCategory, PropSidebarItemLink, } from '@docusaurus/plugin-content-docs'; -import styles from './styles.module.css'; - -const startsWithEmoji = (str: string) => - /^\p{Extended_Pictographic}/u.test(str); - function useCategoryItemsPlural() { const {selectMessage} = usePluralForm(); return (count: number) => @@ -45,54 +39,24 @@ function useCategoryItemsPlural() { ); } -function CardContainer({ - className, - href, - children, -}: { - className?: string; - href: string; - children: ReactNode; -}): ReactNode { - return ( - - {children} - - ); +function getFallbackEmojiIcon( + item: PropSidebarItemLink | PropSidebarItemCategory, +): string { + if (item.type === 'category') { + return '๐Ÿ—ƒ'; + } + return isInternalUrl(item.href) ? '๐Ÿ“„๏ธ' : '๐Ÿ”—'; } -function CardLayout({ - className, - href, - icon, - title, - description, -}: { - className?: string; - href: string; - icon: ReactNode; - title: string; - description?: string; -}): ReactNode { - return ( - - - {icon} {title} - - {description && ( -

- {description} -

- )} -
- ); +function getIconTitleProps( + item: PropSidebarItemLink | PropSidebarItemCategory, +): {icon: ReactNode; title: string} { + const extracted = extractLeadingEmoji(item.label); + const emoji = extracted.emoji ?? getFallbackEmojiIcon(item); + return { + icon: emoji, + title: extracted.rest.trim(), + }; } function CardCategory({item}: {item: PropSidebarItemCategory}): ReactNode { @@ -103,35 +67,26 @@ function CardCategory({item}: {item: PropSidebarItemCategory}): ReactNode { if (!href) { return null; } - - const icon = startsWithEmoji(item.label) ? undefined : '๐Ÿ—ƒ๏ธ'; - return ( - ); } function CardLink({item}: {item: PropSidebarItemLink}): ReactNode { - const icon = startsWithEmoji(item.label) - ? undefined - : isInternalUrl(item.href) - ? '๐Ÿ“„๏ธ' - : '๐Ÿ”—'; - const doc = useDocById(item.docId ?? undefined); return ( - ); } From 4af893f65856f9a44975f553aed3cef143899312 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 13 Feb 2026 15:29:08 +0100 Subject: [PATCH 4/7] Add stable classNames + revert to former CSS module classNames --- .../src/theme/DocCard/Description/index.tsx | 7 +++++- .../DocCard/Description/styles.module.css | 2 +- .../src/theme/DocCard/Heading/index.tsx | 25 ++++++++++++++++--- .../theme/DocCard/Heading/styles.module.css | 6 ++--- .../src/theme/DocCard/Layout/index.tsx | 8 +++++- .../theme/DocCard/Layout/styles.module.css | 6 ++--- .../src/utils/ThemeClassNames.ts | 7 ++++++ 7 files changed, 49 insertions(+), 12 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Description/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Description/index.tsx index a227eb41e2eb..55d2ce312b8e 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/Description/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Description/index.tsx @@ -7,6 +7,7 @@ import React, {type ReactNode} from 'react'; import clsx from 'clsx'; +import {ThemeClassNames} from '@docusaurus/theme-common'; import type {Props} from '@theme/DocCard/Description'; import styles from './styles.module.css'; @@ -14,7 +15,11 @@ import styles from './styles.module.css'; export default function DocCardDescription({description}: Props): ReactNode { return (

{description}

diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Description/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCard/Description/styles.module.css index e5f6bc2e960b..c28ebe6cd137 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/Description/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Description/styles.module.css @@ -5,6 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -.description { +.cardDescription { font-size: 0.8rem; } diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx index 7e267e5688df..89164758ffdf 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx @@ -7,6 +7,7 @@ import React, {type ReactNode} from 'react'; import clsx from 'clsx'; +import {ThemeClassNames} from '@docusaurus/theme-common'; import Heading from '@theme/Heading'; import type {Props} from '@theme/DocCard/Heading'; @@ -16,10 +17,28 @@ export default function DocCardHeading({title, icon}: Props): ReactNode { return ( - {icon && {icon}} - {title} + {icon && ( + + {icon} + + )} + + {title} + ); } diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css index 973e7b4fc0d9..57045ab672b6 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css @@ -5,16 +5,16 @@ * LICENSE file in the root directory of this source tree. */ -.heading { +.cardTitle { display: inline-flex; align-items: center; } -.headingIcon { +.cardTitleIcon { font-size: 1.6rem; margin-right: 0.6rem; } -.headingText { +.cardTitleText { font-size: 1.2rem; } diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/index.tsx index 6081367fcb22..448ec09921c8 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/index.tsx @@ -8,6 +8,7 @@ import React, {type ReactNode} from 'react'; import clsx from 'clsx'; import Link from '@docusaurus/Link'; +import {ThemeClassNames} from '@docusaurus/theme-common'; import Heading from '@theme/DocCard/Heading'; import Description from '@theme/DocCard/Description'; import type {Props} from '@theme/DocCard/Layout'; @@ -26,7 +27,12 @@ function Container({ return ( + className={clsx( + 'card padding--lg', + ThemeClassNames.docs.docCard.container, + styles.cardContainer, + className, + )}> {children} ); diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/styles.module.css index 2144b70f8d1b..41ab7bb7d12a 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Layout/styles.module.css @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -.container { +.cardContainer { --ifm-link-color: var(--ifm-color-emphasis-800); --ifm-link-hover-color: var(--ifm-color-emphasis-700); --ifm-link-hover-decoration: none; @@ -16,11 +16,11 @@ transition-property: border, box-shadow; } -.container:hover { +.cardContainer:hover { border-color: var(--ifm-color-primary); box-shadow: 0 3px 6px 0 rgb(0 0 0 / 20%); } -.container *:last-child { +.cardContainer *:last-child { margin-bottom: 0; } diff --git a/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts b/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts index a414908d8870..4b039f652dca 100644 --- a/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts +++ b/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts @@ -100,6 +100,13 @@ export const ThemeClassNames = { docSidebarItemLinkLevel: (level: number) => `theme-doc-sidebar-item-link-level-${level}` as const, // TODO add other stable classNames here + docCard: { + container: 'theme-doc-card-container', + heading: 'theme-doc-card-heading', + icon: 'theme-doc-card-icon', + title: 'theme-doc-card-title', + description: 'theme-doc-card-description', + }, }, blog: { // TODO add other stable classNames here From c1cff2111d2f7bd58e78c950cc423d7c6c19ed02 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 13 Feb 2026 15:41:15 +0100 Subject: [PATCH 5/7] extract useDocCardDescriptionCategoryItemsPlural --- .../src/theme/DocCard/index.tsx | 26 ++++------------- .../docusaurus-theme-common/src/internal.ts | 1 + .../src/translations/docsTranslations.tsx | 28 +++++++++++++++++++ 3 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 packages/docusaurus-theme-common/src/translations/docsTranslations.tsx diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx index 4aa078111b32..643e0063dff6 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/index.tsx @@ -10,10 +10,11 @@ import { useDocById, findFirstSidebarItemLink, } from '@docusaurus/plugin-content-docs/client'; -import {usePluralForm} from '@docusaurus/theme-common'; -import {extractLeadingEmoji} from '@docusaurus/theme-common/internal'; +import { + extractLeadingEmoji, + useDocCardDescriptionCategoryItemsPlural, +} from '@docusaurus/theme-common/internal'; import isInternalUrl from '@docusaurus/isInternalUrl'; -import {translate} from '@docusaurus/Translate'; import Layout from '@theme/DocCard/Layout'; import type {Props} from '@theme/DocCard'; @@ -22,23 +23,6 @@ import type { PropSidebarItemLink, } from '@docusaurus/plugin-content-docs'; -function useCategoryItemsPlural() { - const {selectMessage} = usePluralForm(); - return (count: number) => - selectMessage( - count, - translate( - { - message: '1 item|{count} items', - id: 'theme.docs.DocCard.categoryDescription.plurals', - description: - 'The default description for a category card in the generated index about how many items this category includes', - }, - {count}, - ), - ); -} - function getFallbackEmojiIcon( item: PropSidebarItemLink | PropSidebarItemCategory, ): string { @@ -61,7 +45,7 @@ function getIconTitleProps( function CardCategory({item}: {item: PropSidebarItemCategory}): ReactNode { const href = findFirstSidebarItemLink(item); - const categoryItemsPlural = useCategoryItemsPlural(); + const categoryItemsPlural = useDocCardDescriptionCategoryItemsPlural(); // Unexpected: categories that don't have a link have been filtered upfront if (!href) { diff --git a/packages/docusaurus-theme-common/src/internal.ts b/packages/docusaurus-theme-common/src/internal.ts index c89d0edd471d..02c5776173d1 100644 --- a/packages/docusaurus-theme-common/src/internal.ts +++ b/packages/docusaurus-theme-common/src/internal.ts @@ -105,6 +105,7 @@ export {useLockBodyScroll} from './hooks/useLockBodyScroll'; export {useCodeWordWrap} from './hooks/useCodeWordWrap'; export {useBackToTopButton} from './hooks/useBackToTopButton'; +export {useDocCardDescriptionCategoryItemsPlural} from './translations/docsTranslations'; export { useBlogTagsPostsPageTitle, useBlogAuthorPageTitle, diff --git a/packages/docusaurus-theme-common/src/translations/docsTranslations.tsx b/packages/docusaurus-theme-common/src/translations/docsTranslations.tsx new file mode 100644 index 000000000000..5d6d76f5f69f --- /dev/null +++ b/packages/docusaurus-theme-common/src/translations/docsTranslations.tsx @@ -0,0 +1,28 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {translate} from '@docusaurus/Translate'; +import {usePluralForm} from '../utils/usePluralForm'; + +export function useDocCardDescriptionCategoryItemsPlural(): ( + count: number, +) => string { + const {selectMessage} = usePluralForm(); + return (count: number) => + selectMessage( + count, + translate( + { + message: '1 item|{count} items', + id: 'theme.docs.DocCard.categoryDescription.plurals', + description: + 'The default description for a category card in the generated index about how many items this category includes', + }, + {count}, + ), + ); +} From fb9c7667de1553de9a35e0e0178b600130c1ca9a Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 13 Feb 2026 16:09:19 +0100 Subject: [PATCH 6/7] extract heading into text/icon subcomponents --- .../src/theme-classic.d.ts | 24 ++++++++++++++++++ .../src/theme/DocCard/Heading/Icon/index.tsx | 22 ++++++++++++++++ .../DocCard/Heading/Icon/styles.module.css | 11 ++++++++ .../src/theme/DocCard/Heading/Text/index.tsx | 25 +++++++++++++++++++ .../DocCard/Heading/Text/styles.module.css | 10 ++++++++ .../src/theme/DocCard/Heading/index.tsx | 22 ++++------------ .../theme/DocCard/Heading/styles.module.css | 9 ------- 7 files changed, 97 insertions(+), 26 deletions(-) create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Icon/index.tsx create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Icon/styles.module.css create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/index.tsx create mode 100644 packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/styles.module.css diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 13644d77530f..8dd8f87f8a19 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -595,6 +595,30 @@ declare module '@theme/DocCard/Heading' { export default function DocCardHeading(props: Props): ReactNode; } +declare module '@theme/DocCard/Heading/Icon' { + import type {ReactNode} from 'react'; + import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; + + export interface Props { + readonly item: PropSidebarItem; + readonly icon: ReactNode; + } + + export default function DocCardHeadingIcon(props: Props): ReactNode; +} + +declare module '@theme/DocCard/Heading/Text' { + import type {ReactNode} from 'react'; + import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; + + export interface Props { + readonly item: PropSidebarItem; + readonly title: string; + } + + export default function DocCardHeadingText(props: Props): ReactNode; +} + declare module '@theme/DocCard/Description' { import type {ReactNode} from 'react'; import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Icon/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Icon/index.tsx new file mode 100644 index 000000000000..d646aa277082 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Icon/index.tsx @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import {ThemeClassNames} from '@docusaurus/theme-common'; +import type {Props} from '@theme/DocCard/Heading/Icon'; + +import styles from './styles.module.css'; + +export default function DocCardHeadingIcon({icon}: Props): ReactNode { + return ( + + {icon} + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Icon/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Icon/styles.module.css new file mode 100644 index 000000000000..0362537d321d --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Icon/styles.module.css @@ -0,0 +1,11 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.cardTitleIcon { + font-size: 1.6rem; + margin-right: 0.6rem; +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/index.tsx new file mode 100644 index 000000000000..97add48354e9 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/index.tsx @@ -0,0 +1,25 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, {type ReactNode} from 'react'; +import clsx from 'clsx'; +import {ThemeClassNames} from '@docusaurus/theme-common'; +import type {Props} from '@theme/DocCard/Heading/Text'; + +import styles from './styles.module.css'; + +export default function DocCardHeadingText({title}: Props): ReactNode { + return ( + + {title} + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/styles.module.css new file mode 100644 index 000000000000..a240e033473d --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/styles.module.css @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.cardTitleText { + font-size: 1.2rem; +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx index 89164758ffdf..c3e3c312a85d 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx @@ -9,11 +9,13 @@ import React, {type ReactNode} from 'react'; import clsx from 'clsx'; import {ThemeClassNames} from '@docusaurus/theme-common'; import Heading from '@theme/Heading'; +import Icon from '@theme/DocCard/Heading/Icon'; +import Text from '@theme/DocCard/Heading/Text'; import type {Props} from '@theme/DocCard/Heading'; import styles from './styles.module.css'; -export default function DocCardHeading({title, icon}: Props): ReactNode { +export default function DocCardHeading({item, title, icon}: Props): ReactNode { return ( - {icon && ( - - {icon} - - )} - - {title} - + {icon && } + ); } diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css index 57045ab672b6..9054ee359fa6 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/styles.module.css @@ -9,12 +9,3 @@ display: inline-flex; align-items: center; } - -.cardTitleIcon { - font-size: 1.6rem; - margin-right: 0.6rem; -} - -.cardTitleText { - font-size: 1.2rem; -} From a9d79a36b956cd85df0248a260cf4b3a6fd07691 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 13 Feb 2026 16:37:17 +0100 Subject: [PATCH 7/7] move text--truncate class --- .../src/theme/DocCard/Heading/Text/index.tsx | 2 ++ .../src/theme/DocCard/Heading/index.tsx | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/index.tsx index 97add48354e9..9b091534331f 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/Text/index.tsx @@ -16,6 +16,8 @@ export default function DocCardHeadingText({title}: Props): ReactNode { return ( diff --git a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx index c3e3c312a85d..7e5487e3ff9c 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocCard/Heading/index.tsx @@ -19,11 +19,7 @@ export default function DocCardHeading({item, title, icon}: Props): ReactNode { return ( {icon && }