diff --git a/apps/site/components/Downloads/DownloadReleasesTable.tsx b/apps/site/components/Downloads/DownloadReleasesTable.tsx deleted file mode 100644 index 0c0acff74e59c..0000000000000 --- a/apps/site/components/Downloads/DownloadReleasesTable.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { getTranslations } from 'next-intl/server'; -import type { FC } from 'react'; - -import LinkWithArrow from '@/components/LinkWithArrow'; -import getReleaseData from '@/next-data/releaseData'; -import { BASE_CHANGELOG_URL } from '@/next.constants.mjs'; -import { getNodeApiLink } from '@/util/getNodeApiLink'; - -// This is a React Async Server Component -// Note that Hooks cannot be used in a RSC async component -// Async Components do not get re-rendered at all. -const DownloadReleasesTable: FC = async () => { - const releaseData = await getReleaseData(); - - const t = await getTranslations(); - - return ( - - - - - - - - - - - - - {releaseData.map(release => ( - - - - - - - - - ))} - -
{t('components.downloadReleasesTable.version')}{t('components.downloadReleasesTable.nApiVersion')}{t('components.downloadReleasesTable.codename')}{t('components.downloadReleasesTable.releaseDate')}{t('components.downloadReleasesTable.npmVersion')}
v{release.version}v{release.modules}{release.codename || '-'} - - v{release.npm} - - {t('components.downloadReleasesTable.actions.releases')} - - - - {t('components.downloadReleasesTable.actions.changelog')} - - - - {t('components.downloadReleasesTable.actions.docs')} - -
- ); -}; - -export default DownloadReleasesTable; diff --git a/apps/site/components/Downloads/DownloadReleasesTable/DetailsButton.tsx b/apps/site/components/Downloads/DownloadReleasesTable/DetailsButton.tsx new file mode 100644 index 0000000000000..3c20ba738e4d4 --- /dev/null +++ b/apps/site/components/Downloads/DownloadReleasesTable/DetailsButton.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; +import { use } from 'react'; + +import LinkWithArrow from '@/components/LinkWithArrow'; +import { ReleaseModalContext } from '@/providers/releaseModalProvider'; +import type { NodeRelease } from '@/types'; + +type DetailsButtonProps = { + versionData: NodeRelease; +}; + +const DetailsButton: FC = ({ versionData }) => { + const t = useTranslations('components.downloadReleasesTable'); + + const { openModal } = use(ReleaseModalContext); + + return ( + openModal(versionData)} + > + {t('details')} + + ); +}; + +export default DetailsButton; diff --git a/apps/site/components/Downloads/DownloadReleasesTable/index.tsx b/apps/site/components/Downloads/DownloadReleasesTable/index.tsx new file mode 100644 index 0000000000000..460e4e29eab2e --- /dev/null +++ b/apps/site/components/Downloads/DownloadReleasesTable/index.tsx @@ -0,0 +1,55 @@ +import Badge from '@node-core/ui-components/Common/Badge'; +import { getTranslations } from 'next-intl/server'; +import type { FC } from 'react'; + +import FormattedTime from '@/components/Common/FormattedTime'; +import DetailsButton from '@/components/Downloads/DownloadReleasesTable/DetailsButton'; +import getReleaseData from '@/next-data/releaseData'; + +const DownloadReleasesTable: FC = async () => { + const releaseData = await getReleaseData(); + + const t = await getTranslations(); + + return ( + + + + + + + + + + + + + {releaseData.map(release => ( + + + + + + + + + ))} + +
{t('components.downloadReleasesTable.version')}{t('components.downloadReleasesTable.codename')}{t('components.downloadReleasesTable.firstReleased')}{t('components.downloadReleasesTable.lastUpdated')}{t('components.downloadReleasesTable.status')}
v{release.major}{release.codename || '-'} + + + + + + {release.status} + + + +
+ ); +}; + +export default DownloadReleasesTable; diff --git a/apps/site/components/Downloads/MinorReleasesTable/index.module.css b/apps/site/components/Downloads/MinorReleasesTable/index.module.css new file mode 100644 index 0000000000000..5740edadd7de7 --- /dev/null +++ b/apps/site/components/Downloads/MinorReleasesTable/index.module.css @@ -0,0 +1,8 @@ +@reference "../../../styles/index.css"; + +.links { + @apply flex + h-4 + items-center + gap-2; +} diff --git a/apps/site/components/Downloads/MinorReleasesTable/index.tsx b/apps/site/components/Downloads/MinorReleasesTable/index.tsx new file mode 100644 index 0000000000000..0fde534416cbf --- /dev/null +++ b/apps/site/components/Downloads/MinorReleasesTable/index.tsx @@ -0,0 +1,64 @@ +'use client'; + +import Separator from '@node-core/ui-components/Common/Separator'; +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; + +import Link from '@/components/Link'; +import { BASE_CHANGELOG_URL } from '@/next.constants.mjs'; +import type { MinorVersion } from '@/types'; +import { getNodeApiLink } from '@/util/getNodeApiLink'; + +import styles from './index.module.css'; + +type MinorReleasesTableProps = { + releases: Array; +}; + +export const MinorReleasesTable: FC = ({ + releases, +}) => { + const t = useTranslations('components.minorReleasesTable'); + + return ( + + + + + + + + + {releases.map(release => ( + + + + + ))} + +
{t('version')}{t('links')}
v{release.version} +
+ + {t('actions.release')} + + + + {t('actions.changelog')} + + + + {t('actions.docs')} + +
+
+ ); +}; diff --git a/apps/site/components/Downloads/ReleaseModal/index.tsx b/apps/site/components/Downloads/ReleaseModal/index.tsx new file mode 100644 index 0000000000000..23b50e785739d --- /dev/null +++ b/apps/site/components/Downloads/ReleaseModal/index.tsx @@ -0,0 +1,62 @@ +import AlertBox from '@node-core/ui-components/Common/AlertBox'; +import Modal from '@node-core/ui-components/Common/Modal'; +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; + +import { MinorReleasesTable } from '@/components/Downloads/MinorReleasesTable'; +import { ReleaseOverview } from '@/components/Downloads/ReleaseOverview'; +import LinkWithArrow from '@/components/LinkWithArrow'; +import type { NodeRelease } from '@/types'; + +type ReleaseModalProps = { + isOpen: boolean; + closeModal: () => void; + release: NodeRelease; +}; + +const ReleaseModal: FC = ({ + isOpen, + closeModal, + release, +}) => { + const t = useTranslations(); + + const modalHeadingKey = release.codename + ? 'components.releaseModal.title' + : 'components.releaseModal.titleWithoutCodename'; + + const modalHeading = t(modalHeadingKey, { + version: release.major, + codename: release.codename ?? '', + }); + + return ( + + {release.status === 'End-of-life' && ( + + {t('components.releaseModal.unsupportedVersionWarning')} + + )} + + {release.releaseAnnounceLink && ( + + {t('components.releaseModal.releaseAnnouncement')} + + )} + +
{t('components.releaseModal.overview')}
+ + + +
{t('components.releaseModal.minorVersions')}
+ + +
+ ); +}; + +export default ReleaseModal; diff --git a/apps/site/components/Downloads/ReleaseOverview/index.module.css b/apps/site/components/Downloads/ReleaseOverview/index.module.css new file mode 100644 index 0000000000000..5043967ac27f1 --- /dev/null +++ b/apps/site/components/Downloads/ReleaseOverview/index.module.css @@ -0,0 +1,38 @@ +@reference "../../../styles/index.css"; + +.root { + @apply rounded + border + border-neutral-200 + p-4 + text-neutral-900 + dark:border-neutral-800 + dark:text-white; + + .container { + @apply grid + grid-cols-2 + gap-4 + lg:grid-cols-3; + } + + .item { + @apply flex + items-center + gap-2; + + h1 { + @apply text-sm + font-semibold; + } + + h2 { + @apply text-xs + font-normal; + } + + svg { + @apply size-4; + } + } +} diff --git a/apps/site/components/Downloads/ReleaseOverview/index.tsx b/apps/site/components/Downloads/ReleaseOverview/index.tsx new file mode 100644 index 0000000000000..c444908c99586 --- /dev/null +++ b/apps/site/components/Downloads/ReleaseOverview/index.tsx @@ -0,0 +1,81 @@ +import { + CalendarIcon, + ClockIcon, + CodeBracketSquareIcon, + Square3Stack3DIcon, +} from '@heroicons/react/24/outline'; +import NpmIcon from '@node-core/ui-components/Icons/PackageManager/Npm'; +import { useTranslations } from 'next-intl'; +import type { FC, ReactNode, SVGProps } from 'react'; + +import FormattedTime from '@/components/Common/FormattedTime'; +import type { NodeRelease } from '@/types'; + +import styles from './index.module.css'; + +type ItemProps = { + Icon: FC>; + title: ReactNode; + subtitle: ReactNode; +}; + +const Item: FC = ({ Icon, title, subtitle }) => { + return ( +
+ +
+

{subtitle}

+

{title}

+
+
+ ); +}; + +type ReleaseOverviewProps = { + release: NodeRelease; +}; + +export const ReleaseOverview: FC = ({ release }) => { + const t = useTranslations(); + + return ( +
+
+ } + subtitle={t('components.releaseOverview.firstReleased')} + /> + } + subtitle={t('components.releaseOverview.lastUpdated')} + /> + + {release.modules && ( + + )} + {release.npm && ( + + )} + +
+
+ ); +}; diff --git a/apps/site/components/withBadge.tsx b/apps/site/components/withBadgeGroup.tsx similarity index 69% rename from apps/site/components/withBadge.tsx rename to apps/site/components/withBadgeGroup.tsx index 7b91c84c373c5..4047f3af8ec1c 100644 --- a/apps/site/components/withBadge.tsx +++ b/apps/site/components/withBadgeGroup.tsx @@ -1,27 +1,27 @@ -import Badge from '@node-core/ui-components/Common/Badge'; +import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; import type { FC } from 'react'; import Link from '@/components/Link'; import { siteConfig } from '@/next.json.mjs'; import { dateIsBetween } from '@/util/dateUtils'; -const WithBadge: FC<{ section: string }> = ({ section }) => { +const WithBadgeGroup: FC<{ section: string }> = ({ section }) => { const badge = siteConfig.websiteBadges[section]; if (badge && dateIsBetween(badge.startDate, badge.endDate)) { return ( - {badge.text} - + ); } return null; }; -export default WithBadge; +export default WithBadgeGroup; diff --git a/apps/site/layouts/About.tsx b/apps/site/layouts/About.tsx index 5599ecbccfda3..7deab417f4fae 100644 --- a/apps/site/layouts/About.tsx +++ b/apps/site/layouts/About.tsx @@ -6,9 +6,10 @@ import WithMetaBar from '@/components/withMetaBar'; import WithNavBar from '@/components/withNavBar'; import WithSidebar from '@/components/withSidebar'; import ArticleLayout from '@/layouts/Article'; +import { ReleaseModalProvider } from '@/providers/releaseModalProvider'; const AboutLayout: FC = ({ children }) => ( - <> + @@ -24,7 +25,7 @@ const AboutLayout: FC = ({ children }) => ( - + ); export default AboutLayout; diff --git a/apps/site/next-data/generators/releaseData.mjs b/apps/site/next-data/generators/releaseData.mjs index d04ced8340e8a..3db21e4166cf1 100644 --- a/apps/site/next-data/generators/releaseData.mjs +++ b/apps/site/next-data/generators/releaseData.mjs @@ -1,15 +1,20 @@ 'use strict'; import nodevu from '@nodevu/core'; +import { glob } from 'glob'; // Gets the appropriate release status for each major release const getNodeReleaseStatus = (now, support) => { - const { endOfLife, ltsStart, currentStart } = support; + const { endOfLife, maintenanceStart, ltsStart, currentStart } = support; if (endOfLife && now >= new Date(endOfLife)) { return 'End-of-life'; } + if (maintenanceStart && now >= new Date(maintenanceStart)) { + return 'Maintenance'; + } + if (ltsStart && now >= new Date(ltsStart)) { return 'LTS'; } @@ -27,47 +32,80 @@ const getNodeReleaseStatus = (now, support) => { * * @returns {Promise>} */ -const generateReleaseData = () => { - return nodevu({ fetch: fetch }).then(nodevuOutput => { - // Filter out those without documented support - // Basically those not in schedule.json - const majors = Object.values(nodevuOutput).filter(major => !!major.support); - - const nodeReleases = majors.map(major => { - const [latestVersion] = Object.values(major.releases); - - const support = { - currentStart: major.support.phases.dates.start, - ltsStart: major.support.phases.dates.lts, - maintenanceStart: major.support.phases.dates.maintenance, - endOfLife: major.support.phases.dates.end, - }; - - // Get the major release status based on our Release Schedule - const status = getNodeReleaseStatus(new Date(), support); - - return { - ...support, - status, - major: latestVersion.semver.major, - version: latestVersion.semver.raw, - versionWithPrefix: `v${latestVersion.semver.raw}`, - codename: major.support.codename || '', - isLts: status === 'LTS', - npm: latestVersion.dependencies.npm || '', - v8: latestVersion.dependencies.v8 || '', - releaseDate: latestVersion.releaseDate || '', - modules: latestVersion.modules.version || '', - }; - }); - - // nodevu returns duplicated v0.x versions (v0.12, v0.10, ...). - // This behavior seems intentional as the case is hardcoded in nodevu, - // see https://github.com/cutenode/nodevu/blob/0c8538c70195fb7181e0a4d1eeb6a28e8ed95698/core/index.js#L24. - // This line ignores those duplicated versions and takes the latest - // v0.x version (v0.12.18). It is also consistent with the legacy - // nodejs.org implementation. - return nodeReleases.filter(r => r.major !== 0 || r.version === '0.12.18'); +const generateReleaseData = async () => { + const releaseAnnouncements = await glob('**/*-release-announce.md', { + root: process.cwd(), + cwd: 'pages/en/blog/announcements/', + absolute: false, + }); + + console.log('Release Announcement:', process.cwd(), releaseAnnouncements); + + const nodevuOutput = await nodevu({ fetch: fetch }); + + const majors = Object.entries(nodevuOutput).filter( + ([version, { support }]) => { + // Filter out those without documented support + // Basically those not in schedule.json + if (!support) { + return false; + } + + // nodevu returns duplicated v0.x versions (v0.12, v0.10, ...). + // This behavior seems intentional as the case is hardcoded in nodevu, + // see https://github.com/cutenode/nodevu/blob/0c8538c70195fb7181e0a4d1eeb6a28e8ed95698/core/index.js#L24. + // This line ignores those duplicated versions and takes the latest + // v0.x version (v0.12.18). It is also consistent with the legacy + // nodejs.org implementation. + if (version.startsWith('v0.') && version !== 'v0.12') { + return false; + } + + return true; + } + ); + + return majors.map(([, major]) => { + const [latestVersion] = Object.values(major.releases); + + const support = { + currentStart: major.support.phases.dates.start, + ltsStart: major.support.phases.dates.lts, + maintenanceStart: major.support.phases.dates.maintenance, + endOfLife: major.support.phases.dates.end, + }; + + // Get the major release status based on our Release Schedule + const status = getNodeReleaseStatus(new Date(), support); + + const minorVersions = Object.entries(major.releases).map(([, release]) => ({ + version: release.semver.raw, + releaseDate: release.releaseDate, + })); + + const majorVersion = latestVersion.semver.major; + + const releaseAnnounceLink = releaseAnnouncements.includes( + `v${majorVersion}-release-announce.md` + ) + ? `/blog/announcements/v${majorVersion}-release-announce` + : undefined; + + return { + ...support, + status, + major: latestVersion.semver.major, + version: latestVersion.semver.raw, + versionWithPrefix: `v${latestVersion.semver.raw}`, + codename: major.support.codename || '', + isLts: status === 'LTS', + npm: latestVersion.dependencies.npm || '', + v8: latestVersion.dependencies.v8, + releaseDate: latestVersion.releaseDate, + modules: latestVersion.modules.version || '', + releaseAnnounceLink, + minorVersions, + }; }); }; diff --git a/apps/site/next.mdx.use.mjs b/apps/site/next.mdx.use.mjs index 2f44a0bab6dce..537872c020e64 100644 --- a/apps/site/next.mdx.use.mjs +++ b/apps/site/next.mdx.use.mjs @@ -2,7 +2,7 @@ import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable'; import UpcomingMeetings from './components/MDX/Calendar/UpcomingMeetings'; -import WithBadge from './components/withBadge'; +import WithBadgeGroup from './components/withBadgeGroup'; import WithBanner from './components/withBanner'; import WithNodeRelease from './components/withNodeRelease'; @@ -18,7 +18,7 @@ export const mdxComponents = { // HOC for providing Banner Data WithBanner: WithBanner, // HOC for providing Badge Data - WithBadge: WithBadge, + WithBadgeGroup: WithBadgeGroup, // Renders an container for Upcoming Node.js Meetings UpcomingMeetings: UpcomingMeetings, }; diff --git a/apps/site/pages/en/index.mdx b/apps/site/pages/en/index.mdx index 1558dac0319aa..7b9f1403e3c7b 100644 --- a/apps/site/pages/en/index.mdx +++ b/apps/site/pages/en/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

Run JavaScript Everywhere

diff --git a/apps/site/pages/es/index.mdx b/apps/site/pages/es/index.mdx index 72c9f55539fa3..7597663b63680 100644 --- a/apps/site/pages/es/index.mdx +++ b/apps/site/pages/es/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

Ejecuta JavaScript en cualquier parte

diff --git a/apps/site/pages/fa/index.mdx b/apps/site/pages/fa/index.mdx index 10b0356201947..ab1dc8d23fca3 100644 --- a/apps/site/pages/fa/index.mdx +++ b/apps/site/pages/fa/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

اجرا جاوااسکریپت در همه جا

diff --git a/apps/site/pages/fr/index.mdx b/apps/site/pages/fr/index.mdx index 64f3ec059b2da..2cd8a9da0ec2c 100644 --- a/apps/site/pages/fr/index.mdx +++ b/apps/site/pages/fr/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

Exécuter du JavaScript partout

diff --git a/apps/site/pages/id/index.mdx b/apps/site/pages/id/index.mdx index ae92dab406685..f321063c28a4b 100644 --- a/apps/site/pages/id/index.mdx +++ b/apps/site/pages/id/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

Jalankan JavaScript Di Mana Saja

diff --git a/apps/site/pages/ja/index.mdx b/apps/site/pages/ja/index.mdx index dc21ad41a0bb4..f99616a253d6c 100644 --- a/apps/site/pages/ja/index.mdx +++ b/apps/site/pages/ja/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

どこでもJavaScriptを使おう

diff --git a/apps/site/pages/ko/index.mdx b/apps/site/pages/ko/index.mdx index 25f31ed624ab0..f392ffcbcb1bb 100644 --- a/apps/site/pages/ko/index.mdx +++ b/apps/site/pages/ko/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

어디서든 JavaScript를 실행하세요

diff --git a/apps/site/pages/pt/index.mdx b/apps/site/pages/pt/index.mdx index 57261fea5aba2..af90aa6b5727e 100644 --- a/apps/site/pages/pt/index.mdx +++ b/apps/site/pages/pt/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

Executar a JavaScript em Toda Parte

diff --git a/apps/site/pages/tr/index.mdx b/apps/site/pages/tr/index.mdx index 742f803ab7bbc..19d3aa83e7ede 100644 --- a/apps/site/pages/tr/index.mdx +++ b/apps/site/pages/tr/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

Her Yerde JavaScript Çalıştırın

diff --git a/apps/site/pages/uk/index.mdx b/apps/site/pages/uk/index.mdx index 07671833ad3e8..937fe4a2709e1 100644 --- a/apps/site/pages/uk/index.mdx +++ b/apps/site/pages/uk/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

Запускайте JavaScript будь-де

diff --git a/apps/site/pages/zh-cn/index.mdx b/apps/site/pages/zh-cn/index.mdx index 9809abe73cdb7..120fe32b48227 100644 --- a/apps/site/pages/zh-cn/index.mdx +++ b/apps/site/pages/zh-cn/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

在任何地方运行 JavaScript

diff --git a/apps/site/pages/zh-tw/index.mdx b/apps/site/pages/zh-tw/index.mdx index b329dd7ed0112..37766774fd1e3 100644 --- a/apps/site/pages/zh-tw/index.mdx +++ b/apps/site/pages/zh-tw/index.mdx @@ -4,7 +4,7 @@ layout: home ---
- +

隨時隨地執行 JavaScript

diff --git a/apps/site/providers/releaseModalProvider.tsx b/apps/site/providers/releaseModalProvider.tsx new file mode 100644 index 0000000000000..acbcb17cc8e5c --- /dev/null +++ b/apps/site/providers/releaseModalProvider.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { createContext, useState } from 'react'; +import type { FC, PropsWithChildren } from 'react'; + +import ReleaseModal from '@/components/Downloads/ReleaseModal'; +import type { NodeRelease } from '@/types'; + +type ReleaseModalContextType = { + activeRelease: NodeRelease | null; + openModal: (activeRelease: NodeRelease) => void; + closeModal: () => void; +}; + +export const ReleaseModalContext = createContext({ + activeRelease: null, + openModal: () => {}, + closeModal: () => {}, +}); + +export const ReleaseModalProvider: FC = ({ children }) => { + const [activeRelease, setValue] = useState(null); + + const openModal = (activeRelease: NodeRelease) => { + setValue(activeRelease); + }; + + const closeModal = () => { + setValue(null); + }; + + return ( + + {children} + + {activeRelease && ( + + )} + + ); +}; diff --git a/apps/site/types/releases.ts b/apps/site/types/releases.ts index 2fc4a4f101d54..cf71f7bea406d 100644 --- a/apps/site/types/releases.ts +++ b/apps/site/types/releases.ts @@ -1,4 +1,9 @@ -export type NodeReleaseStatus = 'LTS' | 'Current' | 'End-of-life' | 'Pending'; +export type NodeReleaseStatus = + | 'LTS' + | 'Maintenance' + | 'Current' + | 'End-of-life' + | 'Pending'; export interface NodeReleaseSource { major: number; @@ -9,13 +14,20 @@ export interface NodeReleaseSource { maintenanceStart?: string; endOfLife: string; npm?: string; - v8?: string; - releaseDate?: string; + v8: string; + releaseDate: string; modules?: string; } +export interface MinorVersion { + version: string; + releaseDate: string; +} + export interface NodeRelease extends NodeReleaseSource { versionWithPrefix: string; isLts: boolean; status: NodeReleaseStatus; + releaseAnnounceLink?: string; + minorVersions: Array; } diff --git a/package-lock.json b/package-lock.json index f91b24ed5ea71..bf274d0bd3a97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4365,6 +4365,85 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.3.tgz", + "integrity": "sha512-2omrWKJvxR0U/tkIXezcc1nFMwtLU0+b/rDK40gnzJqTLWQ/TD/D5IYVefp9sC3QWfeQbpSbEA6op9MQKyaALQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz", + "integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", @@ -21241,6 +21320,7 @@ "@radix-ui/react-label": "~2.1.2", "@radix-ui/react-scroll-area": "~1.2.3", "@radix-ui/react-select": "~2.1.6", + "@radix-ui/react-separator": "~1.1.3", "@radix-ui/react-tabs": "~1.1.3", "@radix-ui/react-toast": "~1.2.6", "@radix-ui/react-tooltip": "~1.1.8", diff --git a/packages/i18n/locales/en.json b/packages/i18n/locales/en.json index ab38e13762342..7a0f6b1620cc7 100644 --- a/packages/i18n/locales/en.json +++ b/packages/i18n/locales/en.json @@ -148,15 +148,38 @@ "downloadReleasesTable": { "version": "Node.js", "nApiVersion": "N-API", - "npmVersion": "npm", "codename": "Codename", "releaseDate": "Released at", + "firstReleased": "First released", + "lastUpdated": "Last updated", + "status": "Status", + "details": "Details" + }, + "releaseModal": { + "title": "Node.js {version} ({codename})", + "titleWithoutCodename": "Node.js {version}", + "overview": "Overview", + "minorVersions": "Minor versions", + "releaseAnnouncement": "Release Announcement", + "unsupportedVersionWarning": "This version is out of maintenance. Please use a currently supported version." + }, + "minorReleasesTable": { + "version": "Version", + "links": "Links", "actions": { + "release": "Release", "changelog": "Changelog", - "releases": "Releases", "docs": "Docs" } }, + "releaseOverview": { + "firstReleased": "First released", + "lastUpdated": "Last updated", + "minorVersions": "Minor versions", + "nApiVersion": "N-API version", + "npmVersion": "npm version", + "v8Version": "V8 version" + }, "pagination": { "next": "Next", "previous": "Previous" diff --git a/packages/ui-components/Common/Badge/index.module.css b/packages/ui-components/Common/Badge/index.module.css index 9420740a87cf1..19bdba865743c 100644 --- a/packages/ui-components/Common/Badge/index.module.css +++ b/packages/ui-components/Common/Badge/index.module.css @@ -1,101 +1,38 @@ @reference "../../styles/index.css"; -.wrapper { - @apply flex - w-fit - items-center - rounded-full +.badge { + @apply rounded-full border - py-1 - pl-1 - pr-2.5 - text-sm - font-medium; + text-center + text-white; - .icon { - @apply size-4; - } - - .badge { - @apply mr-2 - rounded-full - border - px-2.5 + &.small { + @apply px-1.5 py-0.5 - text-center - text-white; + text-xs; } - .message { - @apply mr-1; + &.medium { + @apply px-2.5 + py-0.5 + text-base; } &.default { @apply border-green-200 - bg-green-100 - dark:border-green-700 - dark:bg-neutral-900; - - .icon { - @apply text-green-500 - dark:text-green-300; - } - - .badge { - @apply border-green-200 - bg-green-600 - dark:border-green-600; - } - - .message { - @apply text-green-700 - dark:text-green-300; - } + bg-green-600 + dark:border-green-800; } &.error { @apply border-danger-200 - bg-danger-100 - dark:border-danger-700 - dark:bg-neutral-900; - - .icon { - @apply text-danger-500 - dark:text-danger-300; - } - - .badge { - @apply border-danger-200 - bg-danger-600 - dark:border-danger-600; - } - - .message { - @apply text-danger-700 - dark:text-danger-300; - } + bg-danger-600 + dark:border-danger-800; } &.warning { @apply border-warning-200 - bg-warning-100 - dark:border-warning-700 - dark:bg-neutral-900; - - .icon { - @apply text-warning-500 - dark:text-warning-300; - } - - .badge { - @apply border-warning-200 - bg-warning-600 - dark:border-warning-600; - } - - .message { - @apply text-warning-700 - dark:text-warning-300; - } + bg-warning-600 + dark:border-warning-600; } } diff --git a/packages/ui-components/Common/Badge/index.stories.tsx b/packages/ui-components/Common/Badge/index.stories.tsx index b37dfa5b0a621..874ec97c26fd9 100644 --- a/packages/ui-components/Common/Badge/index.stories.tsx +++ b/packages/ui-components/Common/Badge/index.stories.tsx @@ -7,29 +7,32 @@ type Meta = MetaObj; export const Default: Story = { args: { - href: '/', - children: 'OpenJS Foundation Certification 2023', kind: 'default', - badgeText: 'New', }, }; export const Error: Story = { args: { - href: '/', - children: 'OpenJS Foundation Certification 2023', kind: 'error', - badgeText: 'New', }, }; export const Warning: Story = { args: { - href: '/', - children: 'OpenJS Foundation Certification 2023', kind: 'warning', - badgeText: 'New', }, }; -export default { component: Badge } as Meta; +export const Small: Story = { + args: { + size: 'small', + }, +}; + +export const Medium: Story = { + args: { + size: 'medium', + }, +}; + +export default { component: Badge, args: { children: 'Badge' } } as Meta; diff --git a/packages/ui-components/Common/Badge/index.tsx b/packages/ui-components/Common/Badge/index.tsx index 49590ebefe08e..5b9730368e35d 100644 --- a/packages/ui-components/Common/Badge/index.tsx +++ b/packages/ui-components/Common/Badge/index.tsx @@ -1,28 +1,35 @@ -import ArrowRightIcon from '@heroicons/react/24/solid/ArrowRightIcon'; -import type { ComponentProps, FC, PropsWithChildren } from 'react'; - -import type { LinkLike } from '@node-core/ui-components/types'; +import classNames from 'classnames'; +import type { FC, HTMLAttributes, PropsWithChildren } from 'react'; import styles from './index.module.css'; -type BadgeProps = { - kind?: 'default' | 'warning' | 'error'; - badgeText?: string; - as: LinkLike; -} & ComponentProps; +type BadgeKind = 'default' | 'warning' | 'error'; + +type BadgeProps = HTMLAttributes & { + size?: 'small' | 'medium'; + kind?: BadgeKind; +}; const Badge: FC> = ({ kind = 'default', - badgeText, + size = 'medium', + className, children, - as: Component = 'a', - ...args -}) => ( - - {badgeText && {badgeText}} - {children} - {args.href && } - -); + ...props +}) => { + return ( + + {children} + + ); +}; export default Badge; diff --git a/packages/ui-components/Common/BadgeGroup/index.module.css b/packages/ui-components/Common/BadgeGroup/index.module.css new file mode 100644 index 0000000000000..754356aa6012c --- /dev/null +++ b/packages/ui-components/Common/BadgeGroup/index.module.css @@ -0,0 +1,77 @@ +@reference "../../styles/index.css"; + +.wrapper { + @apply flex + w-fit + items-center + rounded-full + border + py-1 + pl-1 + pr-2.5 + text-sm + font-medium; + + .icon { + @apply size-4; + } + + .badge { + @apply mr-2; + } + + .message { + @apply mr-1; + } + + &.default { + @apply border-green-200 + bg-green-100 + dark:border-green-700 + dark:bg-neutral-900; + + .icon { + @apply text-green-500 + dark:text-green-300; + } + + .message { + @apply text-green-700 + dark:text-green-300; + } + } + + &.error { + @apply border-danger-200 + bg-danger-100 + dark:border-danger-700 + dark:bg-neutral-900; + + .icon { + @apply text-danger-500 + dark:text-danger-300; + } + + .message { + @apply text-danger-700 + dark:text-danger-300; + } + } + + &.warning { + @apply border-warning-200 + bg-warning-100 + dark:border-warning-700 + dark:bg-neutral-900; + + .icon { + @apply text-warning-500 + dark:text-warning-300; + } + + .message { + @apply text-warning-700 + dark:text-warning-300; + } + } +} diff --git a/packages/ui-components/Common/BadgeGroup/index.stories.tsx b/packages/ui-components/Common/BadgeGroup/index.stories.tsx new file mode 100644 index 0000000000000..ac2fe623dfdb5 --- /dev/null +++ b/packages/ui-components/Common/BadgeGroup/index.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + href: '/', + children: 'OpenJS Foundation Certification 2023', + kind: 'default', + badgeText: 'New', + }, +}; + +export const Error: Story = { + args: { + href: '/', + children: 'OpenJS Foundation Certification 2023', + kind: 'error', + badgeText: 'New', + }, +}; + +export const Warning: Story = { + args: { + href: '/', + children: 'OpenJS Foundation Certification 2023', + kind: 'warning', + badgeText: 'New', + }, +}; + +export default { component: BadgeGroup } as Meta; diff --git a/packages/ui-components/Common/BadgeGroup/index.tsx b/packages/ui-components/Common/BadgeGroup/index.tsx new file mode 100644 index 0000000000000..761ca9e3aaec6 --- /dev/null +++ b/packages/ui-components/Common/BadgeGroup/index.tsx @@ -0,0 +1,35 @@ +import ArrowRightIcon from '@heroicons/react/24/solid/ArrowRightIcon'; +import type { ComponentProps, FC, PropsWithChildren } from 'react'; + +import Badge from '@node-core/ui-components/Common/Badge'; +import type { LinkLike } from '@node-core/ui-components/types'; + +import styles from './index.module.css'; + +type BadgeGroupKind = 'default' | 'warning' | 'error'; + +type BadgeGroupProps = { + kind?: BadgeGroupKind; + badgeText?: string; + as: LinkLike; +} & ComponentProps; + +const BadgeGroup: FC> = ({ + kind = 'default', + badgeText, + children, + as: Component = 'a', + ...args +}) => ( + + {badgeText && ( + + {badgeText} + + )} + {children} + {args.href && } + +); + +export default BadgeGroup; diff --git a/packages/ui-components/Common/Modal/index.module.css b/packages/ui-components/Common/Modal/index.module.css index 1495d7e71bf46..5b399767fdf7e 100644 --- a/packages/ui-components/Common/Modal/index.module.css +++ b/packages/ui-components/Common/Modal/index.module.css @@ -15,6 +15,7 @@ m-4 inline-flex w-full + max-w-3xl flex-col overflow-y-auto rounded @@ -24,7 +25,6 @@ p-8 focus:outline-none sm:my-20 - lg:max-w-[900px] xl:p-12 dark:bg-neutral-950; } diff --git a/packages/ui-components/Common/Modal/index.tsx b/packages/ui-components/Common/Modal/index.tsx index 635ab82a54cfa..787a80e0cb73a 100644 --- a/packages/ui-components/Common/Modal/index.tsx +++ b/packages/ui-components/Common/Modal/index.tsx @@ -8,7 +8,7 @@ import styles from './index.module.css'; type ModalProps = PropsWithChildren<{ heading: string; - subheading: string; + subheading?: string; open?: boolean; onOpenChange?: (open: boolean) => void; }>; @@ -30,9 +30,11 @@ const Modal: FC = ({ {heading} - - {subheading} - + {subheading && ( + + {subheading} + + )}
{children}
diff --git a/packages/ui-components/Common/Separator/index.module.css b/packages/ui-components/Common/Separator/index.module.css new file mode 100644 index 0000000000000..61d7dc140faa0 --- /dev/null +++ b/packages/ui-components/Common/Separator/index.module.css @@ -0,0 +1,16 @@ +@reference "../../styles/index.css"; + +.root { + @apply shrink-0 + bg-neutral-800; + + &.horizontal { + @apply h-px + w-full; + } + + &.vertical { + @apply h-full + w-px; + } +} diff --git a/packages/ui-components/Common/Separator/index.stories.tsx b/packages/ui-components/Common/Separator/index.stories.tsx new file mode 100644 index 0000000000000..4fe7337c8b58c --- /dev/null +++ b/packages/ui-components/Common/Separator/index.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +import Separator from '@node-core/ui-components/Common/Separator'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Horizontal: Story = { + args: { + orientation: 'horizontal', + }, +}; + +export const Vertical: Story = { + args: { + orientation: 'vertical', + }, +}; + +export default { + component: Separator, + parameters: { + layout: 'centered', + }, + decorators: [ + Story => ( +
+ +
+ ), + ], +} as Meta; diff --git a/packages/ui-components/Common/Separator/index.tsx b/packages/ui-components/Common/Separator/index.tsx new file mode 100644 index 0000000000000..2a51b8bd41adc --- /dev/null +++ b/packages/ui-components/Common/Separator/index.tsx @@ -0,0 +1,27 @@ +'use client'; + +import * as SeparatorPrimitive from '@radix-ui/react-separator'; +import classNames from 'classnames'; +import type { FC, ComponentProps } from 'react'; + +import styles from './index.module.css'; + +const Separator: FC> = ({ + className, + orientation = 'horizontal', + decorative = true, + ...props +}) => ( + +); + +export default Separator; diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json index c8631c59943e3..9610a025a7e9a 100644 --- a/packages/ui-components/package.json +++ b/packages/ui-components/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-label": "~2.1.2", "@radix-ui/react-scroll-area": "~1.2.3", "@radix-ui/react-select": "~2.1.6", + "@radix-ui/react-separator": "~1.1.3", "@radix-ui/react-tabs": "~1.1.3", "@radix-ui/react-toast": "~1.2.6", "@radix-ui/react-tooltip": "~1.1.8",