@meith/theme-midnight 0.6.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.
- package/package.json +3 -3
- package/src/copy.ts +186 -0
- package/src/index.ts +4 -4
- package/src/messages/en.json +90 -0
- package/src/messages/index.ts +5 -0
- package/src/slots/announcement.tsx +14 -4
- package/src/slots/board-index.tsx +10 -4
- package/src/slots/board-stats.tsx +16 -12
- package/src/slots/category-block.tsx +13 -6
- package/src/slots/footer.tsx +14 -3
- package/src/slots/forum-display.tsx +25 -8
- package/src/slots/forum-row.tsx +8 -6
- package/src/slots/header.tsx +13 -3
- package/src/slots/latest-posts.tsx +8 -5
- package/src/slots/latest-threads.tsx +12 -5
- package/src/slots/member-profile.tsx +13 -9
- package/src/slots/navigation.tsx +6 -3
- package/src/slots/notice.tsx +11 -5
- package/src/slots/pagination.tsx +18 -7
- package/src/slots/post-actions.tsx +17 -10
- package/src/slots/post-bit.tsx +29 -16
- package/src/slots/shell.tsx +6 -3
- package/src/slots/subforum-list.tsx +10 -4
- package/src/slots/thread-row.tsx +18 -9
- package/src/slots/thread-view.tsx +18 -5
- package/src/slots/user-panel.tsx +12 -9
- package/src/slots/who-is-online.tsx +17 -12
- package/src/theme.ts +53 -1
|
@@ -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
|
-
|
|
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">
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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">
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
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=
|
|
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=
|
|
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}
|
package/src/slots/navigation.tsx
CHANGED
|
@@ -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=
|
|
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
|
package/src/slots/notice.tsx
CHANGED
|
@@ -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
|
|
25
|
-
|
|
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>
|
package/src/slots/pagination.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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}
|
|
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=
|
|
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}
|
package/src/slots/post-bit.tsx
CHANGED
|
@@ -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({
|
|
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({
|
|
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
|
-
|
|
141
|
+
{c('ignoredPrefix')} {post.ignored.authorUsername}
|
|
142
|
+
{c('ignoredSuffix')}{' '}
|
|
130
143
|
<a href={post.ignored.revealHref} className="text-primary hover:underline">
|
|
131
|
-
|
|
144
|
+
{c('showIt')}
|
|
132
145
|
</a>
|
|
133
146
|
</p>
|
|
134
147
|
) : (
|
package/src/slots/shell.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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>
|
package/src/slots/thread-row.tsx
CHANGED
|
@@ -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">(
|
|
34
|
-
{thread.isSticky &&
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
61
|
+
{c('markRead')}
|
|
49
62
|
</button>
|
|
50
63
|
</form>
|
|
51
64
|
)}
|
package/src/slots/user-panel.tsx
CHANGED
|
@@ -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
|
-
|
|
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 ?? '
|
|
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">
|
|
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
|
-
|
|
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
|
-
|
|
24
|
+
{c('onlineNow')} {total.label}
|
|
20
25
|
</h2>
|
|
21
26
|
<p className="px-3 py-2 text-xs">
|
|
22
|
-
{
|
|
23
|
-
<span className="text-muted-foreground">
|
|
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
|
-
|
|
47
|
+
{c('fullList')}
|
|
43
48
|
</a>
|
|
44
|
-
{' ·
|
|
45
|
-
{recordCount}
|
|
49
|
+
{' · '}
|
|
50
|
+
{c('record')} {recordCount.label}
|
|
46
51
|
{recordAt !== null && (
|
|
47
52
|
<>
|
|
48
|
-
{'
|
|
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>
|