diff --git a/.github/workflows/lint-and-tests.yml b/.github/workflows/lint-and-tests.yml
index 4f1dacb2c68ad..86995c912fe6b 100644
--- a/.github/workflows/lint-and-tests.yml
+++ b/.github/workflows/lint-and-tests.yml
@@ -204,7 +204,7 @@ jobs:
# sha reference has no stable git tag reference or URL. see https://github.com/chromaui/chromatic-cli/issues/797
uses: chromaui/action@30b6228aa809059d46219e0f556752e8672a7e26
with:
- workingDir: apps/site
+ workingDir: packages/ui-components
buildScriptName: storybook:build
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitOnceUploaded: true
@@ -220,6 +220,9 @@ jobs:
uses: MishaKav/jest-coverage-comment@d74238813c33e6ea20530ff91b5ea37953d11c91 # v1.0.27
with:
title: 'Unit Test Coverage Report'
- junitxml-path: ./apps/site/junit.xml
- junitxml-title: Unit Test Report
- coverage-summary-path: ./apps/site/coverage/coverage-summary.json
+ multiple-junitxml-files: |
+ @node-core/ui-components, ./packages/ui-components/junit.xml
+ @nodejs/website, ./apps/site/junit.xml
+ multiple-files: |
+ @node-core/ui-components, ./packages/ui-components/coverage/coverage-summary.json
+ @nodejs/website, ./apps/site/coverage/coverage-summary.json
diff --git a/COLLABORATOR_GUIDE.md b/COLLABORATOR_GUIDE.md
index 00b0939cf28fd..00316821f8541 100644
--- a/COLLABORATOR_GUIDE.md
+++ b/COLLABORATOR_GUIDE.md
@@ -102,10 +102,10 @@ The Website also uses several other Open Source libraries (not limited to) liste
Locations are subject to change. (If you are someone updating these paths,
please document those changes here.)
-- React Components are defined on `apps/site/components`
+- React Components are defined on `apps/site/components` and `packages/ui-components`
- React Templates are defined on `apps/site/layouts`
-- Global Stylesheets are declared on `apps/site/styles`
- - Styles are done with [PostCSS][]
+- Global Stylesheets are declared on `packages/ui-components/styles`
+ - Styles are done with [PostCSS][] and [Tailwind][]
- Public files are stored on `apps/site/public`
- Static Images, JavaScript files, and others are stored within `apps/site/public/static`
- Internationalisation is done on `apps/site/i18n`
@@ -126,7 +126,7 @@ please document those changes here.)
- Generation of build-time indexes such as blog data
- Multi-Purpose Scripts are stored within `apps/site/scripts`
- Such as Node.js Release Blog Post generation
-- Storybook Configuration is done within `apps/site/.storybook`
+- Storybook Configuration is done within `packages/ui-components/.storybook`
- We use an almost out-of-the-box Storybook Experience with a few extra customisations
### Adding new Pages
@@ -197,8 +197,6 @@ Finally, if you're unfamiliar with how to use Tailwind or how to use Tailwind wi
- We discourage the usage of any plain CSS styles and tokens, when in doubt ask for help
- We require that you define one Tailwind Token per line, just as shown on the example above, since this improves readability
- Only write CSS within CSS Modules, avoid writing CSS within JavaScript files
-- We recommend creating mixins for reusable animations, effects and more
- - You can create Mixins within the `apps/site/styles/mixins` folder
> \[!NOTE]\
> Tailwind is already configured for this repository. You don't need to import any Tailwind module within your CSS module.\
@@ -211,26 +209,76 @@ Finally, if you're unfamiliar with how to use Tailwind or how to use Tailwind wi
### Best practices when creating a Component
-- All React Components should be placed within the `apps/site/components` folder.
-- Each Component should be placed, whenever possible, within a sub-folder, which we call the "Domain" of the Component
- - The domain represents where these Components belong to or where they will be used.
- - For example, Components used within Article Pages or that are part of the structure of an Article or the Article Layouts,
- should be placed within `apps/site/components/Article`
-- Each component should have its folder with the name of the Component
-- The structure of each component folder follows the following template:
+- **All React components** should be placed within either `@node-core/ui-components` (for reusable components) or `apps/site/components` (for website-specific components).
+- **Generic UI components** that are not tied to the website should be placed in the `@node-core/ui-components` package.
+ - These components should be **framework-agnostic** and must not rely on Next.js-specific features such as `usePathname()` or `useTranslations()`.
+ - If a component previously relied on Next.js, it should now accept these values as **props** instead.
+- **Website-specific components** that rely on Next.js or are tied to the website should remain in `apps/site/components`.
+ - These components can use Next.js-specific hooks, API calls, or configurations.
+ - When using a generic UI component that requires Next.js functionality, pass it as a **prop** instead of modifying the component.
+- **Each component** should be placed within a sub-folder, which we call the **"Domain"** of the component.
+ - The domain represents where the component belongs or where it will be used.
+ - For example, components used within article pages or related to the structure of an article should be placed within `@node-core/ui-components/Common/Article`.
+- **Each component should have its own folder** with the name of the component.
+- The structure of each component folder follows this template:
+
```text
- ComponentName
- - index.tsx // the component itself
- - index.module.css // all styles of the component are placed there
- - index.stories.tsx // component Storybook stories
- - __tests__ // component tests (such as unit tests, etc)
- - index.test.mjs // unit tests should be done in ESM and not TypeScript
+ - index.tsx // The component itself
+ - index.module.css // Component-specific styles
+ - index.stories.tsx // Storybook stories (only for @node-core/ui-components)
+ - __tests__/ // Component tests (such as unit tests, etc.)
+ - index.test.mjs // Unit tests should be done in ESM, not TypeScript
```
-- React Hooks belonging to a single Component should be placed within the Component's folder
- - If the Hook as a wider usability or can be used by other components, it should be placed in the root `hooks` folder.
-- If the Component has "sub-components" they should follow the same philosophy as the Component itself.
- - For example, if the Component `ComponentName` has a sub-component called `SubComponentName`,
- then it should be placed within `ComponentName/SubComponentName`
+
+- **If a component requires Next.js features, it should be wrapped within `apps/site`** rather than being modified directly in `@node-core/ui-components`.
+
+ - Example: A component that requires `usePathname()` should **not** call it directly inside `@node-core/ui-components`. Instead:
+ - The **base component** should accept `pathname` as a prop.
+ - The **wrapper component** in `apps/site` should call `usePathname()` and pass it to the base component.
+
+ Example structure:
+
+ - **Base Component (`@node-core/ui-components`)**
+
+ ```tsx
+ const BaseComponent: FC<...> = ({ pathname, ariaLabel }) => {
+ return <... ariaLabel={ariaLabel}>;
+ };
+ ```
+
+ - **Wrapper Component (`apps/site/components`)**
+
+ ```tsx
+ const Component: FC<...> = (...) => {
+ const pathname = usePathname();
+ const t = useTranslations();
+
+ return ;
+ };
+ ```
+
+ - **Importing Components:**
+ - **For website-specific functionality**, import the wrapper from `apps/site/components`.
+ - **For direct UI use cases**, import from `@node-core/ui-components`.
+
+- **Storybook is now a dependency of `@node-core/ui-components`** and should not be included in `apps/site`.
+
+ - Storybook stories should be written only for components in `@node-core/ui-components`.
+
+- **React Hooks that belong to a single component should be placed within that component’s folder.**
+
+ - If the hook has a **wider usability** or can be used by multiple components, it should be placed in the `apps/site/hooks` folder.
+ - These hooks should only exist in `apps/site`.
+
+- **If a component has sub-components, they should follow the same structure as the main component.**
+ - Example: If `ComponentName` has a sub-component called `SubComponentName`, it should be placed within:
+ ```text
+ - ComponentName/
+ - index.tsx
+ - SubComponentName/
+ - index.tsx
+ ```
#### How a new Component should look like when freshly created
@@ -272,7 +320,7 @@ To add a new download installation method, follow these steps:
- Add a new entry to the `INSTALL_METHODS` array.
- Each entry should have the following properties:
- - `iconImage`: The React component of the icon image for the installation method. This should be an SVG component stored within `apps/site/components/Icons/InstallationMethod` and must follow the other icon component references (being a `FC` supporting `SVGSVGElement` props).
+ - `iconImage`: The React component of the icon image for the installation method. This should be an SVG component stored within `@node-core/ui-components/Icons/InstallationMethod` and must follow the other icon component references (being a `FC` supporting `SVGSVGElement` props).
- Don't forget to add it on the `index.tsx` file from the `InstallationMethod` folder.
- `recommended`: A boolean indicating if this method is recommended. This property is available only for official installation methods.
- `url`: The URL for the installation method.
@@ -379,7 +427,7 @@ Each new feature or bug fix should be accompanied by a unit test (when deemed va
We use [Jest][] as our test runner and [React Testing Library][] for our React unit tests.
We also use [Storybook][] to document our components.
-Each component should have a storybook story that documents the component's usage.
+Components within `packages/ui-components` should have a storybook story that documents the component's usage.
Visual Regression Testing is automatically done via [Chromatic](https://www.chromatic.com/) to ensure that Components are rendered correctly.
@@ -407,7 +455,7 @@ They also allow Developers to preview Components and be able to test them manual
```tsx
import type { Meta as MetaObj, StoryObj } from '@storybook/react';
-import NameOfComponent from '@components/PathTo/YourComponent';
+import NameOfComponent from '@node-core/ui-components/PathTo/YourComponent';
type Story = StoryObj;
type Meta = MetaObj;
@@ -548,7 +596,7 @@ The Node.js Website uses Tailwind as a CSS Framework for crafting our React Comp
#### Font Families on the Website
We use `next/fonts` Open Sans as the default font for the Node.js Website.
-The font is configured as a CSS variable and then configured on `tailwind.config.js` as the default font for the Website.
+The font is configured as a CSS variable and then configured on `packages/ui-components/tailwind.config.ts` as the default font for the Website.
#### Why we use RadixUI?
diff --git a/apps/site/.storybook/main.ts b/apps/site/.storybook/main.ts
deleted file mode 100644
index f6b01fc299b9a..0000000000000
--- a/apps/site/.storybook/main.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { join } from 'node:path';
-
-import type { StorybookConfig } from '@storybook/react-webpack5';
-
-const mocksFolder = join(__dirname, '../components/__mocks__');
-
-const config: StorybookConfig = {
- stories: ['../components/**/*.stories.tsx'],
- logLevel: 'error',
- staticDirs: ['../public'],
- typescript: { reactDocgen: false, check: false },
- core: { disableTelemetry: true, disableWhatsNewNotifications: true },
- framework: '@storybook/react-webpack5',
- swc: () => ({
- jsc: {
- parser: {
- syntax: 'typescript',
- tsx: true,
- },
- transform: {
- react: {
- runtime: 'automatic',
- },
- },
- },
- }),
- addons: [
- '@storybook/addon-webpack5-compiler-swc',
- '@storybook/addon-controls',
- '@storybook/addon-interactions',
- '@storybook/addon-themes',
- '@storybook/addon-viewport',
- {
- name: '@storybook/addon-styling-webpack',
- options: {
- rules: [
- {
- test: /\.css$/,
- use: [
- 'style-loader',
- { loader: 'css-loader', options: { url: false } },
- 'postcss-loader',
- ],
- },
- ],
- },
- },
- ],
- webpack: async config => ({
- ...config,
- // We want to conform as much as possible with our target settings
- target: 'browserslist:development',
- // Performance Hints do not make sense on Storybook as it is bloated by design
- performance: { hints: false },
- // `nodevu` is a Node.js-specific package that requires Node.js modules
- // this is incompatible with Storybook. So we just mock the module
- resolve: {
- ...config.resolve,
- alias: {
- 'next/image': join(mocksFolder, './next-image.mjs'),
- 'next-intl/navigation': join(mocksFolder, './next-intl.mjs'),
- '@/client-context': join(mocksFolder, './client-context.mjs'),
- '@': join(__dirname, '../'),
- },
- },
- // Removes Pesky Critical Dependency Warnings due to `next/font`
- ignoreWarnings: [
- e =>
- e.message.includes('was not found in') ||
- e.message.includes('generated code contains'),
- ],
- }),
-};
-
-export default config;
diff --git a/apps/site/.stylelintignore b/apps/site/.stylelintignore
index cb71469e033ce..a8600c22f4b77 100644
--- a/apps/site/.stylelintignore
+++ b/apps/site/.stylelintignore
@@ -10,8 +10,5 @@ public
# Jest
coverage
-# Storybook
-storybook-static
-
# Old Styles
styles/old
diff --git a/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx b/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx
index 844c9fa6753f5..effcd1719afc8 100644
--- a/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx
+++ b/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx
@@ -1,7 +1,7 @@
+import HexagonGrid from '@node-core/ui-components/Icons/HexagonGrid';
+import JsWhiteIcon from '@node-core/ui-components/Icons/Logos/JsWhite';
import { ImageResponse } from 'next/og';
-import HexagonGrid from '@/components/Icons/HexagonGrid';
-import JsIconWhite from '@/components/Icons/Logos/JsIconWhite';
import { DEFAULT_CATEGORY_OG_TYPE } from '@/next.constants.mjs';
import { defaultLocale } from '@/next.locales.mjs';
import tailwindConfig from '@/tailwind.config';
@@ -37,7 +37,7 @@ export const GET = async (_: Request, props: StaticParams) => {
-
+
{params.title.slice(0, 100)}
diff --git a/apps/site/components/Blog/BlogHeader/index.stories.tsx b/apps/site/components/Blog/BlogHeader/index.stories.tsx
deleted file mode 100644
index 0e29b475119bc..0000000000000
--- a/apps/site/components/Blog/BlogHeader/index.stories.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { Meta as MetaObj, StoryObj } from '@storybook/react';
-
-import BlogHeader from '@/components/Blog/BlogHeader';
-
-type Story = StoryObj;
-type Meta = MetaObj;
-
-export const Default: Story = {
- args: {
- // See `@/site.json` for the `rssFeeds` object
- category: 'all',
- },
- decorators: [
- // We need to wrap to allow global styles to be applied (markdown styles)
- Story => (
-
-
-
- ),
- ],
-};
-
-export default { component: BlogHeader } as Meta;
diff --git a/apps/site/components/Common/BlogPostCard/__tests__/index.test.mjs b/apps/site/components/Blog/BlogPostCard/__tests__/index.test.mjs
similarity index 97%
rename from apps/site/components/Common/BlogPostCard/__tests__/index.test.mjs
rename to apps/site/components/Blog/BlogPostCard/__tests__/index.test.mjs
index e4752d4b09dc9..3094ba35e9c03 100644
--- a/apps/site/components/Common/BlogPostCard/__tests__/index.test.mjs
+++ b/apps/site/components/Blog/BlogPostCard/__tests__/index.test.mjs
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
-import BlogPostCard from '@/components/Common/BlogPostCard';
+import BlogPostCard from '@/components/Blog/BlogPostCard';
function renderBlogPostCard({
title = 'Blog post title',
diff --git a/apps/site/components/Common/BlogPostCard/index.module.css b/apps/site/components/Blog/BlogPostCard/index.module.css
similarity index 100%
rename from apps/site/components/Common/BlogPostCard/index.module.css
rename to apps/site/components/Blog/BlogPostCard/index.module.css
diff --git a/apps/site/components/Common/BlogPostCard/index.tsx b/apps/site/components/Blog/BlogPostCard/index.tsx
similarity index 96%
rename from apps/site/components/Common/BlogPostCard/index.tsx
rename to apps/site/components/Blog/BlogPostCard/index.tsx
index 30815ad9ed62c..b99c1ac13a465 100644
--- a/apps/site/components/Common/BlogPostCard/index.tsx
+++ b/apps/site/components/Blog/BlogPostCard/index.tsx
@@ -1,8 +1,8 @@
+import Preview from '@node-core/ui-components/Common/Preview';
import { useTranslations } from 'next-intl';
import type { FC } from 'react';
import FormattedTime from '@/components/Common/FormattedTime';
-import Preview from '@/components/Common/Preview';
import Link from '@/components/Link';
import WithAvatarGroup from '@/components/withAvatarGroup';
import type { BlogCategory } from '@/types';
diff --git a/apps/site/components/Common/ActiveLink.tsx b/apps/site/components/Common/ActiveLink.tsx
new file mode 100644
index 0000000000000..63f6b2f2e16eb
--- /dev/null
+++ b/apps/site/components/Common/ActiveLink.tsx
@@ -0,0 +1,14 @@
+'use client';
+
+import type { ActiveLocalizedLinkProps } from '@node-core/ui-components/Common/BaseActiveLink';
+import BaseActiveLink from '@node-core/ui-components/Common/BaseActiveLink';
+import type { FC } from 'react';
+
+import Link from '@/components/Link';
+import { usePathname } from '@/navigation.mjs';
+
+const ActiveLink: FC<
+ Omit
+> = props => ;
+
+export default ActiveLink;
diff --git a/apps/site/components/Common/AvatarGroup/Avatar/index.tsx b/apps/site/components/Common/AvatarGroup/Avatar/index.tsx
deleted file mode 100644
index 3e7f98724cb80..0000000000000
--- a/apps/site/components/Common/AvatarGroup/Avatar/index.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import classNames from 'classnames';
-import Image from 'next/image';
-import type { HTMLAttributes } from 'react';
-import { forwardRef } from 'react';
-
-import Link from '@/components/Link';
-
-import styles from './index.module.css';
-
-export type AvatarProps = {
- image?: string;
- name?: string;
- nickname: string;
- fallback?: string;
- size?: 'small' | 'medium';
- url?: string;
-};
-
-// @TODO: We temporarily removed the Avatar Radix UI primitive, since it was causing flashing
-// during initial load and not being able to render nicely when images are already cached.
-// @see https://github.com/radix-ui/primitives/pull/3008
-const Avatar = forwardRef<
- HTMLSpanElement,
- HTMLAttributes & AvatarProps
->(({ image, nickname, name, fallback, url, size = 'small', ...props }, ref) => {
- const Wrapper = url ? Link : 'div';
-
- return (
-
-
- {image && (
-
- )}
-
- {!image && (
-
- {fallback}
-
- )}
-
-
- );
-});
-
-export default Avatar;
diff --git a/apps/site/components/Common/AvatarGroup/Overlay/index.stories.tsx b/apps/site/components/Common/AvatarGroup/Overlay/index.stories.tsx
deleted file mode 100644
index 1c462999ccd77..0000000000000
--- a/apps/site/components/Common/AvatarGroup/Overlay/index.stories.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Meta as MetaObj, StoryObj } from '@storybook/react';
-
-import AvatarOverlay from '@/components/Common/AvatarGroup/Overlay';
-import { getAuthorWithId, getAuthorWithName } from '@/util/authorUtils';
-
-type Story = StoryObj;
-type Meta = MetaObj;
-
-export const Default: Story = {
- args: getAuthorWithId(['nodejs'], true)[0],
-};
-
-export const FallBack: Story = {
- args: getAuthorWithName(['Node.js'], true)[0],
-};
-
-export const WithoutName: Story = {
- args: getAuthorWithId(['canerakdas'], true)[0],
-};
-
-export default { component: AvatarOverlay } as Meta;
diff --git a/apps/site/components/Common/AvatarGroup/index.tsx b/apps/site/components/Common/AvatarGroup/index.tsx
deleted file mode 100644
index 2bacc71b8ab7b..0000000000000
--- a/apps/site/components/Common/AvatarGroup/index.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-'use client';
-
-import classNames from 'classnames';
-import type { FC } from 'react';
-import { useState, useMemo, Fragment } from 'react';
-
-import type { AvatarProps } from '@/components/Common/AvatarGroup/Avatar';
-import Avatar from '@/components/Common/AvatarGroup/Avatar';
-import avatarstyles from '@/components/Common/AvatarGroup/Avatar/index.module.css';
-import AvatarOverlay from '@/components/Common/AvatarGroup/Overlay';
-import Tooltip from '@/components/Common/Tooltip';
-
-import styles from './index.module.css';
-
-type AvatarGroupProps = {
- avatars: Array;
- limit?: number;
- isExpandable?: boolean;
- size?: AvatarProps['size'];
- container?: HTMLElement;
-};
-
-const AvatarGroup: FC = ({
- avatars,
- limit = 10,
- isExpandable = true,
- size = 'small',
- container,
-}) => {
- const [showMore, setShowMore] = useState(false);
-
- const renderAvatars = useMemo(
- () => avatars.slice(0, showMore ? avatars.length : limit),
- [showMore, avatars, limit]
- );
-
- return (
-
@@ -45,4 +50,4 @@ const LinkTabs: FC> = ({
>
);
-export default LinkTabs;
+export default BaseLinkTabs;
diff --git a/apps/site/components/Common/Pagination/Ellipsis/index.module.css b/packages/ui-components/Common/BasePagination/Ellipsis/index.module.css
similarity index 100%
rename from apps/site/components/Common/Pagination/Ellipsis/index.module.css
rename to packages/ui-components/Common/BasePagination/Ellipsis/index.module.css
diff --git a/apps/site/components/Common/Pagination/Ellipsis/index.stories.tsx b/packages/ui-components/Common/BasePagination/Ellipsis/index.stories.tsx
similarity index 74%
rename from apps/site/components/Common/Pagination/Ellipsis/index.stories.tsx
rename to packages/ui-components/Common/BasePagination/Ellipsis/index.stories.tsx
index 0949bcc20cf72..916c1f6c9b802 100644
--- a/apps/site/components/Common/Pagination/Ellipsis/index.stories.tsx
+++ b/packages/ui-components/Common/BasePagination/Ellipsis/index.stories.tsx
@@ -1,6 +1,6 @@
import type { Meta as MetaObj, StoryObj } from '@storybook/react';
-import Ellipsis from '@/components/Common/Pagination/Ellipsis';
+import Ellipsis from '@node-core/ui-components/Common/BasePagination/Ellipsis';
type Story = StoryObj;
type Meta = MetaObj;
diff --git a/apps/site/components/Common/Pagination/Ellipsis/index.tsx b/packages/ui-components/Common/BasePagination/Ellipsis/index.tsx
similarity index 100%
rename from apps/site/components/Common/Pagination/Ellipsis/index.tsx
rename to packages/ui-components/Common/BasePagination/Ellipsis/index.tsx
diff --git a/apps/site/components/Common/Pagination/PaginationListItem/__tests__/index.test.mjs b/packages/ui-components/Common/BasePagination/PaginationListItem/__tests__/index.test.mjs
similarity index 92%
rename from apps/site/components/Common/Pagination/PaginationListItem/__tests__/index.test.mjs
rename to packages/ui-components/Common/BasePagination/PaginationListItem/__tests__/index.test.mjs
index 9c0d2b26f8c6e..eff421e52fa06 100644
--- a/apps/site/components/Common/Pagination/PaginationListItem/__tests__/index.test.mjs
+++ b/packages/ui-components/Common/BasePagination/PaginationListItem/__tests__/index.test.mjs
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
-import PaginationListItem from '@/components/Common/Pagination/PaginationListItem';
+import PaginationListItem from '@node-core/ui-components/Common/BasePagination/PaginationListItem';
function renderPaginationListItem({
url,
diff --git a/apps/site/components/Common/Pagination/PaginationListItem/index.module.css b/packages/ui-components/Common/BasePagination/PaginationListItem/index.module.css
similarity index 100%
rename from apps/site/components/Common/Pagination/PaginationListItem/index.module.css
rename to packages/ui-components/Common/BasePagination/PaginationListItem/index.module.css
diff --git a/apps/site/components/Common/Pagination/PaginationListItem/index.stories.tsx b/packages/ui-components/Common/BasePagination/PaginationListItem/index.stories.tsx
similarity index 87%
rename from apps/site/components/Common/Pagination/PaginationListItem/index.stories.tsx
rename to packages/ui-components/Common/BasePagination/PaginationListItem/index.stories.tsx
index ed4de699b966e..5a0934c9a39d9 100644
--- a/apps/site/components/Common/Pagination/PaginationListItem/index.stories.tsx
+++ b/packages/ui-components/Common/BasePagination/PaginationListItem/index.stories.tsx
@@ -1,6 +1,6 @@
import type { Meta as MetaObj, StoryObj } from '@storybook/react';
-import PaginationListItem from '@/components/Common/Pagination/PaginationListItem';
+import PaginationListItem from '@node-core/ui-components/Common/BasePagination/PaginationListItem';
type Story = StoryObj;
type Meta = MetaObj;
diff --git a/apps/site/components/Common/Pagination/PaginationListItem/index.tsx b/packages/ui-components/Common/BasePagination/PaginationListItem/index.tsx
similarity index 75%
rename from apps/site/components/Common/Pagination/PaginationListItem/index.tsx
rename to packages/ui-components/Common/BasePagination/PaginationListItem/index.tsx
index a1fd525c7a58f..66543d4c8601a 100644
--- a/apps/site/components/Common/Pagination/PaginationListItem/index.tsx
+++ b/packages/ui-components/Common/BasePagination/PaginationListItem/index.tsx
@@ -1,7 +1,6 @@
-import { useTranslations } from 'next-intl';
import type { FC } from 'react';
-import Link from '@/components/Link';
+import type { LinkLike } from '@node-core/ui-components/types';
import styles from './index.module.css';
@@ -11,6 +10,8 @@ export type PaginationListItemProps = {
// One-based number of the current page
currentPage: number;
totalPages: number;
+ as?: LinkLike;
+ label: string;
};
const PaginationListItem: FC = ({
@@ -18,19 +19,19 @@ const PaginationListItem: FC = ({
pageNumber,
currentPage,
totalPages,
+ as: Component = 'a',
+ label,
}) => {
- const t = useTranslations();
-
return (
-
{pageNumber}
-
+
);
};
diff --git a/apps/site/components/Common/PrevNextArrow.tsx b/packages/ui-components/Common/BasePagination/PrevNextArrow.tsx
similarity index 100%
rename from apps/site/components/Common/PrevNextArrow.tsx
rename to packages/ui-components/Common/BasePagination/PrevNextArrow.tsx
diff --git a/apps/site/components/Common/Pagination/__tests__/index.test.mjs b/packages/ui-components/Common/BasePagination/__tests__/index.test.mjs
similarity index 89%
rename from apps/site/components/Common/Pagination/__tests__/index.test.mjs
rename to packages/ui-components/Common/BasePagination/__tests__/index.test.mjs
index b28e69af50f65..8e4f5b710db0e 100644
--- a/apps/site/components/Common/Pagination/__tests__/index.test.mjs
+++ b/packages/ui-components/Common/BasePagination/__tests__/index.test.mjs
@@ -1,6 +1,15 @@
import { render, screen } from '@testing-library/react';
-import Pagination from '@/components/Common/Pagination';
+import BasePagination from '@node-core/ui-components/Common/BasePagination';
+
+const getPageLabel = number => number.toString();
+const labels = {
+ aria: 'Aria',
+ prevAria: 'Previous Aria',
+ prev: 'Previous',
+ nextAria: 'Next Aria',
+ next: 'Next',
+};
function renderPagination({
currentPage = 1,
@@ -12,10 +21,12 @@ function renderPagination({
.map(item => ({ url: `${item.url}-${Math.random()}` }));
render(
-
);
@@ -34,13 +45,13 @@ describe('Pagination', () => {
expect(
screen.getByRole('button', {
- name: 'components.common.pagination.prevAriaLabel',
+ name: labels.prevAria,
})
).toBeVisible();
expect(
screen.getByRole('button', {
- name: 'components.common.pagination.nextAriaLabel',
+ name: labels.nextAria,
})
).toBeVisible();
});
@@ -160,7 +171,7 @@ describe('Pagination', () => {
expect(
screen.getByRole('button', {
- name: 'components.common.pagination.prevAriaLabel',
+ name: labels.prevAria,
})
).toHaveAttribute('aria-disabled');
});
@@ -173,7 +184,7 @@ describe('Pagination', () => {
expect(
screen.getByRole('button', {
- name: 'components.common.pagination.nextAriaLabel',
+ name: labels.nextAria,
})
).toHaveAttribute('aria-disabled');
});
diff --git a/apps/site/components/Common/Pagination/index.module.css b/packages/ui-components/Common/BasePagination/index.module.css
similarity index 100%
rename from apps/site/components/Common/Pagination/index.module.css
rename to packages/ui-components/Common/BasePagination/index.module.css
diff --git a/apps/site/components/Common/Pagination/index.stories.tsx b/packages/ui-components/Common/BasePagination/index.stories.tsx
similarity index 69%
rename from apps/site/components/Common/Pagination/index.stories.tsx
rename to packages/ui-components/Common/BasePagination/index.stories.tsx
index 2fc4287fea3c3..98fb6397da289 100644
--- a/apps/site/components/Common/Pagination/index.stories.tsx
+++ b/packages/ui-components/Common/BasePagination/index.stories.tsx
@@ -1,15 +1,23 @@
import type { Meta as MetaObj, StoryObj } from '@storybook/react';
-import Pagination from '@/components/Common/Pagination';
+import BasePagination from '@node-core/ui-components/Common/BasePagination';
-type Story = StoryObj;
-type Meta = MetaObj;
+type Story = StoryObj;
+type Meta = MetaObj;
export const Default: Story = {
args: {
currentPage: 1,
currentPageSiblingsCount: 1,
pages: [{ url: '1' }, { url: '2' }, { url: '3' }],
+ getPageLabel: value => `Page: ${value}`,
+ labels: {
+ aria: 'Aria',
+ prevAria: 'Previous Aria',
+ prev: 'Previous',
+ nextAria: 'Next Aria',
+ next: 'Next',
+ },
},
};
@@ -56,4 +64,4 @@ export const TwoEllipses: Story = {
},
};
-export default { component: Pagination } as Meta;
+export default { component: BasePagination } as Meta;
diff --git a/apps/site/components/Common/Pagination/index.tsx b/packages/ui-components/Common/BasePagination/index.tsx
similarity index 56%
rename from apps/site/components/Common/Pagination/index.tsx
rename to packages/ui-components/Common/BasePagination/index.tsx
index 969cf4dc86fc3..4cabe6e6e2683 100644
--- a/apps/site/components/Common/Pagination/index.tsx
+++ b/packages/ui-components/Common/BasePagination/index.tsx
@@ -1,66 +1,77 @@
import { ArrowRightIcon, ArrowLeftIcon } from '@heroicons/react/20/solid';
-import { useTranslations } from 'next-intl';
import type { FC } from 'react';
-import Button from '@/components/Common/Button';
-import { useGetPageElements } from '@/components/Common/Pagination/useGetPageElements';
+import Button from '@node-core/ui-components/Common/BaseButton';
+import { useGetPageElements } from '@node-core/ui-components/Common/BasePagination/useGetPageElements';
+import type { LinkLike } from '@node-core/ui-components/types';
import styles from './index.module.css';
type Page = { url: string };
-type PaginationProps = {
+export type PaginationProps = {
// One-based number of the current page
currentPage: number;
pages: Array;
// The number of page buttons on each side of the current page button
// @default 1
currentPageSiblingsCount?: number;
+ as?: LinkLike;
+ getPageLabel: (pageNumber: number) => string;
+ labels: {
+ aria: string;
+ prevAria: string;
+ prev: string;
+ nextAria: string;
+ next: string;
+ };
};
-const Pagination: FC = ({
+const BasePagination: FC = ({
currentPage,
pages,
+ as = 'a',
currentPageSiblingsCount = 1,
+ labels,
+ getPageLabel,
}) => {
- const t = useTranslations();
-
const parsedPages = useGetPageElements(
currentPage,
pages,
- currentPageSiblingsCount
+ currentPageSiblingsCount,
+ as,
+ getPageLabel
);
return (
-