Skip to content
83 changes: 83 additions & 0 deletions packages/ui-components/Common/ChangeHistory/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
@reference "../../styles/index.css";

.summary {
@apply outline-hidden
flex
h-9
cursor-pointer
select-none
items-center
gap-2
rounded-md
border
border-neutral-200
p-2
text-sm
text-neutral-700
motion-safe:transition-colors
dark:border-neutral-900
dark:text-neutral-300;

&:hover,
&:focus-visible {
@apply bg-neutral-100
dark:bg-neutral-900;
}
}

.dropdownContentWrapper {
@apply absolute
right-0
top-full
z-50
mt-1
max-h-80
w-52
overflow-hidden
rounded-sm
border
border-neutral-200
bg-white
shadow-lg
dark:border-neutral-900
dark:bg-neutral-950;
}

.dropdownContentInner {
@apply max-h-80
w-52
overflow-y-auto;
}

.dropdownItem {
@apply outline-hidden
block
px-2.5
py-1.5
text-sm
font-medium
text-neutral-800
no-underline
motion-safe:transition-colors
dark:text-white;

&:hover,
&:focus-visible {
@apply bg-green-600
text-white;
}
}

.dropdownLabel {
@apply block
text-sm
font-medium
leading-tight;
}

.dropdownVersions {
@apply block
text-xs
leading-tight
opacity-75;
}
130 changes: 130 additions & 0 deletions packages/ui-components/Common/ChangeHistory/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { Meta as MetaObj, StoryObj } from '@storybook/react';

import ChangeHistory from '#ui/Common/ChangeHistory';

type Story = StoryObj<typeof ChangeHistory>;
type Meta = MetaObj<typeof ChangeHistory>;

const SAMPLE_CHANGES = [
{
versions: ['v15.4.0'],
label: 'No longer experimental',
url: 'https://github.com/nodejs/node/pull/12345',
},
{
versions: ['v15.0.0', 'v14.17.0'],
label: 'Added in v15.0.0, v14.17.0',
url: 'https://github.com/nodejs/node/pull/67890',
},
{
versions: ['v16.0.0'],
label: 'Deprecated in 16',
},
];

const LARGE_SAMPLE_CHANGES = [
{
versions: ['v20.0.0'],
label: 'Breaking change in v20',
url: 'https://github.com/nodejs/node/pull/50001',
},
{
versions: ['v19.8.0'],
label: 'Performance improvement',
url: 'https://github.com/nodejs/node/pull/49999',
},
{
versions: ['v19.0.0'],
label: 'API redesign',
url: 'https://github.com/nodejs/node/pull/49000',
},
{
versions: ['v18.17.0', 'v18.16.1'],
label: 'Security fix backported',
url: 'https://github.com/nodejs/node/pull/48500',
},
{
versions: ['v18.0.0'],
label: 'Major version release',
url: 'https://github.com/nodejs/node/pull/47000',
},
{
versions: ['v17.9.0'],
label: 'Experimental feature added',
url: 'https://github.com/nodejs/node/pull/46500',
},
{
versions: ['v17.0.0'],
label: 'Node.js 17 release',
url: 'https://github.com/nodejs/node/pull/45000',
},
{
versions: ['v16.15.0', 'v16.14.2'],
label: 'Bug fix release',
url: 'https://github.com/nodejs/node/pull/44000',
},
{
versions: ['v16.0.0'],
label: 'Deprecated in v16',
url: 'https://github.com/nodejs/node/pull/43000',
},
{
versions: ['v15.14.0'],
label: 'Feature enhancement',
url: 'https://github.com/nodejs/node/pull/42000',
},
{
versions: ['v15.0.0', 'v14.17.0'],
label: 'Initial implementation',
url: 'https://github.com/nodejs/node/pull/41000',
},
{
versions: ['v14.18.0'],
label: 'Documentation update',
url: 'https://github.com/nodejs/node/pull/40000',
},
{
versions: ['v14.0.0'],
label: 'Added to stable API',
url: 'https://github.com/nodejs/node/pull/39000',
},
{
versions: ['v13.14.0'],
label: 'Experimental flag removed',
url: 'https://github.com/nodejs/node/pull/38000',
},
{
versions: ['v12.22.0', 'v12.21.0'],
label: 'Backported to LTS',
url: 'https://github.com/nodejs/node/pull/37000',
},
{
versions: ['v12.0.0'],
label: 'First experimental version',
url: 'https://github.com/nodejs/node/pull/36000',
},
];

export const Default: Story = {
render: args => (
<div className="right-0 flex justify-end">
<ChangeHistory {...args} />
</div>
),
args: {
changes: SAMPLE_CHANGES,
},
};

export const LargeHistory: Story = {
render: args => (
<div className="right-0 flex justify-end">
<ChangeHistory {...args} />
</div>
),
args: {
changes: LARGE_SAMPLE_CHANGES,
},
};

export default { component: ChangeHistory } as Meta;
67 changes: 67 additions & 0 deletions packages/ui-components/Common/ChangeHistory/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ChevronDownIcon, ClockIcon } from '@heroicons/react/24/outline';
import classNames from 'classnames';
import type { FC, ComponentProps } from 'react';

import type { LinkLike } from '#ui/types.js';

import styles from './index.module.css';

export type HistoryChange = {
versions: Array<string>;
label: string;
url?: string;
};

type ChangeHistoryProps = ComponentProps<'div'> & {
label: string;
changes: Array<HistoryChange>;
as?: LinkLike;
};

const ChangeHistory: FC<ChangeHistoryProps> = ({
label = 'History',
changes = [],
className,
as: As = 'a',
'aria-label': ariaLabel = label,
...props
}) => (
<div className={classNames('relative', 'inline-block', className)} {...props}>
<details className="group">
<summary className={styles.summary} role="button" aria-haspopup="menu">
<ClockIcon className="size-4" />
<span>{label}</span>
<ChevronDownIcon className="size-3 group-open:rotate-180 motion-safe:transition-transform" />
</summary>
<div
className={styles.dropdownContentWrapper}
role="menu"
aria-label={ariaLabel}
>
<div className={styles.dropdownContentInner}>
{changes.map((change, index) => {
const MenuItem = change.url ? As : 'div';

return (
<MenuItem
key={index}
className={styles.dropdownItem}
role="menuitem"
tabIndex={0}
aria-label={`${change.label}: ${change.versions.join(', ')}`}
href={change.url}
>
<div className={styles.dropdownLabel}>{change.label}</div>
<div className={styles.dropdownVersions}>
{change.versions.join(', ')}
</div>
</MenuItem>
);
})}
</div>
</div>
</details>
</div>
);

export default ChangeHistory;
Loading