[website] Add sidebar menu and breadcrumbs (#408)
* feat: create side-menu component for the website * feat: create index to export necessary components from website * fix: changes from review * feat: add breadcrumbs and refactor routes for insights
This commit is contained in:
parent
8a47bb4b02
commit
ca6490783f
|
@ -0,0 +1,60 @@
|
|||
import { Text } from '@status-im/components'
|
||||
import { ChevronRightIcon } from '@status-im/icons'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Link } from '../link'
|
||||
import { formatSegment } from './format-segment'
|
||||
|
||||
type Props = {
|
||||
cutFirstSegment?: boolean
|
||||
}
|
||||
|
||||
const Breadcrumbs = (props: Props) => {
|
||||
const { cutFirstSegment = true } = props
|
||||
|
||||
const router = useRouter()
|
||||
const { asPath } = router
|
||||
|
||||
// Splits the current path into segments
|
||||
const segments = asPath.split('/').filter(segment => segment !== '')
|
||||
|
||||
return (
|
||||
<nav className="flex items-center space-x-1">
|
||||
{segments.map((segment, index) => {
|
||||
// Builds the path up to the current segment
|
||||
const path = `/${segments.slice(0, index + 1).join('/')}`
|
||||
|
||||
// Determines if the current segment is the last one
|
||||
const isLastSegment = index === segments.length - 1
|
||||
|
||||
// If the first segment should be cut, skip it
|
||||
if (cutFirstSegment && index === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center" key={segment}>
|
||||
{/* Always render the first chevron unless cutFirstSegment is true and we need to avoid render the chevron before */}
|
||||
{!cutFirstSegment || (cutFirstSegment && index > 1) ? (
|
||||
<ChevronRightIcon size={20} color="$neutral-40" />
|
||||
) : null}
|
||||
|
||||
{isLastSegment ? (
|
||||
<Text size={15} color="$neutral-50" weight="medium">
|
||||
{formatSegment(segment)}
|
||||
</Text>
|
||||
) : (
|
||||
<Link href={path}>
|
||||
<Text size={15} weight="medium">
|
||||
{formatSegment(segment)}
|
||||
</Text>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export { Breadcrumbs }
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Util function that formats a segment for the breadcrumbs
|
||||
* @param segment - string to be formatted
|
||||
* formatSegment @returns a formatted string with the first letter of each word capitalized and spaces instead of "-"
|
||||
**/
|
||||
const formatSegment = (segment: string): string => {
|
||||
// Replaces "-" with a space and capitalize the first letter of each word
|
||||
const words = segment.split('-')
|
||||
const formattedSegment = words
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
return formattedSegment
|
||||
}
|
||||
|
||||
export { formatSegment }
|
|
@ -0,0 +1,2 @@
|
|||
export { Breadcrumbs } from './breadcrumbs/breadcrumbs'
|
||||
export { SideBar } from './side-bar/side-bar'
|
|
@ -0,0 +1,31 @@
|
|||
import { Text } from '@status-im/components'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Link } from '../link'
|
||||
|
||||
import type { LinkProps } from 'next/link'
|
||||
|
||||
const NavLink = (
|
||||
props: LinkProps & {
|
||||
children: string
|
||||
}
|
||||
) => {
|
||||
const { children, ...linkProps } = props
|
||||
|
||||
const { asPath } = useRouter()
|
||||
const active = asPath === props.href
|
||||
|
||||
return (
|
||||
<Link className="pl-5 transition-opacity hover:opacity-50" {...linkProps}>
|
||||
<Text
|
||||
size={19}
|
||||
weight="medium"
|
||||
color={active ? '$neutral-50' : '$neutral-100'}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export { NavLink }
|
|
@ -0,0 +1,72 @@
|
|||
import * as Accordion from '@radix-ui/react-accordion'
|
||||
import {} from '@radix-ui/react-accordion'
|
||||
import { Text } from '@status-im/components'
|
||||
import { ChevronRightIcon } from '@status-im/icons'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Link } from '../link'
|
||||
|
||||
import type { Url } from 'next/dist/shared/lib/router/router'
|
||||
|
||||
type NavLinkProps = {
|
||||
label: string
|
||||
links: {
|
||||
label: string
|
||||
href: Url
|
||||
}[]
|
||||
}
|
||||
|
||||
const NavNestedLinks = (props: NavLinkProps) => {
|
||||
const { label, links } = props
|
||||
|
||||
const { asPath } = useRouter()
|
||||
|
||||
return (
|
||||
<Accordion.Item value={label} className="accordion-item">
|
||||
<div>
|
||||
<Accordion.Trigger className="accordion-trigger">
|
||||
<div className="accordion-chevron inline-flex h-5 w-5">
|
||||
<ChevronRightIcon size={20} />
|
||||
</div>
|
||||
<Text size={19} weight="medium" color={'$neutral-100'}>
|
||||
{label}
|
||||
</Text>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content className="accordion-content">
|
||||
<div
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
paddingLeft: 20,
|
||||
}}
|
||||
>
|
||||
{links.map((link, index) => {
|
||||
const active = asPath === link.href
|
||||
|
||||
const paddingClassName = index === 0 ? 'pt-5' : 'pt-2'
|
||||
const paddingLastChild = index === links.length - 1 ? 'pb-5' : ''
|
||||
|
||||
return (
|
||||
<div
|
||||
key={link.label + index}
|
||||
className={`transition-opacity hover:opacity-50 ${paddingClassName} ${paddingLastChild}`}
|
||||
>
|
||||
<Link href={link.href}>
|
||||
<Text
|
||||
size={15}
|
||||
weight="medium"
|
||||
color={active ? '$neutral-50' : '$neutral-100'}
|
||||
>
|
||||
{link.label}
|
||||
</Text>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</div>
|
||||
</Accordion.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { NavNestedLinks }
|
|
@ -0,0 +1,80 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import * as Accordion from '@radix-ui/react-accordion'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { NavLink } from './nav-link'
|
||||
import { NavNestedLinks } from './nav-nested-links'
|
||||
|
||||
import type { Url } from 'next/dist/shared/lib/router/router'
|
||||
|
||||
type Props = {
|
||||
data?: {
|
||||
label: string
|
||||
href?: Url
|
||||
links?: {
|
||||
label: string
|
||||
href: Url
|
||||
}[]
|
||||
}[]
|
||||
}
|
||||
|
||||
const SideBar = (props: Props) => {
|
||||
const { data } = props
|
||||
|
||||
const [label, setLabel] = useState<string>('')
|
||||
|
||||
const { asPath } = useRouter()
|
||||
|
||||
const defaultLabel = data?.find(
|
||||
item =>
|
||||
item.href === asPath || item.links?.find(link => link.href === asPath)
|
||||
)?.label
|
||||
|
||||
useEffect(() => {
|
||||
setLabel(defaultLabel || '')
|
||||
}, [defaultLabel])
|
||||
|
||||
return (
|
||||
<div className="border-neutral-10 border-r p-5">
|
||||
<aside className=" sticky top-5 min-w-[320px]">
|
||||
<Accordion.Root
|
||||
type="single"
|
||||
collapsible
|
||||
value={label}
|
||||
onValueChange={value => setLabel(value)}
|
||||
className="accordion-root flex flex-col gap-3"
|
||||
>
|
||||
{data?.map((item, index) => {
|
||||
if (item.links) {
|
||||
return (
|
||||
<NavNestedLinks
|
||||
key={index}
|
||||
label={item.label}
|
||||
links={item.links}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion.Item
|
||||
key={item.label}
|
||||
value={item.label}
|
||||
className="accordion-item"
|
||||
>
|
||||
<Accordion.Trigger
|
||||
className="accordion-trigger"
|
||||
onClick={() => setLabel(item.label)}
|
||||
>
|
||||
<NavLink href={item.href || ''}>{item.label}</NavLink>
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Item>
|
||||
)
|
||||
})}
|
||||
</Accordion.Root>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { SideBar }
|
|
@ -1,42 +1,91 @@
|
|||
import { Text } from '@status-im/components'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Link } from '@/components/link'
|
||||
|
||||
import { SideBar } from '../components'
|
||||
import { AppLayout } from './app-layout'
|
||||
|
||||
import type { PageLayout } from 'next'
|
||||
import type { LinkProps } from 'next/link'
|
||||
|
||||
// Eventually this will be fetched from the API, at least the nested links
|
||||
const MENU_LINKS = [
|
||||
{
|
||||
label: 'Epics',
|
||||
links: [
|
||||
{
|
||||
label: 'Overview',
|
||||
href: '/insights/epics',
|
||||
},
|
||||
{
|
||||
label: 'Community Protocol',
|
||||
href: '/insights/epics/community-protocol',
|
||||
},
|
||||
{
|
||||
label: 'Keycard',
|
||||
href: '/insights/epics/keycard',
|
||||
},
|
||||
{
|
||||
label: 'Notifications Settings',
|
||||
href: '/insights/epics/notifications-settings',
|
||||
},
|
||||
{
|
||||
label: 'Wallet',
|
||||
href: '/insights/epics/wallet',
|
||||
},
|
||||
{
|
||||
label: 'Communities',
|
||||
href: '/insights/epics/communities',
|
||||
},
|
||||
{
|
||||
label: 'Acitivity Center',
|
||||
href: '/insights/epics/activity-center',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Workstreams',
|
||||
links: [
|
||||
{
|
||||
label: 'Overview',
|
||||
href: '/insights/workstreams',
|
||||
},
|
||||
{
|
||||
label: 'Community Protocol 2',
|
||||
href: '/insights/workstreams/community-protocol-2',
|
||||
},
|
||||
{
|
||||
label: 'Keycard 2',
|
||||
href: '/insights/workstreams/keycard-2',
|
||||
},
|
||||
{
|
||||
label: 'Notifications Settings 2',
|
||||
href: '/insights/workstreams/notifications-settings-2',
|
||||
},
|
||||
{
|
||||
label: 'Wallet 2',
|
||||
href: '/insights/workstreams/wallet-2',
|
||||
},
|
||||
{
|
||||
label: 'Communities 2',
|
||||
href: '/insights/workstreams/communities-2',
|
||||
},
|
||||
{
|
||||
label: 'Acitivity Center 2',
|
||||
href: '/insights/workstreams/activity-center-2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Orphans',
|
||||
href: '/insights/orphans',
|
||||
},
|
||||
{
|
||||
label: 'Repos',
|
||||
href: '/insights/repos',
|
||||
},
|
||||
]
|
||||
|
||||
export const InsightsLayout: PageLayout = page => {
|
||||
return AppLayout(
|
||||
<div className="bg-white-100 mx-1 grid min-h-[calc(100vh-56px-4px)] grid-cols-[320px_1fr] items-stretch rounded-3xl">
|
||||
<aside className="border-neutral-10 flex flex-col gap-3 border-r p-5">
|
||||
<NavLink href="/insights">Epics</NavLink>
|
||||
<NavLink href="/insights/detail">Detail</NavLink>
|
||||
<NavLink href="/insights/orphans">Orphans</NavLink>
|
||||
<NavLink href="/insights/repos">Repos</NavLink>
|
||||
</aside>
|
||||
<main className="p-10">{page}</main>
|
||||
<div className="bg-white-100 relative mx-1 flex min-h-[calc(100vh-56px-4px)] rounded-3xl">
|
||||
<SideBar data={MENU_LINKS} />
|
||||
<main className="flex-1">{page}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NavLink = (props: LinkProps & { children: string }) => {
|
||||
const { children, ...linkProps } = props
|
||||
|
||||
const { asPath } = useRouter()
|
||||
const active = asPath === props.href
|
||||
|
||||
return (
|
||||
<Link className="transition-opacity hover:opacity-50" {...linkProps}>
|
||||
<Text
|
||||
size={19}
|
||||
weight="medium"
|
||||
color={active ? '$neutral-50' : '$neutral-100'}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import '@/styles/global.css'
|
||||
import '@/styles/nav-nested-links.css'
|
||||
|
||||
import { Provider } from '@status-im/components'
|
||||
import { Inter } from 'next/font/google'
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import { EpicOverview } from '@/components/epic-overview'
|
||||
import { TableIssues } from '@/components/table-issues'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
|
||||
import type { Page } from 'next'
|
||||
|
||||
const InsightsDetailPage: Page = () => {
|
||||
return (
|
||||
<>
|
||||
<EpicOverview
|
||||
title="Communities protocol"
|
||||
description="Detecting keycard reader removal for the beginning of each flow"
|
||||
fullscreen
|
||||
/>
|
||||
|
||||
<div role="separator" className="bg-neutral-10 -mx-6 my-6 h-px" />
|
||||
|
||||
<TableIssues />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
InsightsDetailPage.getLayout = InsightsLayout
|
||||
|
||||
export default InsightsDetailPage
|
|
@ -0,0 +1,31 @@
|
|||
import { Breadcrumbs } from '@/components'
|
||||
import { EpicOverview } from '@/components/epic-overview'
|
||||
import { TableIssues } from '@/components/table-issues'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
|
||||
import type { Page } from 'next'
|
||||
|
||||
const EpicsDetailPage: Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="border-neutral-10 border-b px-5 py-3">
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
<div className="px-10 py-6">
|
||||
<EpicOverview
|
||||
title="Communities protocol"
|
||||
description="Detecting keycard reader removal for the beginning of each flow"
|
||||
fullscreen
|
||||
/>
|
||||
|
||||
<div role="separator" className="bg-neutral-10 -mx-6 my-6 h-px" />
|
||||
|
||||
<TableIssues />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
EpicsDetailPage.getLayout = InsightsLayout
|
||||
|
||||
export default EpicsDetailPage
|
|
@ -0,0 +1,62 @@
|
|||
import { IconButton, Shadow, Tag, Text } from '@status-im/components'
|
||||
import {
|
||||
DoneIcon,
|
||||
NotStartedIcon,
|
||||
OpenIcon,
|
||||
SearchIcon,
|
||||
SortIcon,
|
||||
} from '@status-im/icons'
|
||||
|
||||
import { EpicOverview } from '@/components/epic-overview'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
|
||||
import type { Page } from 'next'
|
||||
|
||||
const epics = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Communities protocol',
|
||||
description: 'Support Encrypted Communities',
|
||||
},
|
||||
{
|
||||
id: 5155,
|
||||
title: 'Keycard',
|
||||
description:
|
||||
'Detecting keycard reader removal for the beginning of each flow',
|
||||
},
|
||||
]
|
||||
|
||||
const EpicsPage: Page = () => {
|
||||
return (
|
||||
<div className="space-y-4 p-10">
|
||||
<Text size={27} weight="semibold">
|
||||
Epics
|
||||
</Text>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Tag size={32} label="In Progress" icon={OpenIcon} selected />
|
||||
<Tag size={32} label="Closed" icon={DoneIcon} />
|
||||
<Tag size={32} label="Not Started" icon={NotStartedIcon} />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<IconButton variant="outline" icon={<SearchIcon size={20} />} />
|
||||
<IconButton variant="outline" icon={<SortIcon size={20} />} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{epics.map(epic => (
|
||||
<Shadow key={epic.id} variant="$2" className="rounded-2xl px-4 py-3">
|
||||
<EpicOverview title={epic.title} description={epic.description} />
|
||||
</Shadow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
EpicsPage.getLayout = InsightsLayout
|
||||
|
||||
export default EpicsPage
|
|
@ -1,62 +1,16 @@
|
|||
import { IconButton, Shadow, Tag, Text } from '@status-im/components'
|
||||
import {
|
||||
DoneIcon,
|
||||
NotStartedIcon,
|
||||
OpenIcon,
|
||||
SearchIcon,
|
||||
SortIcon,
|
||||
} from '@status-im/icons'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { EpicOverview } from '@/components/epic-overview'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import type { Page } from 'next'
|
||||
export default function InsightsPage() {
|
||||
const router = useRouter()
|
||||
|
||||
const epics = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Communities protocol',
|
||||
description: 'Support Encrypted Communities',
|
||||
},
|
||||
{
|
||||
id: 5155,
|
||||
title: 'Keycard',
|
||||
description:
|
||||
'Detecting keycard reader removal for the beginning of each flow',
|
||||
},
|
||||
]
|
||||
|
||||
const InsightsPage: Page = () => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Text size={27} weight="semibold">
|
||||
Epics
|
||||
</Text>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Tag size={32} label="In Progress" icon={OpenIcon} selected />
|
||||
<Tag size={32} label="Closed" icon={DoneIcon} />
|
||||
<Tag size={32} label="Not Started" icon={NotStartedIcon} />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<IconButton variant="outline" icon={<SearchIcon size={20} />} />
|
||||
<IconButton variant="outline" icon={<SortIcon size={20} />} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{epics.map(epic => (
|
||||
<Shadow key={epic.id} variant="$2" className="rounded-2xl px-4 py-3">
|
||||
<EpicOverview title={epic.title} description={epic.description} />
|
||||
</Shadow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
useEffect(() => {
|
||||
// Redirect to the epics page if the user lands on the insights page
|
||||
if (router.pathname === '/insights') {
|
||||
router.replace('/insights/epics')
|
||||
}
|
||||
}, [router])
|
||||
|
||||
InsightsPage.getLayout = InsightsLayout
|
||||
|
||||
export default InsightsPage
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import type { Page } from 'next'
|
|||
|
||||
const OrphansPage: Page = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 p-10">
|
||||
<Text size={27} weight="semibold">
|
||||
Orphans
|
||||
</Text>
|
||||
|
|
|
@ -46,7 +46,7 @@ const repos = [
|
|||
|
||||
const ReposPage: Page = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="p-10">
|
||||
<div className="mb-6">
|
||||
<Text size={27} weight="semibold">
|
||||
Repos
|
||||
|
@ -81,7 +81,7 @@ const ReposPage: Page = () => {
|
|||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { Breadcrumbs } from '@/components'
|
||||
import { EpicOverview } from '@/components/epic-overview'
|
||||
import { TableIssues } from '@/components/table-issues'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
|
||||
import type { Page } from 'next'
|
||||
|
||||
const WorkstreamDetailPage: Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="border-neutral-10 border-b px-5 py-3">
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
<div className="px-10 py-6">
|
||||
<EpicOverview
|
||||
title="Communities protocol"
|
||||
description="Detecting keycard reader removal for the beginning of each flow"
|
||||
fullscreen
|
||||
/>
|
||||
|
||||
<div role="separator" className="bg-neutral-10 -mx-6 my-6 h-px" />
|
||||
|
||||
<TableIssues />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
WorkstreamDetailPage.getLayout = InsightsLayout
|
||||
|
||||
export default WorkstreamDetailPage
|
|
@ -0,0 +1,69 @@
|
|||
import { IconButton, Shadow, Tag, Text } from '@status-im/components'
|
||||
import {
|
||||
DoneIcon,
|
||||
NotStartedIcon,
|
||||
OpenIcon,
|
||||
SearchIcon,
|
||||
SortIcon,
|
||||
} from '@status-im/icons'
|
||||
|
||||
import { EpicOverview } from '@/components/epic-overview'
|
||||
import { InsightsLayout } from '@/layouts/insights-layout'
|
||||
|
||||
import type { Page } from 'next'
|
||||
|
||||
const workstreams = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Workstream protocol',
|
||||
description: 'Support Encrypted Communities',
|
||||
},
|
||||
{
|
||||
id: 5155,
|
||||
title: 'Work with keys',
|
||||
description:
|
||||
'Detecting keycard reader removal for the beginning of each flow',
|
||||
},
|
||||
]
|
||||
|
||||
const WorkstreamsPage: Page = () => {
|
||||
return (
|
||||
<div className="space-y-4 p-10">
|
||||
<Text size={27} weight="semibold">
|
||||
Workstreams
|
||||
</Text>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Tag size={32} label="In Progress" icon={OpenIcon} selected />
|
||||
<Tag size={32} label="Closed" icon={DoneIcon} />
|
||||
<Tag size={32} label="Not Started" icon={NotStartedIcon} />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<IconButton variant="outline" icon={<SearchIcon size={20} />} />
|
||||
<IconButton variant="outline" icon={<SortIcon size={20} />} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{workstreams.map(workstream => (
|
||||
<Shadow
|
||||
key={workstream.id}
|
||||
variant="$2"
|
||||
className="rounded-2xl px-4 py-3"
|
||||
>
|
||||
<EpicOverview
|
||||
title={workstream.title}
|
||||
description={workstream.description}
|
||||
/>
|
||||
</Shadow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
WorkstreamsPage.getLayout = InsightsLayout
|
||||
|
||||
export default WorkstreamsPage
|
|
@ -0,0 +1,41 @@
|
|||
.accordion-trigger {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
.accordion-content[data-state='open'] {
|
||||
animation: slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1);
|
||||
}
|
||||
.accordion-content[data-state='closed'] {
|
||||
animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1);
|
||||
}
|
||||
|
||||
.accordion-chevron {
|
||||
transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1);
|
||||
}
|
||||
.accordion-trigger[data-state='open'] > .accordion-chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue