@meith/theme-clubhouse 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ import { Card, CardRows, Empty, EmptyAction, EmptyDescription, EmptyTitle, cn } from '@meith/ui'
2
+ import type { ForumDisplayModel } from '@meith/theme-kit'
3
+
4
+ import {
5
+ BUTTON,
6
+ ColumnHeads,
7
+ HEADING,
8
+ MICRO,
9
+ NUMERIC,
10
+ PAGE_BODY,
11
+ PageHead,
12
+ isEmptyRegion,
13
+ } from '../shared'
14
+
15
+ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }: ForumDisplayModel) {
16
+ return (
17
+ <div className={PAGE_BODY}>
18
+ <PageHead
19
+ actions={
20
+ <>
21
+ {markReadAction !== null && (
22
+ <form action={markReadAction} method="post">
23
+ <button
24
+ type="submit"
25
+ className={cn(
26
+ BUTTON,
27
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
28
+ )}
29
+ >
30
+ Mark read
31
+ </button>
32
+ </form>
33
+ )}
34
+ {newThreadHref !== null && (
35
+ <a
36
+ href={newThreadHref}
37
+ className={cn(BUTTON, 'bg-primary text-primary-foreground hover:bg-primary-hover')}
38
+ >
39
+ New thread
40
+ </a>
41
+ )}
42
+ </>
43
+ }
44
+ >
45
+ <h1 className={`${HEADING} text-xl text-balance`}>{forum.title}</h1>
46
+ <p className="mt-0.5 flex flex-wrap items-baseline gap-x-2 text-xs text-muted-foreground">
47
+ {forum.type !== 'link' && (
48
+ <span className={`${MICRO} ${NUMERIC}`}>
49
+ {forum.threadCount.toLocaleString('en')} threads ·{' '}
50
+ {forum.postCount.toLocaleString('en')} posts
51
+ </span>
52
+ )}
53
+ {forum.description !== null && <span>{forum.description}</span>}
54
+ </p>
55
+ </PageHead>
56
+
57
+ {regions.tools !== undefined && (
58
+ <div className="flex flex-col gap-3 empty:hidden">{regions.tools}</div>
59
+ )}
60
+
61
+ {regions.announcements !== undefined && (
62
+ <div className="flex flex-col gap-3">{regions.announcements}</div>
63
+ )}
64
+
65
+ {regions.subforums}
66
+
67
+ <Card>
68
+ {isEmptyRegion(regions.threads) ? (
69
+ <Empty>
70
+ <EmptyTitle>No threads here yet</EmptyTitle>
71
+ <EmptyDescription>
72
+ {newThreadHref === null
73
+ ? 'Nothing has been posted in this forum.'
74
+ : 'Nothing has been posted in this forum. Yours would be the first.'}
75
+ </EmptyDescription>
76
+ {newThreadHref !== null && (
77
+ <EmptyAction>
78
+ <a
79
+ href={newThreadHref}
80
+ className={cn(
81
+ BUTTON,
82
+ 'bg-primary text-primary-foreground hover:bg-primary-hover',
83
+ )}
84
+ >
85
+ Start the first thread
86
+ </a>
87
+ </EmptyAction>
88
+ )}
89
+ </Empty>
90
+ ) : (
91
+ <>
92
+ <ColumnHeads first="Thread" counts={['Replies', 'Views']} last="Latest" />
93
+ <CardRows>{regions.threads}</CardRows>
94
+ </>
95
+ )}
96
+ </Card>
97
+
98
+ {regions.pagination}
99
+
100
+ {regions.afterContent !== undefined && (
101
+ <div className="flex flex-wrap items-center justify-between gap-x-8 gap-y-3 border-t border-border pt-4 empty:hidden">
102
+ {regions.afterContent}
103
+ </div>
104
+ )}
105
+ </div>
106
+ )
107
+ }
@@ -0,0 +1,76 @@
1
+ import type { ForumRowSlotModel } from '@meith/theme-kit'
2
+
3
+ import { CHIP, HEADING, LINK, MICRO, ReadMark, Stamp, TABLE_ROW, Tally, UserRef } from '../shared'
4
+
5
+ export function ForumRow({ forum }: ForumRowSlotModel) {
6
+ const isLink = forum.type === 'link'
7
+
8
+ return (
9
+ <li
10
+ data-unread={forum.isUnread ? '' : undefined}
11
+ className={`${TABLE_ROW} transition-colors hover:bg-accent`}
12
+ >
13
+ <ReadMark unread={forum.isUnread} />
14
+
15
+ <div className="min-w-0">
16
+ <a
17
+ href={forum.href}
18
+ className={`${HEADING} text-sm ${forum.isUnread ? 'text-forum-unread' : 'text-forum-read'} ${LINK}`}
19
+ >
20
+ {forum.title}
21
+ </a>
22
+ {forum.isUnread && <span className="sr-only"> (new posts)</span>}
23
+ {isLink && <span className={`${MICRO} ml-2`}>Link</span>}
24
+
25
+ {forum.description !== null && (
26
+ <p className="mt-0.5 text-xs text-muted-foreground">{forum.description}</p>
27
+ )}
28
+
29
+ {forum.subforums.length > 0 && (
30
+ <p className="mt-1.5 flex flex-wrap items-center gap-1">
31
+ {forum.subforums.map((sub) => (
32
+ <a key={sub.href} href={sub.href} className={CHIP}>
33
+ {sub.label}
34
+ </a>
35
+ ))}
36
+ </p>
37
+ )}
38
+ </div>
39
+
40
+ {!isLink && (
41
+ <>
42
+ <span className="col-start-2 md:col-start-3 md:block md:text-right">
43
+ <Tally value={forum.threadCount} label="threads" />
44
+ <span className="ms-3 md:hidden">
45
+ <Tally value={forum.postCount} label="posts" />
46
+ </span>
47
+ </span>
48
+
49
+ <span className="hidden md:block md:text-right">
50
+ <Tally value={forum.postCount} label="posts" />
51
+ </span>
52
+
53
+ <div className="col-start-2 min-w-0 text-xs text-muted-foreground md:col-start-5">
54
+ {forum.lastPost === null ? (
55
+ <span className={MICRO}>No posts yet</span>
56
+ ) : (
57
+ <>
58
+ <a
59
+ href={forum.lastPost.href}
60
+ className={`block truncate font-semibold text-foreground ${LINK}`}
61
+ >
62
+ {forum.lastPost.threadTitle}
63
+ </a>
64
+ <span className="mt-0.5 block truncate">
65
+ <UserRef user={forum.lastPost.author} className="font-medium" />
66
+ {' · '}
67
+ <Stamp at={forum.lastPost.at} />
68
+ </span>
69
+ </>
70
+ )}
71
+ </div>
72
+ </>
73
+ )}
74
+ </li>
75
+ )
76
+ }
@@ -0,0 +1,71 @@
1
+ import type { HeaderModel } from '@meith/theme-kit'
2
+
3
+ import { ClubRule, Crest, HEADING, PAGE, TAB } from '../shared'
4
+
5
+ function BoardMark({ boardTitle, logo }: Pick<HeaderModel, 'boardTitle' | 'logo'>) {
6
+ if (logo === undefined) {
7
+ return (
8
+ <>
9
+ <Crest title={boardTitle} />
10
+ <span className={`${HEADING} text-lg leading-none text-balance sm:text-xl`}>
11
+ {boardTitle}
12
+ </span>
13
+ </>
14
+ )
15
+ }
16
+
17
+ const image = (
18
+ <img
19
+ src={logo.src}
20
+ alt={logo.alt}
21
+ className="h-9 w-auto max-w-48 object-contain"
22
+ decoding="async"
23
+ />
24
+ )
25
+
26
+ if (logo.darkSrc === null) return image
27
+
28
+ return (
29
+ <picture>
30
+ <source media="(prefers-color-scheme: dark)" srcSet={logo.darkSrc} />
31
+ {image}
32
+ </picture>
33
+ )
34
+ }
35
+
36
+ export function Header({ boardTitle, homeHref, navigation, logo, children }: HeaderModel) {
37
+ return (
38
+ <header className="bg-card">
39
+ <div className={`${PAGE} flex flex-wrap items-center justify-between gap-x-6 gap-y-3 py-3`}>
40
+ <a
41
+ href={homeHref}
42
+ className="inline-flex min-w-0 items-center gap-3 text-foreground underline-offset-4 hover:underline"
43
+ >
44
+ <BoardMark boardTitle={boardTitle} logo={logo} />
45
+ </a>
46
+ {children}
47
+ </div>
48
+
49
+ <ClubRule />
50
+
51
+ {navigation.length > 0 && (
52
+ <nav aria-label="Board sections" className="border-b border-border bg-surface">
53
+ <div className={PAGE}>
54
+ <ul className="-mx-4 flex items-stretch overflow-x-auto px-4 sm:-mx-6 sm:px-6">
55
+ {navigation.map((item) => (
56
+ <li key={item.href} className="shrink-0">
57
+ <a
58
+ href={item.href}
59
+ className={`${TAB} border-b-2 border-b-transparent text-muted-foreground hover:border-b-secondary hover:bg-primary hover:text-primary-foreground`}
60
+ >
61
+ {item.label}
62
+ </a>
63
+ </li>
64
+ ))}
65
+ </ul>
66
+ </div>
67
+ </nav>
68
+ )}
69
+ </header>
70
+ )
71
+ }
@@ -0,0 +1,56 @@
1
+ import { Card, CardContent, Empty, EmptyDescription, EmptyTitle } from '@meith/ui'
2
+ import type { LatestPostsModel } from '@meith/theme-kit'
3
+
4
+ import { LINK, MICRO, MUTED_LINK, PanelHead, Stamp, UserRef } from '../shared'
5
+
6
+ export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
7
+ return (
8
+ <Card aria-labelledby="latest-posts-heading">
9
+ <PanelHead
10
+ id="latest-posts-heading"
11
+ title="Latest posts"
12
+ aside={
13
+ <span className={MICRO}>
14
+ As of <Stamp at={capturedAt} />
15
+ </span>
16
+ }
17
+ />
18
+
19
+ {posts.length === 0 ? (
20
+ <Empty className="py-5">
21
+ <EmptyTitle>Nothing said yet</EmptyTitle>
22
+ <EmptyDescription>The newest posts you can see will appear here.</EmptyDescription>
23
+ </Empty>
24
+ ) : (
25
+ <CardContent className="px-0 py-0">
26
+ <ul className="divide-y divide-border">
27
+ {posts.map((post) => (
28
+ <li key={post.href} className="flex flex-col gap-0.5 px-4 py-2">
29
+ <a
30
+ href={post.href}
31
+ className={`truncate text-sm font-semibold text-foreground ${LINK}`}
32
+ >
33
+ {post.threadTitle}
34
+ </a>
35
+
36
+ {post.excerpt !== '' && (
37
+ <p className="line-clamp-2 text-xs text-muted-foreground">{post.excerpt}</p>
38
+ )}
39
+
40
+ <p className="truncate text-xs text-muted-foreground">
41
+ <UserRef user={post.author} className="font-medium" />
42
+ {' in '}
43
+ <a href={post.forum.href} className={MUTED_LINK}>
44
+ {post.forum.label}
45
+ </a>
46
+ {' · '}
47
+ <Stamp at={post.postedAt} />
48
+ </p>
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ </CardContent>
53
+ )}
54
+ </Card>
55
+ )
56
+ }
@@ -0,0 +1,57 @@
1
+ import { Card, CardContent, Empty, EmptyDescription, EmptyTitle } from '@meith/ui'
2
+ import type { LatestThreadsModel } from '@meith/theme-kit'
3
+
4
+ import { LINK, MICRO, MUTED_LINK, NUMERIC, PanelHead, Stamp, UserRef } from '../shared'
5
+
6
+ export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
7
+ return (
8
+ <Card aria-labelledby="latest-threads-heading">
9
+ <PanelHead
10
+ id="latest-threads-heading"
11
+ title="Latest threads"
12
+ aside={
13
+ <span className={MICRO}>
14
+ As of <Stamp at={capturedAt} />
15
+ </span>
16
+ }
17
+ />
18
+
19
+ {threads.length === 0 ? (
20
+ <Empty className="py-5">
21
+ <EmptyTitle>Nothing started yet</EmptyTitle>
22
+ <EmptyDescription>The newest threads you can see will appear here.</EmptyDescription>
23
+ </Empty>
24
+ ) : (
25
+ <CardContent className="px-0 py-0">
26
+ <ul className="divide-y divide-border">
27
+ {threads.map((thread) => (
28
+ <li key={thread.href} className="flex flex-col gap-0.5 px-4 py-2">
29
+ <div className="flex items-baseline justify-between gap-2">
30
+ <a
31
+ href={thread.href}
32
+ className={`truncate text-sm font-semibold text-foreground ${LINK}`}
33
+ >
34
+ {thread.title}
35
+ </a>
36
+ <span className={`shrink-0 text-xs text-muted-foreground ${NUMERIC}`}>
37
+ {thread.replyCount.toLocaleString('en')}
38
+ </span>
39
+ </div>
40
+
41
+ <p className="truncate text-xs text-muted-foreground">
42
+ <UserRef user={thread.author} className="font-medium" />
43
+ {' in '}
44
+ <a href={thread.forum.href} className={MUTED_LINK}>
45
+ {thread.forum.label}
46
+ </a>
47
+ {' · '}
48
+ <Stamp at={thread.startedAt} />
49
+ </p>
50
+ </li>
51
+ ))}
52
+ </ul>
53
+ </CardContent>
54
+ )}
55
+ </Card>
56
+ )
57
+ }
@@ -0,0 +1,118 @@
1
+ import { Avatar, Card, CardContent, cn } from '@meith/ui'
2
+ import type { MemberProfileModel } from '@meith/theme-kit'
3
+
4
+ import { BUTTON, ClubBar, HEADING, MICRO, NUMERIC, PAGE_BODY, PanelHead, Stamp } from '../shared'
5
+
6
+ export function MemberProfile({
7
+ user,
8
+ avatarUrl,
9
+ title,
10
+ joinedAt,
11
+ lastVisitAt,
12
+ postCount,
13
+ signatureHtml,
14
+ fields,
15
+ actions,
16
+ regions,
17
+ }: MemberProfileModel) {
18
+ return (
19
+ <div className={PAGE_BODY}>
20
+ <Card className="flex items-stretch">
21
+ <ClubBar />
22
+
23
+ <div className="min-w-0 flex-1">
24
+ <CardContent className="flex flex-col gap-4 px-4 py-4 sm:flex-row sm:items-center sm:justify-between">
25
+ <div className="flex min-w-0 items-center gap-3">
26
+ <Avatar
27
+ src={avatarUrl}
28
+ name={user.username}
29
+ size={56}
30
+ className="rounded-sm border-border"
31
+ />
32
+
33
+ <div className="min-w-0">
34
+ <h1 className={cn(HEADING, 'text-xl break-words', user.nameClass)}>
35
+ {user.username}
36
+ </h1>
37
+ {title !== null && <p className={`${MICRO} mt-0.5`}>{title}</p>}
38
+ </div>
39
+ </div>
40
+
41
+ {actions.length > 0 && (
42
+ <nav
43
+ aria-label="Member actions"
44
+ className="flex shrink-0 flex-wrap items-center gap-2"
45
+ >
46
+ {actions.map((action, index) => (
47
+ <a
48
+ key={action.href}
49
+ href={action.href}
50
+ className={cn(
51
+ BUTTON,
52
+ index === 0
53
+ ? 'bg-primary text-primary-foreground hover:bg-primary-hover'
54
+ : 'border border-border text-muted-foreground hover:bg-accent hover:text-foreground',
55
+ )}
56
+ >
57
+ {action.label}
58
+ </a>
59
+ ))}
60
+ </nav>
61
+ )}
62
+ </CardContent>
63
+
64
+ <dl className="flex flex-wrap gap-x-6 gap-y-1 border-t-2 border-t-secondary bg-surface px-4 py-2.5 text-xs">
65
+ <div className="flex items-baseline gap-1.5">
66
+ <dt className={MICRO}>Posts</dt>
67
+ <dd className={`${NUMERIC} font-semibold text-foreground`}>
68
+ {postCount.toLocaleString('en')}
69
+ </dd>
70
+ </div>
71
+ <div className="flex items-baseline gap-1.5">
72
+ <dt className={MICRO}>Joined</dt>
73
+ <dd className="font-semibold text-foreground">
74
+ <Stamp at={joinedAt} />
75
+ </dd>
76
+ </div>
77
+ <div className="flex items-baseline gap-1.5">
78
+ <dt className={MICRO}>Last visit</dt>
79
+ <dd className="font-semibold text-foreground">
80
+ {lastVisitAt === null ? 'Never' : <Stamp at={lastVisitAt} />}
81
+ </dd>
82
+ </div>
83
+ </dl>
84
+ </div>
85
+ </Card>
86
+
87
+ {fields.length > 0 && (
88
+ <Card aria-labelledby="profile-fields-heading">
89
+ <PanelHead id="profile-fields-heading" title="About" />
90
+ <CardContent className="px-4 py-3">
91
+ <dl className="grid gap-x-6 gap-y-2.5 sm:grid-cols-2">
92
+ {fields.map((field) => (
93
+ <div key={field.label}>
94
+ <dt className={MICRO}>{field.label}</dt>
95
+ <dd className="text-sm break-words">{field.value}</dd>
96
+ </div>
97
+ ))}
98
+ </dl>
99
+ </CardContent>
100
+ </Card>
101
+ )}
102
+
103
+ {signatureHtml !== null && (
104
+ <Card aria-labelledby="profile-signature-heading">
105
+ <PanelHead id="profile-signature-heading" title="Signature" />
106
+ <CardContent className="px-4 py-3">
107
+ <div
108
+ className="prose-md text-sm text-muted-foreground"
109
+ dangerouslySetInnerHTML={{ __html: signatureHtml }}
110
+ />
111
+ </CardContent>
112
+ </Card>
113
+ )}
114
+
115
+ {regions?.plugins}
116
+ </div>
117
+ )
118
+ }
@@ -0,0 +1,40 @@
1
+ import type { NavigationModel } from '@meith/theme-kit'
2
+
3
+ import { MICRO, MUTED_LINK, PAGE } from '../shared'
4
+
5
+ export function Navigation({ items }: NavigationModel) {
6
+ if (items.length === 0) return null
7
+
8
+ return (
9
+ <nav aria-label="Breadcrumb" className="border-b border-border bg-card">
10
+ <ol
11
+ className={`${PAGE} flex items-center gap-2 overflow-x-auto py-2 whitespace-nowrap ${MICRO}`}
12
+ >
13
+ {items.map((item, index) => {
14
+ const isLast = index === items.length - 1
15
+ return (
16
+ <li key={item.href} className="flex shrink-0 items-center gap-2">
17
+ {index > 0 && (
18
+ <span aria-hidden="true" className="text-border select-none">
19
+ /
20
+ </span>
21
+ )}
22
+ {isLast ? (
23
+ <span
24
+ aria-current="page"
25
+ className="max-w-[26ch] truncate text-foreground sm:max-w-none"
26
+ >
27
+ {item.label}
28
+ </span>
29
+ ) : (
30
+ <a href={item.href} className={MUTED_LINK}>
31
+ {item.label}
32
+ </a>
33
+ )}
34
+ </li>
35
+ )
36
+ })}
37
+ </ol>
38
+ </nav>
39
+ )
40
+ }
@@ -0,0 +1,27 @@
1
+ import { Alert, AlertDescription, AlertTitle } from '@meith/ui'
2
+ import type { NoticeModel } from '@meith/theme-kit'
3
+
4
+ import { MICRO, MUTED_LINK } from '../shared'
5
+
6
+ const KIND_LABELS: Record<NoticeModel['kind'], string> = {
7
+ info: 'Notice',
8
+ success: 'Done',
9
+ warning: 'Warning',
10
+ error: 'Error',
11
+ }
12
+
13
+ export function Notice({ kind, message, dismissHref }: NoticeModel) {
14
+ return (
15
+ <Alert tone={kind}>
16
+ <AlertDescription>
17
+ <AlertTitle className={MICRO}>{KIND_LABELS[kind]}</AlertTitle> {message}
18
+ </AlertDescription>
19
+
20
+ {dismissHref !== null && (
21
+ <a href={dismissHref} className={`shrink-0 text-xs ${MUTED_LINK}`}>
22
+ Dismiss
23
+ </a>
24
+ )}
25
+ </Alert>
26
+ )
27
+ }
@@ -0,0 +1,74 @@
1
+ import { cn } from '@meith/ui'
2
+ import type { PaginationModel } from '@meith/theme-kit'
3
+
4
+ import { BUTTON, MICRO, NUMERIC } from '../shared'
5
+
6
+ export function Pagination({ page, pageCount, pages, previousHref, nextHref }: PaginationModel) {
7
+ if (pageCount <= 1 && previousHref === null && nextHref === null) return null
8
+
9
+ const step = cn(BUTTON, 'min-w-20 justify-center border border-border bg-card hover:bg-accent')
10
+ const stepDisabled = cn(step, 'pointer-events-none opacity-40')
11
+
12
+ return (
13
+ <nav aria-label="Pagination" className="flex flex-wrap items-center justify-between gap-3">
14
+ {previousHref === null ? (
15
+ <span className={stepDisabled} aria-hidden="true">
16
+ Previous
17
+ </span>
18
+ ) : (
19
+ <a href={previousHref} rel="prev" className={step}>
20
+ Previous
21
+ </a>
22
+ )}
23
+
24
+ <ol className="flex items-center gap-1">
25
+ {pages.map((entry, index) => {
26
+ const previous = pages[index - 1]
27
+ const gap = previous !== undefined && entry.page - previous.page > 1
28
+
29
+ return (
30
+ <li key={entry.page} className="flex items-center gap-1">
31
+ {gap && (
32
+ <span aria-hidden="true" className="px-1 text-sm text-muted-foreground">
33
+
34
+ </span>
35
+ )}
36
+ <a
37
+ href={entry.href}
38
+ aria-current={entry.isCurrent ? 'page' : undefined}
39
+ aria-label={`Page ${entry.page}`}
40
+ className={cn(
41
+ BUTTON,
42
+ 'min-w-8 justify-center px-2.5',
43
+ NUMERIC,
44
+ entry.isCurrent
45
+ ? 'bg-primary text-primary-foreground'
46
+ : 'hidden text-muted-foreground hover:bg-accent hover:text-foreground sm:inline-flex',
47
+ )}
48
+ >
49
+ {entry.page}
50
+ </a>
51
+ </li>
52
+ )
53
+ })}
54
+
55
+ <li className={`ml-2 whitespace-nowrap ${MICRO}`}>
56
+ <span className="sr-only">Page </span>
57
+ <span className={NUMERIC}>
58
+ {page} of {pageCount}
59
+ </span>
60
+ </li>
61
+ </ol>
62
+
63
+ {nextHref === null ? (
64
+ <span className={stepDisabled} aria-hidden="true">
65
+ Next
66
+ </span>
67
+ ) : (
68
+ <a href={nextHref} rel="next" className={step}>
69
+ Next
70
+ </a>
71
+ )}
72
+ </nav>
73
+ )
74
+ }
@@ -0,0 +1,55 @@
1
+ import { Separator, cn } from '@meith/ui'
2
+ import type { PostActionsSlotModel } from '@meith/theme-kit'
3
+
4
+ import { BUTTON, isEmptyRegion } from '../shared'
5
+
6
+ interface Action {
7
+ readonly href: string
8
+ readonly label: string
9
+ }
10
+
11
+ const ACTION = cn(BUTTON, 'h-7 px-2.5 text-muted-foreground hover:bg-accent hover:text-foreground')
12
+
13
+ export function PostActions({ actions, children }: PostActionsSlotModel) {
14
+ const reader: Action[] = [
15
+ actions.quoteHref === null ? null : { href: actions.quoteHref, label: 'Quote' },
16
+ actions.editHref === null ? null : { href: actions.editHref, label: 'Edit' },
17
+ actions.rateHref === null ? null : { href: actions.rateHref, label: 'Rate' },
18
+ actions.reportHref === null ? null : { href: actions.reportHref, label: 'Report' },
19
+ ].filter((action): action is Action => action !== null)
20
+
21
+ const staff: Action[] = [
22
+ actions.restoreHref === null ? null : { href: actions.restoreHref, label: 'Restore' },
23
+ actions.warnHref === null ? null : { href: actions.warnHref, label: 'Warn' },
24
+ actions.moderateHref === null ? null : { href: actions.moderateHref, label: 'Moderate' },
25
+ ].filter((action): action is Action => action !== null)
26
+
27
+ const extra = isEmptyRegion(children) ? null : children
28
+
29
+ if (reader.length === 0 && staff.length === 0 && extra === null) return null
30
+
31
+ return (
32
+ <nav
33
+ aria-label="Post actions"
34
+ className="flex flex-wrap items-center gap-1 border-t border-border bg-surface px-3 py-1.5 empty:hidden"
35
+ >
36
+ {reader.map((action) => (
37
+ <a key={action.href} href={action.href} className={ACTION}>
38
+ {action.label}
39
+ </a>
40
+ ))}
41
+
42
+ {reader.length > 0 && staff.length > 0 && (
43
+ <Separator orientation="vertical" className="mx-1.5" />
44
+ )}
45
+
46
+ {staff.map((action) => (
47
+ <a key={action.href} href={action.href} className={ACTION}>
48
+ {action.label}
49
+ </a>
50
+ ))}
51
+
52
+ {extra}
53
+ </nav>
54
+ )
55
+ }