@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.
Files changed (41) hide show
  1. package/package.json +3 -3
  2. package/src/copy.ts +316 -0
  3. package/src/index.ts +6 -6
  4. package/src/messages/en.json +189 -0
  5. package/src/messages/index.ts +5 -0
  6. package/src/panel-icons.tsx +1 -2
  7. package/src/shared.tsx +4 -6
  8. package/src/slots/announcement.tsx +15 -9
  9. package/src/slots/auth-page.tsx +3 -1
  10. package/src/slots/board-index.tsx +11 -4
  11. package/src/slots/board-stats.tsx +14 -12
  12. package/src/slots/category-block.tsx +1 -1
  13. package/src/slots/discovery-view.tsx +16 -15
  14. package/src/slots/error-notice.tsx +17 -9
  15. package/src/slots/footer.tsx +15 -4
  16. package/src/slots/forum-display.tsx +25 -20
  17. package/src/slots/forum-jump.tsx +1 -1
  18. package/src/slots/forum-row.tsx +16 -15
  19. package/src/slots/header.tsx +45 -5
  20. package/src/slots/latest-posts.tsx +21 -12
  21. package/src/slots/latest-threads.tsx +27 -16
  22. package/src/slots/member-profile.tsx +19 -15
  23. package/src/slots/navigation.tsx +7 -4
  24. package/src/slots/notice.tsx +12 -9
  25. package/src/slots/pagination.tsx +16 -10
  26. package/src/slots/panel-nav.tsx +37 -16
  27. package/src/slots/panel-page.tsx +15 -3
  28. package/src/slots/panel-shell.tsx +1 -7
  29. package/src/slots/post-actions.tsx +18 -11
  30. package/src/slots/post-bit.tsx +42 -20
  31. package/src/slots/post-form.tsx +7 -3
  32. package/src/slots/redirect-notice.tsx +16 -7
  33. package/src/slots/search-form.tsx +13 -9
  34. package/src/slots/search-results.tsx +27 -26
  35. package/src/slots/shell.tsx +4 -3
  36. package/src/slots/subforum-list.tsx +12 -9
  37. package/src/slots/thread-row.tsx +19 -18
  38. package/src/slots/thread-view.tsx +23 -13
  39. package/src/slots/user-panel.tsx +19 -14
  40. package/src/slots/who-is-online.tsx +35 -19
  41. 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({ markAllReadAction, regions }: BoardIndexModel) {
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
- Mark all forums read
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="Board activity" className="flex min-w-0 flex-col gap-4">
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
- }: BoardStatsModel) {
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
- Board statistics
22
+ {c('heading')}
19
23
  </h2>
20
24
 
21
25
  {computedAt === null ? (
22
- <span>The board&rsquo;s totals are rolled up on a schedule, and the first run has not happened.</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
- newest member <UserRef user={newestMember} className="text-foreground" />
42
+ {c('newestMember')} <UserRef user={newestMember} className="text-foreground" />
41
43
  </span>
42
44
  )}
43
45
 
44
46
  <span className={`ms-auto ${NUMERIC}`}>
45
- Counted <Stamp at={computedAt} />
47
+ {c('counted')} <Stamp at={computedAt} />
46
48
  </span>
47
49
  </>
48
50
  )}
@@ -1,5 +1,5 @@
1
- import { Card, CardDescription, CardHeader, CardRows, CardTitle } from '@meith/ui'
2
1
  import type { CategoryBlockModel } from '@meith/theme-kit'
2
+ import { Card, CardDescription, CardHeader, CardRows, CardTitle } from '@meith/ui'
3
3
 
4
4
  import { LINK } from '../shared'
5
5
 
@@ -1,7 +1,8 @@
1
- import { Card, CardRows, Empty, EmptyDescription, cn } from '@meith/ui'
2
- import type { DiscoveryViewModel, TabModel } from '@meith/theme-kit'
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, Stamp, pageAt } from '../shared'
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
- }: DiscoveryViewModel) {
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
- · started by {row.authorUsername}
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' : 'replies'}
105
+ {row.replyCount.value === 1 ? c('reply.one') : c('reply.other')}
105
106
  <span className="block">
106
- last post <Stamp at={row.lastPostAt} />
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 { Card, CardContent, CardFooter, buttonVariants } from '@meith/ui'
2
- import type { ErrorNoticeModel } from '@meith/theme-kit'
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({ status, title, message, homeHref, requestId }: ErrorNoticeModel) {
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
- Error {status}
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
- Forum home
27
+ {c('forumHome')}
20
28
  </a>
21
29
  </CardContent>
22
30
 
23
31
  {requestId !== null && (
24
32
  <CardFooter>
25
33
  <span>
26
- Quote this if you report it:{' '}
34
+ {c('quoteThis')}{' '}
27
35
  <code className="font-mono text-foreground select-all">{requestId}</code>
28
36
  </span>
29
37
  </CardFooter>
@@ -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({ boardTitle, links, timezoneLabel, poweredBy }: FooterModel) {
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="Footer" className="flex flex-wrap gap-x-4 gap-y-1">
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>Times are shown in {timezoneLabel}</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, PAGE_BODY, isEmptyRegion } from '../shared'
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: 'Threads',
37
+ label: c('threadsLabel'),
31
38
  value: forum.threadCount,
32
- one: 'thread',
33
- many: 'threads',
39
+ one: c('thread.one'),
40
+ many: c('thread.other'),
34
41
  },
35
42
  {
36
- label: 'Posts',
43
+ label: c('postsLabel'),
37
44
  value: forum.postCount,
38
- one: 'post',
39
- many: 'posts',
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
- Mark read
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
- New thread
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>No threads here yet</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
- Start the first thread
89
+ {c('startFirstThread')}
85
90
  </a>
86
91
  </EmptyAction>
87
92
  )}
@@ -1,5 +1,5 @@
1
- import { Label, NativeSelect, buttonVariants } from '@meith/ui'
2
1
  import type { ForumJumpModel } from '@meith/theme-kit'
2
+ import { buttonVariants, Label, NativeSelect } from '@meith/ui'
3
3
 
4
4
  import { PAGE } from '../shared'
5
5
 
@@ -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"> (new posts)</span>}
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">Subforums</span>
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: 'Threads',
55
+ label: c('threadsLabel'),
53
56
  value: forum.threadCount,
54
- one: 'thread',
55
- many: 'threads',
57
+ one: c('thread.one'),
58
+ many: c('thread.other'),
56
59
  },
57
60
  {
58
- label: 'Posts',
61
+ label: c('postsLabel'),
59
62
  value: forum.postCount,
60
- one: 'post',
61
- many: 'posts',
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">No posts yet</span>
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
- <UserRef user={forum.lastPost.author} className="font-normal" />
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
  )}
@@ -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
- export function Header({ boardTitle, homeHref, navigation, logo, children }: HeaderModel) {
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="Board sections" className="border-t border-border bg-surface">
73
+ <nav aria-label={c('sections')} className="border-t border-border bg-surface">
42
74
  <div className={PAGE}>
43
- <ul className="-mx-4 flex items-stretch gap-1 overflow-x-auto px-4 text-sm [mask-image:linear-gradient(to_right,black_calc(100%-1.5rem),transparent)] sm:-mx-6 sm:px-6 sm:[mask-image:none]">
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 { Card, CardContent, CardHeader, CardTitle, Empty, EmptyDescription, EmptyTitle } from '@meith/ui'
2
- import type { LatestPostsModel } from '@meith/theme-kit'
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">Latest posts</CardTitle>
21
+ <CardTitle id="latest-posts-heading">{c('heading')}</CardTitle>
11
22
  <p className={`text-xs text-muted-foreground ${NUMERIC}`}>
12
- As of <Stamp at={capturedAt} />
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>Nothing said yet</EmptyTitle>
19
- <EmptyDescription>The newest posts you can see will appear here.</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 { Card, CardContent, CardHeader, CardTitle, Empty, EmptyDescription, EmptyTitle } from '@meith/ui'
2
- import type { LatestThreadsModel } from '@meith/theme-kit'
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({ threads, capturedAt }: LatestThreadsModel) {
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">Latest threads</CardTitle>
25
+ <CardTitle id="latest-threads-heading">{c('heading')}</CardTitle>
11
26
  <p className={`text-xs text-muted-foreground ${NUMERIC}`}>
12
- As of <Stamp at={capturedAt} />
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>Nothing started yet</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.toLocaleString('en')}{' '}
37
- {thread.replyCount === 1 ? 'reply' : 'replies'}
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
  ))}