@meith/theme-midnight 0.7.0 → 0.8.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.
@@ -1,19 +1,22 @@
1
- import type { LatestPostsModel } from '@meith/theme-kit'
1
+ import type { LatestPostsModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { UserRef } from '../shared'
4
5
 
5
- export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
6
+ export function LatestPosts({ posts, capturedAt, copy }: LatestPostsModel & { copy: SlotCopy }) {
7
+ const c = (key: string) => fromSlotCopy(copy, `midnight.latestPosts.${key}`)
8
+
6
9
  return (
7
10
  <section aria-labelledby="latest-posts-heading" className="border border-border">
8
11
  <h2
9
12
  id="latest-posts-heading"
10
13
  className="border-b border-border bg-secondary px-3 py-1 font-mono text-xs uppercase tracking-wide"
11
14
  >
12
- Latest posts
15
+ {c('heading')}
13
16
  </h2>
14
17
 
15
18
  {posts.length === 0 ? (
16
- <p className="px-3 py-2 font-mono text-xs text-muted-foreground">nothing said yet</p>
19
+ <p className="px-3 py-2 font-mono text-xs text-muted-foreground">{c('empty')}</p>
17
20
  ) : (
18
21
  <ul className="divide-y divide-border font-mono text-xs">
19
22
  {posts.map((post) => (
@@ -39,7 +42,7 @@ export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
39
42
  )}
40
43
 
41
44
  <p className="border-t border-border px-3 py-1 font-mono text-xs text-muted-foreground">
42
- as of <time dateTime={capturedAt.iso}>{capturedAt.label}</time>
45
+ {c('asOf')} <time dateTime={capturedAt.iso}>{capturedAt.label}</time>
43
46
  </p>
44
47
  </section>
45
48
  )
@@ -1,19 +1,26 @@
1
- import type { LatestThreadsModel } from '@meith/theme-kit'
1
+ import type { LatestThreadsModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { UserRef } from '../shared'
4
5
 
5
- export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
6
+ export function LatestThreads({
7
+ threads,
8
+ capturedAt,
9
+ copy,
10
+ }: LatestThreadsModel & { copy: SlotCopy }) {
11
+ const c = (key: string) => fromSlotCopy(copy, `midnight.latestThreads.${key}`)
12
+
6
13
  return (
7
14
  <section aria-labelledby="latest-threads-heading" className="border border-border">
8
15
  <h2
9
16
  id="latest-threads-heading"
10
17
  className="border-b border-border bg-secondary px-3 py-1 font-mono text-xs uppercase tracking-wide"
11
18
  >
12
- Latest threads
19
+ {c('heading')}
13
20
  </h2>
14
21
 
15
22
  {threads.length === 0 ? (
16
- <p className="px-3 py-2 font-mono text-xs text-muted-foreground">nothing started yet</p>
23
+ <p className="px-3 py-2 font-mono text-xs text-muted-foreground">{c('empty')}</p>
17
24
  ) : (
18
25
  <ul className="divide-y divide-border font-mono text-xs">
19
26
  {threads.map((thread) => (
@@ -36,7 +43,7 @@ export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
36
43
  )}
37
44
 
38
45
  <p className="border-t border-border px-3 py-1 font-mono text-xs text-muted-foreground">
39
- as of <time dateTime={capturedAt.iso}>{capturedAt.label}</time>
46
+ {c('asOf')} <time dateTime={capturedAt.iso}>{capturedAt.label}</time>
40
47
  </p>
41
48
  </section>
42
49
  )
@@ -1,4 +1,5 @@
1
- import type { MemberProfileModel } from '@meith/theme-kit'
1
+ import type { MemberProfileModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  export function MemberProfile({
4
5
  user,
@@ -11,7 +12,10 @@ export function MemberProfile({
11
12
  fields,
12
13
  actions,
13
14
  regions,
14
- }: MemberProfileModel) {
15
+ copy,
16
+ }: MemberProfileModel & { copy: SlotCopy }) {
17
+ const c = (key: string) => fromSlotCopy(copy, `midnight.memberProfile.${key}`)
18
+
15
19
  return (
16
20
  <article className="flex flex-col gap-3 p-3">
17
21
  <header className="flex items-start gap-3 border border-border bg-secondary p-3">
@@ -38,24 +42,24 @@ export function MemberProfile({
38
42
 
39
43
  <dl className="grid grid-cols-2 gap-x-4 gap-y-1 border border-border p-3 font-mono text-xs sm:grid-cols-3">
40
44
  <div>
41
- <dt className="text-muted-foreground">joined</dt>
45
+ <dt className="text-muted-foreground">{c('joined')}</dt>
42
46
  <dd>
43
47
  <time dateTime={joinedAt.iso}>{joinedAt.label}</time>
44
48
  </dd>
45
49
  </div>
46
50
  <div>
47
- <dt className="text-muted-foreground">last visit</dt>
51
+ <dt className="text-muted-foreground">{c('lastVisit')}</dt>
48
52
  <dd>
49
53
  {lastVisitAt === null ? (
50
- 'never'
54
+ c('never')
51
55
  ) : (
52
56
  <time dateTime={lastVisitAt.iso}>{lastVisitAt.label}</time>
53
57
  )}
54
58
  </dd>
55
59
  </div>
56
60
  <div>
57
- <dt className="text-muted-foreground">posts</dt>
58
- <dd>{postCount}</dd>
61
+ <dt className="text-muted-foreground">{c('posts')}</dt>
62
+ <dd>{postCount.label}</dd>
59
63
  </div>
60
64
  {fields.map((field) => (
61
65
  <div key={field.label}>
@@ -66,7 +70,7 @@ export function MemberProfile({
66
70
  </dl>
67
71
 
68
72
  {signatureHtml !== null && (
69
- <section aria-label="Signature" className="border border-border p-3 text-sm">
73
+ <section aria-label={c('signatureLabel')} className="border border-border p-3 text-sm">
70
74
  <div className="prose-md" dangerouslySetInnerHTML={{ __html: signatureHtml }} />
71
75
  </section>
72
76
  )}
@@ -74,7 +78,7 @@ export function MemberProfile({
74
78
  {regions?.plugins}
75
79
 
76
80
  {actions.length > 0 && (
77
- <nav aria-label="Member actions" className="flex flex-wrap gap-2 font-mono text-xs">
81
+ <nav aria-label={c('actionsLabel')} className="flex flex-wrap gap-2 font-mono text-xs">
78
82
  {actions.map((action) => (
79
83
  <a
80
84
  key={action.href}
@@ -1,8 +1,11 @@
1
- import type { NavigationModel } from '@meith/theme-kit'
1
+ import type { NavigationModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+
4
+ export function Navigation({ items, copy }: NavigationModel & { copy: SlotCopy }) {
5
+ const c = (key: string) => fromSlotCopy(copy, `midnight.navigation.${key}`)
2
6
 
3
- export function Navigation({ items }: NavigationModel) {
4
7
  return (
5
- <nav aria-label="Breadcrumb" className="border-b border-border bg-muted px-4 py-1.5">
8
+ <nav aria-label={c('breadcrumbLabel')} className="border-b border-border bg-muted px-4 py-1.5">
6
9
  <ol className="flex flex-wrap items-center gap-1 font-mono text-xs text-muted-foreground">
7
10
  {items.map((item, index) => {
8
11
  const isLast = index === items.length - 1
@@ -1,4 +1,5 @@
1
- import type { NoticeModel } from '@meith/theme-kit'
1
+ import type { NoticeModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  const EDGE: Record<NoticeModel['kind'], string> = {
4
5
  info: 'border-l-primary',
@@ -7,7 +8,9 @@ const EDGE: Record<NoticeModel['kind'], string> = {
7
8
  error: 'border-l-destructive',
8
9
  }
9
10
 
10
- export function Notice({ kind, message, dismissHref }: NoticeModel) {
11
+ export function Notice({ kind, message, dismissHref, copy }: NoticeModel & { copy: SlotCopy }) {
12
+ const c = (key: string) => fromSlotCopy(copy, `midnight.notice.${key}`)
13
+
11
14
  return (
12
15
  <div
13
16
  role={kind === 'error' ? 'alert' : undefined}
@@ -15,14 +18,17 @@ export function Notice({ kind, message, dismissHref }: NoticeModel) {
15
18
  >
16
19
  <p>
17
20
  <span className="font-mono text-xs uppercase tracking-wide text-muted-foreground">
18
- {kind}
21
+ {c(`kind.${kind}`)}
19
22
  {': '}
20
23
  </span>
21
24
  {message}
22
25
  </p>
23
26
  {dismissHref !== null && (
24
- <a href={dismissHref} className="font-mono text-xs text-muted-foreground hover:text-foreground">
25
- dismiss
27
+ <a
28
+ href={dismissHref}
29
+ className="font-mono text-xs text-muted-foreground hover:text-foreground"
30
+ >
31
+ {c('dismiss')}
26
32
  </a>
27
33
  )}
28
34
  </div>
@@ -1,4 +1,5 @@
1
- import type { PaginationModel } from '@meith/theme-kit'
1
+ import type { PaginationModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  export function Pagination({
4
5
  page,
@@ -7,14 +8,20 @@ export function Pagination({
7
8
  pages,
8
9
  previousHref,
9
10
  nextHref,
10
- }: PaginationModel) {
11
+ copy,
12
+ }: PaginationModel & { copy: SlotCopy }) {
11
13
  if (pageCount <= 1 && previousHref === null && nextHref === null) return null
12
14
 
15
+ const c = (key: string) => fromSlotCopy(copy, `midnight.pagination.${key}`)
16
+
13
17
  return (
14
- <nav aria-label="Pagination" className="flex flex-wrap items-center gap-1 font-mono text-xs">
18
+ <nav
19
+ aria-label={c('ariaLabel')}
20
+ className="flex flex-wrap items-center gap-1 font-mono text-xs"
21
+ >
15
22
  {previousHref !== null && (
16
23
  <a href={previousHref} rel="prev" className="border border-border px-2 py-1 hover:bg-muted">
17
- prev
24
+ {c('prev')}
18
25
  </a>
19
26
  )}
20
27
  {pages.map((entry) =>
@@ -27,18 +34,22 @@ export function Pagination({
27
34
  {entry.page}
28
35
  </span>
29
36
  ) : (
30
- <a key={entry.href} href={entry.href} className="border border-border px-2 py-1 hover:bg-muted">
37
+ <a
38
+ key={entry.href}
39
+ href={entry.href}
40
+ className="border border-border px-2 py-1 hover:bg-muted"
41
+ >
31
42
  {entry.page}
32
43
  </a>
33
44
  ),
34
45
  )}
35
46
  {nextHref !== null && (
36
47
  <a href={nextHref} rel="next" className="border border-border px-2 py-1 hover:bg-muted">
37
- next
48
+ {c('next')}
38
49
  </a>
39
50
  )}
40
51
  <span className="ml-2 text-muted-foreground">
41
- page {pageCountIsExact ? `${page} of ${pageCount}` : page}
52
+ {c('page')} {pageCountIsExact ? `${page} ${c('pageOf')} ${pageCount}` : page}
42
53
  </span>
43
54
  </nav>
44
55
  )
@@ -1,20 +1,27 @@
1
- import type { PostActionsSlotModel } from '@meith/theme-kit'
1
+ import type { PostActionsSlotModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+
4
+ export function PostActions({
5
+ actions,
6
+ children,
7
+ copy,
8
+ }: PostActionsSlotModel & { copy: SlotCopy }) {
9
+ const c = (key: string) => fromSlotCopy(copy, `midnight.postActions.${key}`)
2
10
 
3
- export function PostActions({ actions, children }: PostActionsSlotModel) {
4
11
  const items = [
5
- { href: actions.quoteHref, label: 'quote' },
6
- { href: actions.editHref, label: 'edit' },
7
- { href: actions.restoreHref, label: 'restore' },
8
- { href: actions.rateHref, label: 'rate' },
9
- { href: actions.reportHref, label: 'report' },
10
- { href: actions.warnHref, label: 'warn' },
11
- { href: actions.moderateHref, label: 'moderate' },
12
+ { href: actions.quoteHref, label: c('quote') },
13
+ { href: actions.editHref, label: c('edit') },
14
+ { href: actions.restoreHref, label: c('restore') },
15
+ { href: actions.rateHref, label: c('rate') },
16
+ { href: actions.reportHref, label: c('report') },
17
+ { href: actions.warnHref, label: c('warn') },
18
+ { href: actions.moderateHref, label: c('moderate') },
12
19
  ].filter((item): item is { href: string; label: string } => item.href !== null)
13
20
 
14
21
  if (items.length === 0 && children === undefined) return null
15
22
 
16
23
  return (
17
- <nav aria-label="Post actions" className="flex flex-wrap gap-3 font-mono text-xs">
24
+ <nav aria-label={c('ariaLabel')} className="flex flex-wrap gap-3 font-mono text-xs">
18
25
  {items.map((item) => (
19
26
  <a key={item.label} href={item.href} className="text-muted-foreground hover:text-primary">
20
27
  {item.label}
@@ -1,8 +1,13 @@
1
- import type { PostBitSlotModel } from '@meith/theme-kit'
1
+ import type { PostBitSlotModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { UserRef } from '../shared'
4
5
 
5
- function GroupBadge({ badge }: { badge: NonNullable<PostBitSlotModel['post']['author']['badge']> }) {
6
+ function GroupBadge({
7
+ badge,
8
+ }: {
9
+ badge: NonNullable<PostBitSlotModel['post']['author']['badge']>
10
+ }) {
6
11
  const image = (
7
12
  <img
8
13
  src={badge.src}
@@ -24,21 +29,28 @@ function GroupBadge({ badge }: { badge: NonNullable<PostBitSlotModel['post']['au
24
29
  )
25
30
  }
26
31
 
27
- function StatusBanner({ visibility }: { visibility: PostBitSlotModel['post']['visibility'] }) {
32
+ function StatusBanner({
33
+ visibility,
34
+ copy,
35
+ }: {
36
+ visibility: PostBitSlotModel['post']['visibility']
37
+ copy: SlotCopy
38
+ }) {
28
39
  if (visibility === 'visible') return null
40
+ const c = (key: string) => fromSlotCopy(copy, `midnight.postBit.${key}`)
29
41
  return (
30
42
  <p className="border-b border-border bg-muted px-3 py-1 font-mono text-xs text-muted-foreground">
31
- {visibility === 'deleted'
32
- ? 'deleted — visible to moderators only'
33
- : 'awaiting approval — visible to moderators only'}
43
+ {visibility === 'deleted' ? c('deleted') : c('unapproved')}
34
44
  </p>
35
45
  )
36
46
  }
37
47
 
38
- export function PostBit({ post, select, regions }: PostBitSlotModel) {
48
+ export function PostBit({ post, select, regions, copy }: PostBitSlotModel & { copy: SlotCopy }) {
49
+ const c = (key: string) => fromSlotCopy(copy, `midnight.postBit.${key}`)
50
+
39
51
  return (
40
52
  <article id={`post-${post.number}`} data-post-id={post.id} className="border border-border">
41
- <StatusBanner visibility={post.visibility} />
53
+ <StatusBanner visibility={post.visibility} copy={copy} />
42
54
 
43
55
  <div className="grid grid-cols-1 sm:grid-cols-[11rem_1fr]">
44
56
  <div className="border-b border-border bg-secondary px-3 py-2 sm:border-b-0 sm:border-r">
@@ -84,12 +96,12 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
84
96
  )}
85
97
  <dl className="mt-2 font-mono text-xs text-muted-foreground">
86
98
  <div className="flex gap-1">
87
- <dt>posts</dt>
88
- <dd className="text-foreground">{post.author.postCount}</dd>
99
+ <dt>{c('posts')}</dt>
100
+ <dd className="text-foreground">{post.author.postCount.label}</dd>
89
101
  </div>
90
102
  {post.author.joinedAt !== null && (
91
103
  <div className="flex gap-1">
92
- <dt>joined</dt>
104
+ <dt>{c('joined')}</dt>
93
105
  <dd>
94
106
  <time dateTime={post.author.joinedAt.iso}>{post.author.joinedAt.label}</time>
95
107
  </dd>
@@ -97,11 +109,11 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
97
109
  )}
98
110
  {post.author.reputation != null && (
99
111
  <div className="flex gap-1">
100
- <dt>rep</dt>
101
- <dd className="text-foreground">{post.author.reputation}</dd>
112
+ <dt>{c('rep')}</dt>
113
+ <dd className="text-foreground">{post.author.reputation.label}</dd>
102
114
  </div>
103
115
  )}
104
- {post.author.isOnline && <div className="text-forum-unread">online</div>}
116
+ {post.author.isOnline && <div className="text-forum-unread">{c('online')}</div>}
105
117
  </dl>
106
118
 
107
119
  {post.author.fields.length > 0 && (
@@ -126,9 +138,10 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
126
138
 
127
139
  {post.ignored !== null ? (
128
140
  <p className="px-3 py-3 text-sm text-muted-foreground">
129
- Post from {post.ignored.authorUsername}, who you are ignoring.{' '}
141
+ {c('ignoredPrefix')} {post.ignored.authorUsername}
142
+ {c('ignoredSuffix')}{' '}
130
143
  <a href={post.ignored.revealHref} className="text-primary hover:underline">
131
- Show it
144
+ {c('showIt')}
132
145
  </a>
133
146
  </p>
134
147
  ) : (
@@ -1,6 +1,9 @@
1
- import type { ShellModel } from '@meith/theme-kit'
1
+ import type { ShellModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+
4
+ export function Shell({ viewer, children, copy }: ShellModel & { copy: SlotCopy }) {
5
+ const c = (key: string) => fromSlotCopy(copy, `midnight.shell.${key}`)
2
6
 
3
- export function Shell({ viewer, children }: ShellModel) {
4
7
  return (
5
8
  <div
6
9
  className="min-h-dvh bg-background text-foreground"
@@ -10,7 +13,7 @@ export function Shell({ viewer, children }: ShellModel) {
10
13
  href="#board-content"
11
14
  className="sr-only focus:not-sr-only focus:absolute focus:left-2 focus:top-2 focus:z-50 focus:border focus:border-primary focus:bg-card focus:px-3 focus:py-2 focus:text-sm"
12
15
  >
13
- Skip to content
16
+ {c('skipToContent')}
14
17
  </a>
15
18
  <div className="mx-auto flex min-h-dvh w-full max-w-6xl flex-col border-x border-border bg-card">
16
19
  {children}
@@ -1,14 +1,20 @@
1
- import type { SubforumListModel } from '@meith/theme-kit'
1
+ import type { SlotCopy, SubforumListModel } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
- export function SubforumList({ forums }: SubforumListModel) {
4
+ export function SubforumList({ forums, copy }: SubforumListModel & { copy: SlotCopy }) {
4
5
  if (forums.length === 0) return null
5
6
 
7
+ const c = (key: string) => fromSlotCopy(copy, `midnight.subforumList.${key}`)
8
+
6
9
  return (
7
- <nav aria-label="Subforums" className="flex flex-wrap gap-x-3 gap-y-1 border border-border bg-muted px-3 py-2 font-mono text-xs">
10
+ <nav
11
+ aria-label={c('ariaLabel')}
12
+ className="flex flex-wrap gap-x-3 gap-y-1 border border-border bg-muted px-3 py-2 font-mono text-xs"
13
+ >
8
14
  {forums.map((forum) => (
9
15
  <a key={forum.href} href={forum.href} className="text-primary hover:underline">
10
16
  {forum.title}
11
- <span className="ml-1 text-muted-foreground">({forum.threadCount})</span>
17
+ <span className="ml-1 text-muted-foreground">({forum.threadCount.label})</span>
12
18
  </a>
13
19
  ))}
14
20
  </nav>
@@ -1,8 +1,11 @@
1
- import type { ThreadRowSlotModel } from '@meith/theme-kit'
1
+ import type { SlotCopy, ThreadRowSlotModel } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { UserRef } from '../shared'
4
5
 
5
- export function ThreadRow({ thread, select }: ThreadRowSlotModel) {
6
+ export function ThreadRow({ thread, select, copy }: ThreadRowSlotModel & { copy: SlotCopy }) {
7
+ const c = (key: string) => fromSlotCopy(copy, `midnight.threadRow.${key}`)
8
+
6
9
  return (
7
10
  <tr className="border-t border-border align-top odd:bg-card even:bg-muted/40">
8
11
  <td className="px-3 py-2">
@@ -30,20 +33,26 @@ export function ThreadRow({ thread, select }: ThreadRowSlotModel) {
30
33
  <a href={thread.href} className="font-medium hover:text-primary">
31
34
  {thread.title}
32
35
  </a>
33
- {thread.isUnread && <span className="sr-only">(new posts)</span>}
34
- {thread.isSticky && <span className="font-mono text-xs text-thread-pinned">pinned</span>}
35
- {thread.isLocked && <span className="font-mono text-xs text-thread-locked">locked</span>}
36
- {thread.isMoved && <span className="font-mono text-xs text-thread-moved">moved</span>}
36
+ {thread.isUnread && <span className="sr-only">{c('newPosts')}</span>}
37
+ {thread.isSticky && (
38
+ <span className="font-mono text-xs text-thread-pinned">{c('pinned')}</span>
39
+ )}
40
+ {thread.isLocked && (
41
+ <span className="font-mono text-xs text-thread-locked">{c('locked')}</span>
42
+ )}
43
+ {thread.isMoved && (
44
+ <span className="font-mono text-xs text-thread-moved">{c('moved')}</span>
45
+ )}
37
46
  </div>
38
47
  <p className="mt-0.5 font-mono text-xs text-muted-foreground">
39
48
  <UserRef user={thread.author} className="hover:text-foreground" />
40
49
  </p>
41
50
  </td>
42
51
  <td className="w-16 px-2 py-2 text-right font-mono text-xs text-muted-foreground">
43
- {thread.replyCount}
52
+ {thread.replyCount.label}
44
53
  </td>
45
54
  <td className="w-16 px-2 py-2 text-right font-mono text-xs text-muted-foreground">
46
- {thread.viewCount}
55
+ {thread.viewCount.label}
47
56
  </td>
48
57
  <td className="w-56 px-3 py-2 text-xs text-muted-foreground">
49
58
  {thread.lastPost === null ? (
@@ -52,7 +61,7 @@ export function ThreadRow({ thread, select }: ThreadRowSlotModel) {
52
61
  <a href={thread.lastPost.href} className="hover:text-primary">
53
62
  <time dateTime={thread.lastPost.at.iso}>{thread.lastPost.at.label}</time>
54
63
  <span className="block">
55
- by <UserRef user={thread.lastPost.author} linked={false} />
64
+ {c('by')} <UserRef user={thread.lastPost.author} linked={false} />
56
65
  </span>
57
66
  </a>
58
67
  )}
@@ -1,13 +1,23 @@
1
- import type { ThreadViewModel } from '@meith/theme-kit'
1
+ import type { SlotCopy, ThreadViewModel } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+
4
+ export function ThreadView({
5
+ thread,
6
+ forum,
7
+ replyHref,
8
+ markReadAction,
9
+ regions,
10
+ copy,
11
+ }: ThreadViewModel & { copy: SlotCopy }) {
12
+ const c = (key: string) => fromSlotCopy(copy, `midnight.threadView.${key}`)
2
13
 
3
- export function ThreadView({ thread, forum, replyHref, markReadAction, regions }: ThreadViewModel) {
4
14
  const reply =
5
15
  replyHref === null ? null : (
6
16
  <a
7
17
  href={replyHref}
8
18
  className="border border-primary bg-primary px-3 py-1 font-mono text-xs text-primary-foreground hover:opacity-90"
9
19
  >
10
- reply
20
+ {c('reply')}
11
21
  </a>
12
22
  )
13
23
 
@@ -17,7 +27,10 @@ export function ThreadView({ thread, forum, replyHref, markReadAction, regions }
17
27
  <div>
18
28
  <h1 className="font-mono text-lg font-semibold">{thread.title}</h1>
19
29
  <p className="font-mono text-xs text-muted-foreground">
20
- in <a href={forum.href} className="hover:text-primary">{forum.label}</a>
30
+ {c('in')}{' '}
31
+ <a href={forum.href} className="hover:text-primary">
32
+ {forum.label}
33
+ </a>
21
34
  </p>
22
35
  </div>
23
36
  {reply}
@@ -45,7 +58,7 @@ export function ThreadView({ thread, forum, replyHref, markReadAction, regions }
45
58
  type="submit"
46
59
  className="border border-border px-2 py-1 font-mono text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
47
60
  >
48
- mark read
61
+ {c('markRead')}
49
62
  </button>
50
63
  </form>
51
64
  )}
@@ -1,4 +1,5 @@
1
- import type { UserPanelModel } from '@meith/theme-kit'
1
+ import type { SlotCopy, UserPanelModel } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  export function UserPanel({
4
5
  viewer,
@@ -8,23 +9,25 @@ export function UserPanel({
8
9
  notificationsHref,
9
10
  messagesHref,
10
11
  children,
11
- }: UserPanelModel) {
12
+ copy,
13
+ }: UserPanelModel & { copy: SlotCopy }) {
14
+ const c = (key: string) => fromSlotCopy(copy, `midnight.userPanel.${key}`)
15
+
12
16
  return (
13
17
  <div className="flex flex-wrap items-center gap-x-3 gap-y-1 font-mono text-xs">
14
18
  <span className="text-muted-foreground">
15
- {viewer.isGuest ? 'guest' : (viewer.username ?? 'signed in')}
19
+ {viewer.isGuest ? c('guest') : (viewer.username ?? c('signedIn'))}
16
20
  </span>
17
21
 
18
- {unreadNotifications > 0 && (
22
+ {unreadNotifications.value > 0 && (
19
23
  <a href={notificationsHref} className="text-accent hover:underline">
20
- [{unreadNotifications} new]
21
- <span className="sr-only"> notifications</span>
24
+ [{unreadNotifications.label} {c('new')}]
25
+ <span className="sr-only"> {c('notificationsSrOnly')}</span>
22
26
  </a>
23
27
  )}
24
- {unreadMessages > 0 && (
28
+ {unreadMessages.value > 0 && (
25
29
  <a href={messagesHref} className="text-accent hover:underline">
26
- [{unreadMessages} pm]
27
- <span className="sr-only"> unread messages</span>
30
+ [{unreadMessages.label} {c('pm')}]<span className="sr-only"> {c('messagesSrOnly')}</span>
28
31
  </a>
29
32
  )}
30
33
 
@@ -1,52 +1,57 @@
1
- import type { WhoIsOnlineModel } from '@meith/theme-kit'
1
+ import type { SlotCopy, WhoIsOnlineModel } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { UserRef } from '../shared'
4
5
 
5
6
  export function WhoIsOnline({
6
7
  guestCount,
7
8
  members,
9
+ memberCount,
8
10
  total,
9
11
  recordCount,
10
12
  recordAt,
11
13
  fullListHref,
12
- }: WhoIsOnlineModel) {
14
+ copy,
15
+ }: WhoIsOnlineModel & { copy: SlotCopy }) {
16
+ const c = (key: string) => fromSlotCopy(copy, `midnight.whoIsOnline.${key}`)
17
+
13
18
  return (
14
19
  <section aria-labelledby="who-is-online-heading" className="border border-border">
15
20
  <h2
16
21
  id="who-is-online-heading"
17
22
  className="border-b border-border bg-secondary px-3 py-1 font-mono text-xs uppercase tracking-wide"
18
23
  >
19
- Online now — {total}
24
+ {c('onlineNow')} {total.label}
20
25
  </h2>
21
26
  <p className="px-3 py-2 text-xs">
22
- {members.length === 0 ? (
23
- <span className="text-muted-foreground">no members</span>
27
+ {memberCount.value === 0 ? (
28
+ <span className="text-muted-foreground">{c('noMembers')}</span>
24
29
  ) : (
25
30
  members.map((member, index) => (
26
31
  <span key={member.username}>
27
32
  {index > 0 && ', '}
28
33
  <UserRef user={member} className="text-primary hover:underline" />
29
34
  {member.isInvisible && (
30
- <span className="ml-1 font-mono text-muted-foreground">(hidden)</span>
35
+ <span className="ml-1 font-mono text-muted-foreground">{c('hidden')}</span>
31
36
  )}
32
37
  </span>
33
38
  ))
34
39
  )}
35
40
  <span className="text-muted-foreground">
36
41
  {' · '}
37
- {guestCount} guests
42
+ {guestCount.label} {c('guests')}
38
43
  </span>
39
44
  </p>
40
45
  <p className="border-t border-border px-3 py-1 font-mono text-xs text-muted-foreground">
41
46
  <a href={fullListHref} className="hover:text-foreground">
42
- full list
47
+ {c('fullList')}
43
48
  </a>
44
- {' · record '}
45
- {recordCount}
49
+ {' · '}
50
+ {c('record')} {recordCount.label}
46
51
  {recordAt !== null && (
47
52
  <>
48
- {' on '}
49
- <time dateTime={recordAt.iso}>{recordAt.label}</time>
53
+ {' '}
54
+ {c('on')} <time dateTime={recordAt.iso}>{recordAt.label}</time>
50
55
  </>
51
56
  )}
52
57
  </p>