@meith/theme-default 0.7.0 → 0.9.0
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.
- package/package.json +3 -3
- package/src/copy.ts +316 -0
- package/src/index.ts +6 -6
- package/src/messages/en.json +189 -0
- package/src/messages/index.ts +5 -0
- package/src/panel-icons.tsx +1 -2
- package/src/shared.tsx +4 -6
- package/src/slots/announcement.tsx +15 -9
- package/src/slots/auth-page.tsx +3 -1
- package/src/slots/board-index.tsx +11 -4
- package/src/slots/board-stats.tsx +14 -12
- package/src/slots/category-block.tsx +1 -1
- package/src/slots/discovery-view.tsx +16 -15
- package/src/slots/error-notice.tsx +17 -9
- package/src/slots/footer.tsx +15 -4
- package/src/slots/forum-display.tsx +25 -20
- package/src/slots/forum-jump.tsx +1 -1
- package/src/slots/forum-row.tsx +16 -15
- package/src/slots/header.tsx +45 -5
- package/src/slots/latest-posts.tsx +21 -12
- package/src/slots/latest-threads.tsx +27 -16
- package/src/slots/member-profile.tsx +19 -15
- package/src/slots/navigation.tsx +7 -4
- package/src/slots/notice.tsx +12 -9
- package/src/slots/pagination.tsx +16 -10
- package/src/slots/panel-nav.tsx +37 -16
- package/src/slots/panel-page.tsx +15 -3
- package/src/slots/panel-shell.tsx +1 -7
- package/src/slots/post-actions.tsx +18 -11
- package/src/slots/post-bit.tsx +42 -20
- package/src/slots/post-form.tsx +7 -3
- package/src/slots/redirect-notice.tsx +16 -7
- package/src/slots/search-form.tsx +13 -9
- package/src/slots/search-results.tsx +27 -26
- package/src/slots/shell.tsx +4 -3
- package/src/slots/subforum-list.tsx +12 -9
- package/src/slots/thread-row.tsx +19 -18
- package/src/slots/thread-view.tsx +23 -13
- package/src/slots/user-panel.tsx +19 -14
- package/src/slots/who-is-online.tsx +35 -19
- package/src/theme.ts +90 -19
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
import type { BoardIndexModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
1
3
|
import { buttonVariants } from '@meith/ui'
|
|
2
|
-
import type { BoardIndexModel } from '@meith/theme-kit'
|
|
3
4
|
|
|
4
5
|
import { PAGE_BODY } from '../shared'
|
|
5
6
|
|
|
6
|
-
export function BoardIndex({
|
|
7
|
+
export function BoardIndex({
|
|
8
|
+
markAllReadAction,
|
|
9
|
+
regions,
|
|
10
|
+
copy,
|
|
11
|
+
}: BoardIndexModel & { copy: SlotCopy }) {
|
|
7
12
|
const rail = (regions.latest ?? null) !== null
|
|
8
13
|
const footer = regions.stats !== null || regions.online !== null
|
|
9
14
|
|
|
15
|
+
const c = (key: string) => fromSlotCopy(copy, `default.boardIndex.${key}`)
|
|
16
|
+
|
|
10
17
|
return (
|
|
11
18
|
<div className={PAGE_BODY}>
|
|
12
19
|
{regions.announcements !== undefined && (
|
|
@@ -16,7 +23,7 @@ export function BoardIndex({ markAllReadAction, regions }: BoardIndexModel) {
|
|
|
16
23
|
{markAllReadAction !== null && (
|
|
17
24
|
<form action={markAllReadAction} method="post" className="self-end">
|
|
18
25
|
<button type="submit" className={buttonVariants({ variant: 'ghost', size: 'sm' })}>
|
|
19
|
-
|
|
26
|
+
{c('markAllRead')}
|
|
20
27
|
</button>
|
|
21
28
|
</form>
|
|
22
29
|
)}
|
|
@@ -31,7 +38,7 @@ export function BoardIndex({ markAllReadAction, regions }: BoardIndexModel) {
|
|
|
31
38
|
<div className="flex min-w-0 flex-col gap-4">{regions.categories}</div>
|
|
32
39
|
|
|
33
40
|
{rail && (
|
|
34
|
-
<aside aria-label=
|
|
41
|
+
<aside aria-label={c('boardActivity')} className="flex min-w-0 flex-col gap-4">
|
|
35
42
|
{regions.latest}
|
|
36
43
|
</aside>
|
|
37
44
|
)}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BoardStatsModel } from '@meith/theme-kit'
|
|
1
|
+
import type { BoardStatsModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import { NUMERIC, Stamp, UserRef } from '../shared'
|
|
4
5
|
|
|
@@ -8,41 +9,42 @@ export function BoardStats({
|
|
|
8
9
|
memberCount,
|
|
9
10
|
newestMember,
|
|
10
11
|
computedAt,
|
|
11
|
-
|
|
12
|
+
copy,
|
|
13
|
+
}: BoardStatsModel & { copy: SlotCopy }) {
|
|
14
|
+
const c = (key: string) => fromSlotCopy(copy, `default.boardStats.${key}`)
|
|
15
|
+
|
|
12
16
|
return (
|
|
13
17
|
<section
|
|
14
18
|
aria-labelledby="board-stats-heading"
|
|
15
19
|
className="flex flex-wrap items-baseline gap-x-3 gap-y-1"
|
|
16
20
|
>
|
|
17
21
|
<h2 id="board-stats-heading" className="sr-only">
|
|
18
|
-
|
|
22
|
+
{c('heading')}
|
|
19
23
|
</h2>
|
|
20
24
|
|
|
21
25
|
{computedAt === null ? (
|
|
22
|
-
<span>
|
|
26
|
+
<span>{c('notComputed')}</span>
|
|
23
27
|
) : (
|
|
24
28
|
<>
|
|
25
29
|
{[
|
|
26
|
-
{ label: 'threads', value: threadCount },
|
|
27
|
-
{ label: 'posts', value: postCount },
|
|
28
|
-
{ label: 'members', value: memberCount },
|
|
30
|
+
{ label: c('threads'), value: threadCount },
|
|
31
|
+
{ label: c('posts'), value: postCount },
|
|
32
|
+
{ label: c('members'), value: memberCount },
|
|
29
33
|
].map((figure) => (
|
|
30
34
|
<span key={figure.label}>
|
|
31
|
-
<span className={`font-medium text-foreground ${NUMERIC}`}>
|
|
32
|
-
{figure.value.toLocaleString('en')}
|
|
33
|
-
</span>{' '}
|
|
35
|
+
<span className={`font-medium text-foreground ${NUMERIC}`}>{figure.value.label}</span>{' '}
|
|
34
36
|
{figure.label}
|
|
35
37
|
</span>
|
|
36
38
|
))}
|
|
37
39
|
|
|
38
40
|
{newestMember !== null && (
|
|
39
41
|
<span>
|
|
40
|
-
|
|
42
|
+
{c('newestMember')} <UserRef user={newestMember} className="text-foreground" />
|
|
41
43
|
</span>
|
|
42
44
|
)}
|
|
43
45
|
|
|
44
46
|
<span className={`ms-auto ${NUMERIC}`}>
|
|
45
|
-
|
|
47
|
+
{c('counted')} <Stamp at={computedAt} />
|
|
46
48
|
</span>
|
|
47
49
|
</>
|
|
48
50
|
)}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { DiscoveryViewModel, SlotCopy, TabModel } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
|
+
import { Card, CardRows, cn, Empty, EmptyDescription } from '@meith/ui'
|
|
3
4
|
|
|
4
|
-
import { LINK, NUMERIC,
|
|
5
|
+
import { LINK, NUMERIC, pageAt, Stamp } from '../shared'
|
|
5
6
|
|
|
6
7
|
const TAB =
|
|
7
8
|
'inline-flex h-9 items-center rounded-full px-3.5 text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
@@ -43,7 +44,10 @@ export function DiscoveryView({
|
|
|
43
44
|
nextLabel,
|
|
44
45
|
emptyMessage,
|
|
45
46
|
refusal,
|
|
46
|
-
|
|
47
|
+
copy,
|
|
48
|
+
}: DiscoveryViewModel & { copy: SlotCopy }) {
|
|
49
|
+
const c = (key: string) => fromSlotCopy(copy, `default.discoveryView.${key}`)
|
|
50
|
+
|
|
47
51
|
return (
|
|
48
52
|
<main
|
|
49
53
|
id="board-content"
|
|
@@ -58,10 +62,7 @@ export function DiscoveryView({
|
|
|
58
62
|
<Tabs label={tabsLabel} tabs={tabs} />
|
|
59
63
|
|
|
60
64
|
{refusal !== null ? (
|
|
61
|
-
<p
|
|
62
|
-
role="alert"
|
|
63
|
-
className="rounded-md border border-border bg-muted px-3 py-2 text-sm"
|
|
64
|
-
>
|
|
65
|
+
<p role="alert" className="rounded-md border border-border bg-muted px-3 py-2 text-sm">
|
|
65
66
|
{refusal.message}{' '}
|
|
66
67
|
<a href={refusal.signInHref} className={`font-medium text-foreground ${LINK}`}>
|
|
67
68
|
{refusal.signInLabel}
|
|
@@ -89,22 +90,22 @@ export function DiscoveryView({
|
|
|
89
90
|
{row.title}
|
|
90
91
|
</a>
|
|
91
92
|
<p className="truncate text-xs text-muted-foreground">
|
|
92
|
-
in{' '}
|
|
93
|
+
{c('in')}{' '}
|
|
93
94
|
<a href={row.forum.href} className="font-medium hover:underline">
|
|
94
95
|
{row.forum.label}
|
|
95
96
|
</a>{' '}
|
|
96
|
-
|
|
97
|
+
{c('dot')} {c('startedBy')} {row.authorUsername}
|
|
97
98
|
</p>
|
|
98
99
|
</div>
|
|
99
100
|
|
|
100
101
|
<p className="shrink-0 text-xs text-muted-foreground sm:text-right">
|
|
101
102
|
<span className={cn('font-semibold text-foreground', NUMERIC)}>
|
|
102
|
-
{row.replyCount}
|
|
103
|
+
{row.replyCount.label}
|
|
103
104
|
</span>{' '}
|
|
104
|
-
{row.replyCount === 1 ? 'reply' : '
|
|
105
|
+
{row.replyCount.value === 1 ? c('reply.one') : c('reply.other')}
|
|
105
106
|
<span className="block">
|
|
106
|
-
|
|
107
|
-
{row.lastPostUsername === null ? null : ` by ${row.lastPostUsername}`}
|
|
107
|
+
{c('lastPost')} <Stamp at={row.lastPostAt} />
|
|
108
|
+
{row.lastPostUsername === null ? null : ` ${c('by')} ${row.lastPostUsername}`}
|
|
108
109
|
</span>
|
|
109
110
|
</p>
|
|
110
111
|
</li>
|
|
@@ -118,7 +119,7 @@ export function DiscoveryView({
|
|
|
118
119
|
href={nextHref}
|
|
119
120
|
className="inline-flex h-9 w-fit items-center rounded-full bg-secondary px-4 text-sm font-semibold text-secondary-foreground transition-colors hover:bg-accent"
|
|
120
121
|
>
|
|
121
|
-
{nextLabel}
|
|
122
|
+
{nextLabel} {c('nextArrow')}
|
|
122
123
|
</a>
|
|
123
124
|
)}
|
|
124
125
|
</main>
|
|
@@ -1,29 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { ErrorNoticeModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
|
+
import { buttonVariants, Card, CardContent, CardFooter } from '@meith/ui'
|
|
3
4
|
|
|
4
5
|
import { NUMERIC } from '../shared'
|
|
5
6
|
|
|
6
|
-
export function ErrorNotice({
|
|
7
|
+
export function ErrorNotice({
|
|
8
|
+
status,
|
|
9
|
+
title,
|
|
10
|
+
message,
|
|
11
|
+
homeHref,
|
|
12
|
+
requestId,
|
|
13
|
+
copy,
|
|
14
|
+
}: ErrorNoticeModel & { copy: SlotCopy }) {
|
|
15
|
+
const c = (key: string) => fromSlotCopy(copy, `default.errorNotice.${key}`)
|
|
16
|
+
|
|
7
17
|
return (
|
|
8
18
|
<Card className="w-full max-w-lg">
|
|
9
19
|
<CardContent className="p-6">
|
|
10
20
|
<p className={`text-xs font-medium tracking-wide text-destructive uppercase ${NUMERIC}`}>
|
|
11
|
-
|
|
21
|
+
{c('error')} {status}
|
|
12
22
|
</p>
|
|
13
|
-
<h1 className="mt-1 text-2xl font-semibold tracking-tight text-balance">
|
|
14
|
-
{title}
|
|
15
|
-
</h1>
|
|
23
|
+
<h1 className="mt-1 text-2xl font-semibold tracking-tight text-balance">{title}</h1>
|
|
16
24
|
<p className="mt-2 text-sm text-muted-foreground">{message}</p>
|
|
17
25
|
|
|
18
26
|
<a href={homeHref} className={`mt-5 ${buttonVariants({ variant: 'primary' })}`}>
|
|
19
|
-
|
|
27
|
+
{c('forumHome')}
|
|
20
28
|
</a>
|
|
21
29
|
</CardContent>
|
|
22
30
|
|
|
23
31
|
{requestId !== null && (
|
|
24
32
|
<CardFooter>
|
|
25
33
|
<span>
|
|
26
|
-
|
|
34
|
+
{c('quoteThis')}{' '}
|
|
27
35
|
<code className="font-mono text-foreground select-all">{requestId}</code>
|
|
28
36
|
</span>
|
|
29
37
|
</CardFooter>
|
package/src/slots/footer.tsx
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import type { FooterModel } from '@meith/theme-kit'
|
|
1
|
+
import type { FooterModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import { MUTED_LINK, PAGE } from '../shared'
|
|
4
5
|
|
|
5
|
-
export function Footer({
|
|
6
|
+
export function Footer({
|
|
7
|
+
boardTitle,
|
|
8
|
+
links,
|
|
9
|
+
timezoneLabel,
|
|
10
|
+
poweredBy,
|
|
11
|
+
copy,
|
|
12
|
+
}: FooterModel & { copy: SlotCopy }) {
|
|
13
|
+
const c = (key: string) => fromSlotCopy(copy, `default.footer.${key}`)
|
|
14
|
+
|
|
6
15
|
return (
|
|
7
16
|
<footer className="mt-auto border-t border-border bg-card">
|
|
8
17
|
<div
|
|
@@ -11,7 +20,7 @@ export function Footer({ boardTitle, links, timezoneLabel, poweredBy }: FooterMo
|
|
|
11
20
|
<span className="font-medium text-foreground">{boardTitle}</span>
|
|
12
21
|
|
|
13
22
|
{links.length > 0 && (
|
|
14
|
-
<nav aria-label=
|
|
23
|
+
<nav aria-label={c('nav')} className="flex flex-wrap gap-x-4 gap-y-1">
|
|
15
24
|
{links.map((link) => (
|
|
16
25
|
<a key={link.href} href={link.href} className={MUTED_LINK}>
|
|
17
26
|
{link.label}
|
|
@@ -21,7 +30,9 @@ export function Footer({ boardTitle, links, timezoneLabel, poweredBy }: FooterMo
|
|
|
21
30
|
)}
|
|
22
31
|
|
|
23
32
|
<span className="flex flex-wrap items-center gap-x-3 gap-y-1 sm:justify-end sm:text-right">
|
|
24
|
-
<span>
|
|
33
|
+
<span>
|
|
34
|
+
{c('timesShownIn')} {timezoneLabel}
|
|
35
|
+
</span>
|
|
25
36
|
{poweredBy && (
|
|
26
37
|
<a href={poweredBy.href} className={MUTED_LINK}>
|
|
27
38
|
{poweredBy.label}
|
|
@@ -1,24 +1,31 @@
|
|
|
1
|
+
import type { ForumDisplayModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
1
3
|
import {
|
|
4
|
+
buttonVariants,
|
|
2
5
|
Card,
|
|
3
6
|
CardRows,
|
|
4
7
|
Empty,
|
|
5
8
|
EmptyAction,
|
|
6
9
|
EmptyDescription,
|
|
7
10
|
EmptyTitle,
|
|
8
|
-
buttonVariants,
|
|
9
11
|
} from '@meith/ui'
|
|
10
|
-
import type { ForumDisplayModel } from '@meith/theme-kit'
|
|
11
12
|
|
|
12
|
-
import { Counts,
|
|
13
|
+
import { Counts, isEmptyRegion, PAGE_BODY } from '../shared'
|
|
14
|
+
|
|
15
|
+
export function ForumDisplay({
|
|
16
|
+
forum,
|
|
17
|
+
newThreadHref,
|
|
18
|
+
markReadAction,
|
|
19
|
+
regions,
|
|
20
|
+
copy,
|
|
21
|
+
}: ForumDisplayModel & { copy: SlotCopy }) {
|
|
22
|
+
const c = (key: string) => fromSlotCopy(copy, `default.forumDisplay.${key}`)
|
|
13
23
|
|
|
14
|
-
export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }: ForumDisplayModel) {
|
|
15
24
|
return (
|
|
16
25
|
<div className={PAGE_BODY}>
|
|
17
26
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
|
|
18
27
|
<div className="min-w-0">
|
|
19
|
-
<h1 className="text-2xl font-semibold tracking-tight text-balance">
|
|
20
|
-
{forum.title}
|
|
21
|
-
</h1>
|
|
28
|
+
<h1 className="text-2xl font-semibold tracking-tight text-balance">{forum.title}</h1>
|
|
22
29
|
{forum.description !== null && (
|
|
23
30
|
<p className="mt-1 max-w-prose text-sm text-muted-foreground">{forum.description}</p>
|
|
24
31
|
)}
|
|
@@ -27,16 +34,16 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
|
|
|
27
34
|
className="mt-2"
|
|
28
35
|
items={[
|
|
29
36
|
{
|
|
30
|
-
label: '
|
|
37
|
+
label: c('threadsLabel'),
|
|
31
38
|
value: forum.threadCount,
|
|
32
|
-
one: 'thread',
|
|
33
|
-
many: '
|
|
39
|
+
one: c('thread.one'),
|
|
40
|
+
many: c('thread.other'),
|
|
34
41
|
},
|
|
35
42
|
{
|
|
36
|
-
label: '
|
|
43
|
+
label: c('postsLabel'),
|
|
37
44
|
value: forum.postCount,
|
|
38
|
-
one: 'post',
|
|
39
|
-
many: '
|
|
45
|
+
one: c('post.one'),
|
|
46
|
+
many: c('post.other'),
|
|
40
47
|
},
|
|
41
48
|
]}
|
|
42
49
|
/>
|
|
@@ -47,13 +54,13 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
|
|
|
47
54
|
{markReadAction !== null && (
|
|
48
55
|
<form action={markReadAction} method="post">
|
|
49
56
|
<button type="submit" className={buttonVariants({ variant: 'ghost' })}>
|
|
50
|
-
|
|
57
|
+
{c('markRead')}
|
|
51
58
|
</button>
|
|
52
59
|
</form>
|
|
53
60
|
)}
|
|
54
61
|
{newThreadHref !== null && (
|
|
55
62
|
<a href={newThreadHref} className={buttonVariants({ variant: 'primary' })}>
|
|
56
|
-
|
|
63
|
+
{c('newThread')}
|
|
57
64
|
</a>
|
|
58
65
|
)}
|
|
59
66
|
</div>
|
|
@@ -72,16 +79,14 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
|
|
|
72
79
|
<Card>
|
|
73
80
|
{isEmptyRegion(regions.threads) ? (
|
|
74
81
|
<Empty>
|
|
75
|
-
<EmptyTitle>
|
|
82
|
+
<EmptyTitle>{c('noThreadsYet')}</EmptyTitle>
|
|
76
83
|
<EmptyDescription>
|
|
77
|
-
{newThreadHref === null
|
|
78
|
-
? 'Nothing has been posted in this forum.'
|
|
79
|
-
: 'Nothing has been posted in this forum. Yours would be the first.'}
|
|
84
|
+
{newThreadHref === null ? c('emptyNoThread') : c('emptyNoThreadFirst')}
|
|
80
85
|
</EmptyDescription>
|
|
81
86
|
{newThreadHref !== null && (
|
|
82
87
|
<EmptyAction>
|
|
83
88
|
<a href={newThreadHref} className={buttonVariants({ variant: 'primary' })}>
|
|
84
|
-
|
|
89
|
+
{c('startFirstThread')}
|
|
85
90
|
</a>
|
|
86
91
|
</EmptyAction>
|
|
87
92
|
)}
|
package/src/slots/forum-jump.tsx
CHANGED
package/src/slots/forum-row.tsx
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { ForumRowSlotModel } from '@meith/theme-kit'
|
|
1
|
+
import type { ForumRowSlotModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import { Counts, LINK, MUTED_LINK, ReadSpacer, Stamp, UnreadDot, UserRef } from '../shared'
|
|
4
5
|
|
|
5
|
-
export function ForumRow({ forum }: ForumRowSlotModel) {
|
|
6
|
+
export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }) {
|
|
6
7
|
const isLink = forum.type === 'link'
|
|
7
8
|
|
|
9
|
+
const c = (key: string) => fromSlotCopy(copy, `default.forumRow.${key}`)
|
|
10
|
+
|
|
8
11
|
return (
|
|
9
12
|
<li
|
|
10
13
|
data-unread={forum.isUnread ? '' : undefined}
|
|
@@ -25,7 +28,7 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
|
|
|
25
28
|
>
|
|
26
29
|
{forum.title}
|
|
27
30
|
</a>
|
|
28
|
-
{forum.isUnread && <span className="sr-only"> (
|
|
31
|
+
{forum.isUnread && <span className="sr-only"> {c('newPosts')}</span>}
|
|
29
32
|
|
|
30
33
|
{forum.description !== null && (
|
|
31
34
|
<p className="mt-0.5 text-sm text-muted-foreground">{forum.description}</p>
|
|
@@ -33,7 +36,7 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
|
|
|
33
36
|
|
|
34
37
|
{forum.subforums.length > 0 && (
|
|
35
38
|
<p className="mt-1.5 flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs text-muted-foreground">
|
|
36
|
-
<span className="font-medium text-foreground">
|
|
39
|
+
<span className="font-medium text-foreground">{c('subforums')}</span>
|
|
37
40
|
{forum.subforums.map((sub) => (
|
|
38
41
|
<a key={sub.href} href={sub.href} className={MUTED_LINK}>
|
|
39
42
|
{sub.label}
|
|
@@ -49,23 +52,23 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
|
|
|
49
52
|
className="col-start-2 md:col-start-3 md:justify-end"
|
|
50
53
|
items={[
|
|
51
54
|
{
|
|
52
|
-
label: '
|
|
55
|
+
label: c('threadsLabel'),
|
|
53
56
|
value: forum.threadCount,
|
|
54
|
-
one: 'thread',
|
|
55
|
-
many: '
|
|
57
|
+
one: c('thread.one'),
|
|
58
|
+
many: c('thread.other'),
|
|
56
59
|
},
|
|
57
60
|
{
|
|
58
|
-
label: '
|
|
61
|
+
label: c('postsLabel'),
|
|
59
62
|
value: forum.postCount,
|
|
60
|
-
one: 'post',
|
|
61
|
-
many: '
|
|
63
|
+
one: c('post.one'),
|
|
64
|
+
many: c('post.other'),
|
|
62
65
|
},
|
|
63
66
|
]}
|
|
64
67
|
/>
|
|
65
68
|
|
|
66
69
|
<div className="col-start-2 min-w-0 text-xs text-muted-foreground md:col-start-4">
|
|
67
70
|
{forum.lastPost === null ? (
|
|
68
|
-
<span className="text-forum-read">
|
|
71
|
+
<span className="text-forum-read">{c('noPostsYet')}</span>
|
|
69
72
|
) : (
|
|
70
73
|
<>
|
|
71
74
|
<a
|
|
@@ -75,10 +78,8 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
|
|
|
75
78
|
{forum.lastPost.threadTitle}
|
|
76
79
|
</a>
|
|
77
80
|
<span className="mt-0.5 block truncate">
|
|
78
|
-
{'by '}
|
|
79
|
-
<
|
|
80
|
-
{' · '}
|
|
81
|
-
<Stamp at={forum.lastPost.at} />
|
|
81
|
+
{c('by')} <UserRef user={forum.lastPost.author} className="font-normal" />{' '}
|
|
82
|
+
{c('dot')} <Stamp at={forum.lastPost.at} />
|
|
82
83
|
</span>
|
|
83
84
|
</>
|
|
84
85
|
)}
|
package/src/slots/header.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { HeaderModel } from '@meith/theme-kit'
|
|
1
|
+
import type { HeaderModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy, linkTarget } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import { LINK, PAGE } from '../shared'
|
|
4
5
|
|
|
@@ -24,7 +25,38 @@ function BoardMark({ boardTitle, logo }: Pick<HeaderModel, 'boardTitle' | 'logo'
|
|
|
24
25
|
)
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'] }) {
|
|
29
|
+
if (items === undefined || items.length === 0) return null
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<ul className="invisible absolute top-full left-0 z-30 min-w-44 rounded-b-md border border-border bg-card py-1 opacity-0 shadow-elevation transition-opacity group-focus-within:visible group-focus-within:opacity-100 group-hover:visible group-hover:opacity-100">
|
|
33
|
+
{items.map((child) => (
|
|
34
|
+
<li key={child.href}>
|
|
35
|
+
<a
|
|
36
|
+
href={child.href}
|
|
37
|
+
{...linkTarget(child)}
|
|
38
|
+
className="block px-3 py-1.5 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
39
|
+
>
|
|
40
|
+
{child.label}
|
|
41
|
+
</a>
|
|
42
|
+
</li>
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function Header({
|
|
49
|
+
boardTitle,
|
|
50
|
+
homeHref,
|
|
51
|
+
navigation,
|
|
52
|
+
logo,
|
|
53
|
+
children,
|
|
54
|
+
copy,
|
|
55
|
+
}: HeaderModel & { copy: SlotCopy }) {
|
|
56
|
+
const c = (key: string) => fromSlotCopy(copy, `default.header.${key}`)
|
|
57
|
+
|
|
58
|
+
const opensMenus = navigation.some((item) => item.submenu !== undefined)
|
|
59
|
+
|
|
28
60
|
return (
|
|
29
61
|
<header className="border-b border-border bg-card">
|
|
30
62
|
<div className={`${PAGE} flex flex-wrap items-center justify-between gap-x-6 gap-y-3 py-3`}>
|
|
@@ -38,17 +70,25 @@ export function Header({ boardTitle, homeHref, navigation, logo, children }: Hea
|
|
|
38
70
|
</div>
|
|
39
71
|
|
|
40
72
|
{navigation.length > 0 && (
|
|
41
|
-
<nav aria-label=
|
|
73
|
+
<nav aria-label={c('sections')} className="border-t border-border bg-surface">
|
|
42
74
|
<div className={PAGE}>
|
|
43
|
-
<ul
|
|
75
|
+
<ul
|
|
76
|
+
className={`-mx-4 flex items-stretch gap-1 px-4 text-sm sm:-mx-6 sm:px-6 ${
|
|
77
|
+
opensMenus
|
|
78
|
+
? 'flex-wrap'
|
|
79
|
+
: 'overflow-x-auto [mask-image:linear-gradient(to_right,black_calc(100%-1.5rem),transparent)] sm:[mask-image:none]'
|
|
80
|
+
}`}
|
|
81
|
+
>
|
|
44
82
|
{navigation.map((item) => (
|
|
45
|
-
<li key={item.href} className="shrink-0">
|
|
83
|
+
<li key={item.href} className="group relative shrink-0">
|
|
46
84
|
<a
|
|
47
85
|
href={item.href}
|
|
86
|
+
{...linkTarget(item)}
|
|
48
87
|
className="inline-flex h-10 items-center rounded-t-md border-b-2 border-transparent px-2.5 font-medium text-muted-foreground transition-colors hover:border-border hover:text-foreground"
|
|
49
88
|
>
|
|
50
89
|
{item.label}
|
|
51
90
|
</a>
|
|
91
|
+
<Submenu items={item.submenu} />
|
|
52
92
|
</li>
|
|
53
93
|
))}
|
|
54
94
|
</ul>
|
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { LatestPostsModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
Empty,
|
|
9
|
+
EmptyDescription,
|
|
10
|
+
EmptyTitle,
|
|
11
|
+
} from '@meith/ui'
|
|
3
12
|
|
|
4
13
|
import { LINK, MUTED_LINK, NUMERIC, Stamp, UserRef } from '../shared'
|
|
5
14
|
|
|
6
|
-
export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
|
|
15
|
+
export function LatestPosts({ posts, capturedAt, copy }: LatestPostsModel & { copy: SlotCopy }) {
|
|
16
|
+
const c = (key: string) => fromSlotCopy(copy, `default.latestPosts.${key}`)
|
|
17
|
+
|
|
7
18
|
return (
|
|
8
19
|
<Card aria-labelledby="latest-posts-heading">
|
|
9
20
|
<CardHeader>
|
|
10
|
-
<CardTitle id="latest-posts-heading">
|
|
21
|
+
<CardTitle id="latest-posts-heading">{c('heading')}</CardTitle>
|
|
11
22
|
<p className={`text-xs text-muted-foreground ${NUMERIC}`}>
|
|
12
|
-
|
|
23
|
+
{c('asOf')} <Stamp at={capturedAt} />
|
|
13
24
|
</p>
|
|
14
25
|
</CardHeader>
|
|
15
26
|
|
|
16
27
|
{posts.length === 0 ? (
|
|
17
28
|
<Empty className="py-6">
|
|
18
|
-
<EmptyTitle>
|
|
19
|
-
<EmptyDescription>
|
|
29
|
+
<EmptyTitle>{c('nothingYet')}</EmptyTitle>
|
|
30
|
+
<EmptyDescription>{c('emptyDescription')}</EmptyDescription>
|
|
20
31
|
</Empty>
|
|
21
32
|
) : (
|
|
22
33
|
<CardContent className="px-0 py-0">
|
|
@@ -35,13 +46,11 @@ export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
|
|
|
35
46
|
)}
|
|
36
47
|
|
|
37
48
|
<p className="truncate text-xs text-muted-foreground">
|
|
38
|
-
<UserRef user={post.author} className="font-normal" />
|
|
39
|
-
{' in '}
|
|
49
|
+
<UserRef user={post.author} className="font-normal" /> {c('in')}{' '}
|
|
40
50
|
<a href={post.forum.href} className={MUTED_LINK}>
|
|
41
51
|
{post.forum.label}
|
|
42
|
-
</a>
|
|
43
|
-
{'
|
|
44
|
-
<Stamp at={post.postedAt} />
|
|
52
|
+
</a>{' '}
|
|
53
|
+
{c('dot')} <Stamp at={post.postedAt} />
|
|
45
54
|
</p>
|
|
46
55
|
</li>
|
|
47
56
|
))}
|
|
@@ -1,24 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { LatestThreadsModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
Empty,
|
|
9
|
+
EmptyDescription,
|
|
10
|
+
EmptyTitle,
|
|
11
|
+
} from '@meith/ui'
|
|
3
12
|
|
|
4
13
|
import { LINK, MUTED_LINK, NUMERIC, Stamp, UserRef } from '../shared'
|
|
5
14
|
|
|
6
|
-
export function LatestThreads({
|
|
15
|
+
export function LatestThreads({
|
|
16
|
+
threads,
|
|
17
|
+
capturedAt,
|
|
18
|
+
copy,
|
|
19
|
+
}: LatestThreadsModel & { copy: SlotCopy }) {
|
|
20
|
+
const c = (key: string) => fromSlotCopy(copy, `default.latestThreads.${key}`)
|
|
21
|
+
|
|
7
22
|
return (
|
|
8
23
|
<Card aria-labelledby="latest-threads-heading">
|
|
9
24
|
<CardHeader>
|
|
10
|
-
<CardTitle id="latest-threads-heading">
|
|
25
|
+
<CardTitle id="latest-threads-heading">{c('heading')}</CardTitle>
|
|
11
26
|
<p className={`text-xs text-muted-foreground ${NUMERIC}`}>
|
|
12
|
-
|
|
27
|
+
{c('asOf')} <Stamp at={capturedAt} />
|
|
13
28
|
</p>
|
|
14
29
|
</CardHeader>
|
|
15
30
|
|
|
16
31
|
{threads.length === 0 ? (
|
|
17
32
|
<Empty className="py-6">
|
|
18
|
-
<EmptyTitle>
|
|
19
|
-
<EmptyDescription>
|
|
20
|
-
The newest threads you can see will appear here.
|
|
21
|
-
</EmptyDescription>
|
|
33
|
+
<EmptyTitle>{c('nothingYet')}</EmptyTitle>
|
|
34
|
+
<EmptyDescription>{c('emptyDescription')}</EmptyDescription>
|
|
22
35
|
</Empty>
|
|
23
36
|
) : (
|
|
24
37
|
<CardContent className="px-0 py-0">
|
|
@@ -33,19 +46,17 @@ export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
|
|
|
33
46
|
{thread.title}
|
|
34
47
|
</a>
|
|
35
48
|
<span className={`shrink-0 text-xs text-muted-foreground ${NUMERIC}`}>
|
|
36
|
-
{thread.replyCount.
|
|
37
|
-
{thread.replyCount === 1 ? 'reply' : '
|
|
49
|
+
{thread.replyCount.label}{' '}
|
|
50
|
+
{thread.replyCount.value === 1 ? c('reply.one') : c('reply.other')}
|
|
38
51
|
</span>
|
|
39
52
|
</div>
|
|
40
53
|
|
|
41
54
|
<p className="truncate text-xs text-muted-foreground">
|
|
42
|
-
<UserRef user={thread.author} className="font-normal" />
|
|
43
|
-
{' in '}
|
|
55
|
+
<UserRef user={thread.author} className="font-normal" /> {c('in')}{' '}
|
|
44
56
|
<a href={thread.forum.href} className={MUTED_LINK}>
|
|
45
57
|
{thread.forum.label}
|
|
46
|
-
</a>
|
|
47
|
-
{'
|
|
48
|
-
<Stamp at={thread.startedAt} />
|
|
58
|
+
</a>{' '}
|
|
59
|
+
{c('dot')} <Stamp at={thread.startedAt} />
|
|
49
60
|
</p>
|
|
50
61
|
</li>
|
|
51
62
|
))}
|