diff --git a/apps/site/components/EOL/EOLReleaseTable/TableBody.tsx b/apps/site/components/EOL/EOLReleaseTable/TableBody.tsx deleted file mode 100644 index ee2fa061f37c7..0000000000000 --- a/apps/site/components/EOL/EOLReleaseTable/TableBody.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'use client'; - -import { useTranslations } from 'next-intl'; -import type { FC } from 'react'; -import { Fragment, useState } from 'react'; - -import FormattedTime from '#site/components/Common/FormattedTime'; -import LinkWithArrow from '#site/components/Common/LinkWithArrow'; -import EOLModal from '#site/components/EOL/EOLModal'; -import VulnerabilityChips from '#site/components/EOL/VulnerabilityChips'; -import type { NodeRelease } from '#site/types/releases.js'; -import type { GroupedVulnerabilities } from '#site/types/vulnerabilities.js'; - -type EOLReleaseTableBodyProps = { - eolReleases: Array; - vulnerabilities: GroupedVulnerabilities; -}; - -const EOLReleaseTableBody: FC = ({ - eolReleases, - vulnerabilities, -}) => { - const t = useTranslations(); - - const [currentModal, setCurrentModal] = useState(); - - return ( - - {eolReleases.map(release => ( - - - - v{release.major} {release.codename ? `(${release.codename})` : ''} - - - - - - - - - - - - setCurrentModal(release.version)} - > - {t('components.downloadReleasesTable.details')} - - - - - open || setCurrentModal(undefined)} - /> - - ))} - - ); -}; - -export default EOLReleaseTableBody; diff --git a/apps/site/components/EOL/EOLReleaseTable/TableClient.tsx b/apps/site/components/EOL/EOLReleaseTable/TableClient.tsx new file mode 100644 index 0000000000000..fed81872c8d29 --- /dev/null +++ b/apps/site/components/EOL/EOLReleaseTable/TableClient.tsx @@ -0,0 +1,96 @@ +'use client'; + +import Switch from '@node-core/ui-components/Common/Switch'; +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; +import { Fragment, useState } from 'react'; + +import FormattedTime from '#site/components/Common/FormattedTime'; +import LinkWithArrow from '#site/components/Common/LinkWithArrow'; +import EOLModal from '#site/components/EOL/EOLModal'; +import VulnerabilityChips from '#site/components/EOL/VulnerabilityChips'; +import type { NodeRelease } from '#site/types/releases.js'; +import type { GroupedVulnerabilities } from '#site/types/vulnerabilities.js'; + +type EOLReleaseTableBodyProps = { + eolReleases: Array; + vulnerabilities: GroupedVulnerabilities; +}; + +const EOLReleaseTableClient: FC = ({ + eolReleases, + vulnerabilities, +}) => { + const t = useTranslations(); + + const [currentModal, setCurrentModal] = useState(); + const [hideNonLts, setHideNonLts] = useState(false); + + const filteredReleases = hideNonLts + ? eolReleases.filter(release => release.isLts) + : eolReleases; + + return ( + <> + + + + + + + + + + + + + {filteredReleases.map(release => ( + + + + + + + + + + + + open || setCurrentModal(undefined)} + /> + + ))} + +
+ {t('components.eolTable.version')} ( + {t('components.eolTable.codename')}) + {t('components.eolTable.lastUpdated')}{t('components.eolTable.vulnerabilities')}{t('components.eolTable.details')}
+ v{release.major}{' '} + {release.codename ? `(${release.codename})` : ''} + + + + + + setCurrentModal(release.version)} + > + {t('components.downloadReleasesTable.details')} + +
+ + ); +}; + +export default EOLReleaseTableClient; diff --git a/apps/site/components/EOL/EOLReleaseTable/index.tsx b/apps/site/components/EOL/EOLReleaseTable/index.tsx index d1e15f6dc37de..c3315fa0588ee 100644 --- a/apps/site/components/EOL/EOLReleaseTable/index.tsx +++ b/apps/site/components/EOL/EOLReleaseTable/index.tsx @@ -1,11 +1,10 @@ -import { getTranslations } from 'next-intl/server'; import type { FC } from 'react'; import provideReleaseData from '#site/next-data/providers/releaseData'; import provideVulnerabilities from '#site/next-data/providers/vulnerabilities'; import { EOL_VERSION_IDENTIFIER } from '#site/next.constants.mjs'; -import EOLReleaseTableBody from './TableBody'; +import EOLReleaseTableClient from './TableClient'; const EOLReleaseTable: FC = async () => { const releaseData = await provideReleaseData(); @@ -15,27 +14,11 @@ const EOLReleaseTable: FC = async () => { release => release.status === EOL_VERSION_IDENTIFIER ); - const t = await getTranslations(); - return ( - - - - - - - - - - - -
- {t('components.eolTable.version')} ( - {t('components.eolTable.codename')}) - {t('components.eolTable.lastUpdated')}{t('components.eolTable.vulnerabilities')}{t('components.eolTable.details')}
+ ); }; diff --git a/apps/site/next-data/generators/__tests__/releaseData.test.mjs b/apps/site/next-data/generators/__tests__/releaseData.test.mjs index 43de9287c87b1..0591eefd46c6f 100644 --- a/apps/site/next-data/generators/__tests__/releaseData.test.mjs +++ b/apps/site/next-data/generators/__tests__/releaseData.test.mjs @@ -43,7 +43,7 @@ describe('generateReleaseData', () => { version: '14.0.0', versionWithPrefix: 'v14.0.0', codename: '', - isLts: false, + isLts: true, npm: '6.14.10', v8: '8.0.276.20', releaseDate: '2021-04-20', diff --git a/apps/site/next-data/generators/releaseData.mjs b/apps/site/next-data/generators/releaseData.mjs index 9887cef858474..50b8399c9ba65 100644 --- a/apps/site/next-data/generators/releaseData.mjs +++ b/apps/site/next-data/generators/releaseData.mjs @@ -63,7 +63,7 @@ const generateReleaseData = async () => { version: latestVersion.semver.raw, versionWithPrefix: `v${latestVersion.semver.raw}`, codename: major.support.codename || '', - isLts: status.endsWith('LTS'), + isLts: support.ltsStart !== undefined, npm: latestVersion.dependencies.npm || '', v8: latestVersion.dependencies.v8, releaseDate: latestVersion.releaseDate, diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index 07c0769b5ce6d..4cbd7a7773c56 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -214,7 +214,8 @@ "releaseDate": "Released at", "lastUpdated": "Last updated", "vulnerabilities": "Vulnerabilities", - "details": "Details" + "details": "Details", + "hideNonLts": "Hide non-LTS versions" }, "minorReleasesTable": { "version": "Version", diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json index 0ea39b5f4abd0..5eee9b85c9111 100644 --- a/packages/ui-components/package.json +++ b/packages/ui-components/package.json @@ -42,6 +42,7 @@ "@radix-ui/react-label": "~2.1.7", "@radix-ui/react-select": "~2.2.6", "@radix-ui/react-separator": "~1.1.7", + "@radix-ui/react-switch": "~1.2.6", "@radix-ui/react-tabs": "~1.1.13", "@radix-ui/react-toast": "~1.2.15", "@radix-ui/react-tooltip": "~1.2.8", diff --git a/packages/ui-components/src/Common/Switch/index.module.css b/packages/ui-components/src/Common/Switch/index.module.css new file mode 100644 index 0000000000000..7d0a33166540c --- /dev/null +++ b/packages/ui-components/src/Common/Switch/index.module.css @@ -0,0 +1,59 @@ +@reference "../../styles/index.css"; + +.switch { + @apply inline-flex + justify-end + gap-3; + + .label { + @apply cursor-pointer + select-none + text-sm + font-medium + text-neutral-800 + dark:text-neutral-200; + } + + .root { + @apply w-10.5 + relative + inline-flex + h-6 + cursor-pointer + items-center + rounded-full + bg-black + focus:outline-none + focus-visible:ring-2 + focus-visible:ring-green-500 + focus-visible:ring-offset-2 + focus-visible:ring-offset-neutral-100 + motion-safe:transition-colors + motion-safe:duration-100 + motion-safe:ease-out + dark:bg-neutral-700 + dark:focus-visible:ring-green-400 + dark:focus-visible:ring-offset-neutral-900; + } + + .root[data-state='checked'] { + @apply bg-green-600; + } + + .thumb { + @apply pointer-events-none + block + size-5 + translate-x-0.5 + rounded-full + bg-white + ring-0 + motion-safe:transition-transform + motion-safe:duration-100 + motion-safe:ease-out; + } + + .thumb[data-state='checked'] { + @apply translate-x-5; + } +} diff --git a/packages/ui-components/src/Common/Switch/index.stories.tsx b/packages/ui-components/src/Common/Switch/index.stories.tsx new file mode 100644 index 0000000000000..cf32306b7c6bd --- /dev/null +++ b/packages/ui-components/src/Common/Switch/index.stories.tsx @@ -0,0 +1,33 @@ +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; +import { useState } from 'react'; + +import Switch from '#ui/Common/Switch'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Uncontrolled: Story = { + args: { + label: 'Enable Feature', + }, +}; + +export const Controlled: Story = { + args: { + label: 'Enable Feature', + }, + render: args => { + const [checked, setChecked] = useState(false); + + return ; + }, +}; + +export const WithoutLabel: Story = {}; + +export default { + component: Switch, + parameters: { + layout: 'centered', + }, +} as Meta; diff --git a/packages/ui-components/src/Common/Switch/index.tsx b/packages/ui-components/src/Common/Switch/index.tsx new file mode 100644 index 0000000000000..5181b99c2fa4b --- /dev/null +++ b/packages/ui-components/src/Common/Switch/index.tsx @@ -0,0 +1,49 @@ +'use client'; + +import * as SwitchPrimitive from '@radix-ui/react-switch'; +import classNames from 'classnames'; +import { useId } from 'react'; +import type { FC, PropsWithChildren } from 'react'; + +import styles from './index.module.css'; + +type SwitchProps = SwitchPrimitive.SwitchProps & { + label?: string; + checked?: boolean; + onCheckedChange?: (checked: boolean) => void; + thumbClassName?: string; +}; + +const Switch: FC> = ({ + label, + checked, + onCheckedChange, + className, + thumbClassName, + ...props +}) => { + const id = useId(); + + return ( +
+ {label && ( + + )} + + + +
+ ); +}; + +export default Switch; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e94f7f65f57e8..724f9667cec5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -483,6 +483,9 @@ importers: '@radix-ui/react-separator': specifier: ~1.1.7 version: 1.1.7(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-switch': + specifier: ~1.2.6 + version: 1.2.6(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-tabs': specifier: ~1.1.13 version: 1.1.13(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -2483,6 +2486,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + 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 + '@radix-ui/react-tabs@1.1.13': resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} peerDependencies: @@ -11164,6 +11180,20 @@ snapshots: optionalDependencies: '@types/react': 19.2.2 + '@radix-ui/react-switch@1.2.6(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@radix-ui/react-tabs@1.1.13(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -13864,7 +13894,7 @@ snapshots: eslint: 9.36.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)))(eslint@9.36.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.36.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.36.0(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.36.0(jiti@2.6.1)) @@ -13901,7 +13931,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)))(eslint@9.36.0(jiti@2.6.1)) eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13917,7 +13947,7 @@ snapshots: tinyglobby: 0.2.14 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)) eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.46.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13944,6 +13974,17 @@ snapshots: - bluebird - supports-color + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3) + eslint: 9.36.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)): dependencies: debug: 3.2.7 @@ -13954,6 +13995,7 @@ snapshots: eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color + optional: true eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)): dependencies: @@ -13973,7 +14015,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)))(eslint@9.36.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -13984,7 +14026,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.36.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -14002,7 +14044,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -14024,6 +14066,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack