feat(analytics-docs): Add changelog sidebar

This commit is contained in:
Jan Václavík
2026-02-17 15:27:46 +01:00
parent f8d4a2095b
commit 0eb3041e1a
4 changed files with 182 additions and 8 deletions

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import styled from 'styled-components';
@@ -23,6 +23,9 @@ import { Filter } from './components/Filter';
import { GlobalStyle } from './components/GlobalStyle';
import { ResultsInfo } from './components/ResultsInfo';
import { ThemeSwitch } from './components/ThemeSwitch';
import { SIDEBAR_WIDTH, VersionsSidebar } from './components/VersionsSidebar';
import { HEADER_HEIGHT } from './constants';
import { getEventId, getVersionsWithEvents } from './utils/filterUtils';
import { useFilteredEvents } from './utils/useFilteredEvents';
type AppTheme = SuiteThemeColors & { variant: 'light' | 'dark'; mode: 'light' | 'dark' };
@@ -48,7 +51,7 @@ export const Content = styled.div`
padding: 20px 10px;
@media (min-width: ${variables.SCREEN_SIZE.MD}) {
margin: 130px 20px 20px;
margin: ${HEADER_HEIGHT}px 20px 0px;
}
`;
@@ -58,7 +61,30 @@ export const ContentContainer = styled.div`
width: 100%;
`;
const MainWithSidebar = styled.div`
display: flex;
flex: 1;
min-height: 0;
@media (min-width: ${variables.SCREEN_SIZE.MD}) {
margin: ${HEADER_HEIGHT}px 0 0px;
}
`;
const ContentArea = styled.div`
flex: 1;
min-width: 0;
margin: 0;
padding: 20px 10px;
@media (min-width: ${variables.SCREEN_SIZE.MD}) {
margin: 0 20px 20px 0;
margin-left: ${SIDEBAR_WIDTH + 20}px;
}
`;
export const App = ({ theme }: AppProps) => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const {
filteredEvents,
setQuery,
@@ -75,8 +101,18 @@ export const App = ({ theme }: AppProps) => {
const hasActiveFilters = !!query || platform !== 'all' || sort !== 'az';
const versionsWithEvents = useMemo(
() => getVersionsWithEvents(filteredEvents),
[filteredEvents],
);
const eventCards = useMemo(
() => filteredEvents.map(event => <EventCard key={event.name} event={event} />),
() =>
filteredEvents.map(event => (
<div key={event.name} id={getEventId(event.name)}>
<EventCard event={event} />
</div>
)),
[filteredEvents],
);
const isMobile = useMediaQuery(`(max-width: ${variables.SCREEN_SIZE.MD})`);
@@ -104,6 +140,21 @@ export const App = ({ theme }: AppProps) => {
{isFiltering && <Spinner size={20} />}
</Row>
<Row gap={8} alignItems="center">
<Tooltip
content={
isSidebarOpen
? 'Hide versions'
: 'Show versions by last updated'
}
>
<IconButton
icon="sidebar"
onClick={() => setIsSidebarOpen(prev => !prev)}
intent={isSidebarOpen ? 'brand' : 'neutral'}
size="small"
priority={isSidebarOpen ? 'primary' : 'secondary'}
/>
</Tooltip>
<ThemeSwitch />
<Tooltip content="Add event">
{isMobile ? (
@@ -142,11 +193,22 @@ export const App = ({ theme }: AppProps) => {
</Column>
</ContentContainer>
</TopBar>
<Content>
<ContentContainer>
<Column gap={40}>{eventCards}</Column>
</ContentContainer>
</Content>
{isSidebarOpen ? (
<MainWithSidebar>
<VersionsSidebar versionsWithEvents={versionsWithEvents} />
<ContentArea>
<ContentContainer>
<Column gap={40}>{eventCards}</Column>
</ContentContainer>
</ContentArea>
</MainWithSidebar>
) : (
<Content>
<ContentContainer>
<Column gap={40}>{eventCards}</Column>
</ContentContainer>
</Content>
)}
</Box>
</>
);

View File

@@ -0,0 +1,74 @@
import styled from 'styled-components';
import { Badge, Box, CardList, Column, H3, Text, variables } from '@trezor/components';
import type { SuiteThemeColors } from '@trezor/components';
import { HEADER_HEIGHT } from '../constants';
import type { EventDoc } from '../types';
import type { VersionWithEvents } from '../utils/filterUtils';
import { getEventId } from '../utils/filterUtils';
const scrollToEvent = (eventName: string) => {
const el = document.getElementById(getEventId(eventName));
el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
export const SIDEBAR_WIDTH = 280;
const SidebarWrapper = styled.aside<{ theme: SuiteThemeColors }>`
width: ${SIDEBAR_WIDTH}px;
flex-shrink: 0;
background: ${({ theme }) => theme.backgroundSurfaceElevation1};
border-right: 1px solid ${({ theme }) => theme.borderOnElevation1};
padding: 16px 0;
overflow-y: auto;
@media (min-width: ${variables.SCREEN_SIZE.MD}) {
position: fixed;
top: ${HEADER_HEIGHT}px;
left: 0;
bottom: 0;
height: calc(100vh - ${HEADER_HEIGHT}px);
z-index: 10;
}
@media (max-width: ${variables.SCREEN_SIZE.MD}) {
width: 100%;
border-right: none;
border-bottom: 1px solid ${({ theme }) => theme.borderOnElevation1};
}
`;
type VersionsSidebarProps = {
versionsWithEvents: VersionWithEvents[];
};
export const VersionsSidebar = ({ versionsWithEvents }: VersionsSidebarProps) => (
<SidebarWrapper>
<Column gap={0}>
<H3 margin={{ left: 20, top: 8, bottom: 16 }}>Changelog</H3>
{versionsWithEvents.map(({ version, events }) => (
<Box key={version} padding={{ top: 0, bottom: 16, horizontal: 16 }}>
<Badge intent="brand" size="small">
{version}
</Badge>
<CardList>
{events
.sort((a: EventDoc, b: EventDoc) =>
(a.name ?? '').localeCompare(b.name ?? ''),
)
.map(event => (
<CardList.Item
onClick={() => scrollToEvent(event.name)}
key={event.name}
>
<Text>{event.name}</Text>
</CardList.Item>
))}
</CardList>
</Box>
))}
</Column>
</SidebarWrapper>
);

View File

@@ -3,6 +3,8 @@ import { Icon, Row } from '@trezor/components';
import { Platform, Sort } from './types';
import { getPlatformIcon } from './utils/getPlatformIcon';
export const HEADER_HEIGHT = 110;
const PlatformItem = ({ platform }: { platform: string }) => (
<Row alignItems="center" gap={8}>
<Icon name={getPlatformIcon(platform)} size="medium" />

View File

@@ -63,3 +63,39 @@ export const getEventAddedVersion = (e: EventDoc): string | undefined =>
export const getEventUpdatedVersion = (e: EventDoc): string | undefined =>
e.changelog?.lastUpdatedInVersion ?? e.changelog?.addedInVersion;
/** Safe DOM id for scrolling to an event card. */
export const getEventId = (eventName: string): string => `event-${eventName.replace(/\//g, '-')}`;
export type VersionWithEvents = {
version: string;
events: EventDoc[];
};
/** Returns versions (desc by last updated) with events that were added or updated in that version. */
export const getVersionsWithEvents = (events: EventDoc[]): VersionWithEvents[] => {
const versionToEvents = new Map<string, EventDoc[]>();
for (const event of events) {
const added = event.changelog?.addedInVersion;
const updated = event.changelog?.lastUpdatedInVersion ?? added;
if (added) {
const list = versionToEvents.get(added) ?? [];
list.push(event);
versionToEvents.set(added, list);
}
if (updated && updated !== added) {
const list = versionToEvents.get(updated) ?? [];
list.push(event);
versionToEvents.set(updated, list);
}
}
const versions = Array.from(versionToEvents.keys()).sort(compareVersionsDesc);
return versions.map(version => ({
version,
events: versionToEvents.get(version) ?? [],
}));
};