@meith/theme-phasebook 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 +4 -4
- package/src/copy.ts +276 -0
- package/src/index.ts +2 -2
- package/src/messages/en.json +149 -0
- package/src/messages/index.ts +5 -0
- package/src/shared.tsx +5 -5
- package/src/slots/announcement.tsx +13 -3
- package/src/slots/board-index.tsx +12 -5
- package/src/slots/board-stats.tsx +14 -12
- package/src/slots/discovery-view.tsx +13 -7
- package/src/slots/error-notice.tsx +17 -6
- package/src/slots/footer.tsx +15 -4
- package/src/slots/forum-display.tsx +23 -14
- package/src/slots/forum-row.tsx +10 -7
- package/src/slots/header.tsx +17 -4
- package/src/slots/latest-posts.tsx +12 -8
- package/src/slots/latest-threads.tsx +18 -9
- package/src/slots/member-profile.tsx +15 -13
- package/src/slots/navigation.tsx +6 -3
- package/src/slots/notice.tsx +10 -8
- package/src/slots/pagination.tsx +13 -10
- package/src/slots/panel-nav.tsx +30 -8
- package/src/slots/panel-page.tsx +3 -8
- package/src/slots/post-actions.tsx +17 -10
- package/src/slots/post-bit.tsx +21 -16
- package/src/slots/post-form.tsx +7 -3
- package/src/slots/redirect-notice.tsx +15 -6
- package/src/slots/search-form.tsx +15 -8
- package/src/slots/search-results.tsx +18 -17
- package/src/slots/shell.tsx +4 -3
- package/src/slots/subforum-list.tsx +9 -5
- package/src/slots/thread-row.tsx +16 -13
- package/src/slots/thread-view.tsx +33 -10
- package/src/slots/user-panel.tsx +18 -14
- package/src/slots/who-is-online.tsx +33 -16
- package/src/theme.ts +61 -1
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
import type { ErrorNoticeModel } from '@meith/theme-kit'
|
|
1
|
+
import type { ErrorNoticeModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import { NUMERIC, PILL_PRIMARY } from '../shared'
|
|
4
5
|
|
|
5
|
-
export function ErrorNotice({
|
|
6
|
+
export function ErrorNotice({
|
|
7
|
+
status,
|
|
8
|
+
title,
|
|
9
|
+
message,
|
|
10
|
+
homeHref,
|
|
11
|
+
requestId,
|
|
12
|
+
copy,
|
|
13
|
+
}: ErrorNoticeModel & { copy: SlotCopy }) {
|
|
14
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.errorNotice.${key}`)
|
|
15
|
+
|
|
6
16
|
return (
|
|
7
17
|
<section className="w-full max-w-lg overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-elevation">
|
|
8
18
|
<div className="px-5 py-6">
|
|
9
|
-
<p className={`text-xs font-semibold text-destructive ${NUMERIC}`}>
|
|
19
|
+
<p className={`text-xs font-semibold text-destructive ${NUMERIC}`}>
|
|
20
|
+
{c('error')} {status}
|
|
21
|
+
</p>
|
|
10
22
|
|
|
11
23
|
<h1 className="mt-1 text-2xl leading-tight font-bold tracking-tight text-balance">
|
|
12
24
|
{title}
|
|
@@ -14,14 +26,13 @@ export function ErrorNotice({ status, title, message, homeHref, requestId }: Err
|
|
|
14
26
|
<p className="mt-2 text-sm text-muted-foreground">{message}</p>
|
|
15
27
|
|
|
16
28
|
<a href={homeHref} className={`mt-5 ${PILL_PRIMARY}`}>
|
|
17
|
-
|
|
29
|
+
{c('home')}
|
|
18
30
|
</a>
|
|
19
31
|
</div>
|
|
20
32
|
|
|
21
33
|
{requestId !== null && (
|
|
22
34
|
<p className="border-t border-border px-5 py-3 text-xs text-muted-foreground">
|
|
23
|
-
|
|
24
|
-
<code className="font-mono text-foreground select-all">{requestId}</code>
|
|
35
|
+
{c('quote')} <code className="font-mono text-foreground select-all">{requestId}</code>
|
|
25
36
|
</p>
|
|
26
37
|
)}
|
|
27
38
|
</section>
|
package/src/slots/footer.tsx
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
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, `phasebook.footer.${key}`)
|
|
14
|
+
|
|
6
15
|
return (
|
|
7
16
|
<footer className="mt-auto bg-background">
|
|
8
17
|
<div className={`${PAGE} flex flex-col gap-2 py-6 text-xs text-muted-foreground`}>
|
|
9
18
|
{links.length > 0 && (
|
|
10
|
-
<nav aria-label=
|
|
19
|
+
<nav aria-label={c('ariaLabel')} className="flex flex-wrap gap-x-4 gap-y-1">
|
|
11
20
|
{links.map((link) => (
|
|
12
21
|
<a key={link.href} href={link.href} className={MUTED_LINK}>
|
|
13
22
|
{link.label}
|
|
@@ -18,7 +27,9 @@ export function Footer({ boardTitle, links, timezoneLabel, poweredBy }: FooterMo
|
|
|
18
27
|
|
|
19
28
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
20
29
|
<span className="font-semibold text-foreground">{boardTitle}</span>
|
|
21
|
-
<span>
|
|
30
|
+
<span>
|
|
31
|
+
{c('timezone')} {timezoneLabel}
|
|
32
|
+
</span>
|
|
22
33
|
{poweredBy && (
|
|
23
34
|
<a href={poweredBy.href} className={MUTED_LINK}>
|
|
24
35
|
{poweredBy.label}
|
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
import type { ForumDisplayModel } from '@meith/theme-kit'
|
|
1
|
+
import type { ForumDisplayModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
Circle,
|
|
6
|
+
count,
|
|
5
7
|
FEED,
|
|
8
|
+
isEmptyRegion,
|
|
6
9
|
NUMERIC,
|
|
7
10
|
PAGE,
|
|
8
11
|
PILL,
|
|
9
12
|
PILL_PRIMARY,
|
|
10
|
-
count,
|
|
11
|
-
isEmptyRegion,
|
|
12
13
|
plural,
|
|
13
14
|
} from '../shared'
|
|
14
15
|
|
|
15
|
-
export function ForumDisplay({
|
|
16
|
+
export function ForumDisplay({
|
|
17
|
+
forum,
|
|
18
|
+
newThreadHref,
|
|
19
|
+
markReadAction,
|
|
20
|
+
regions,
|
|
21
|
+
copy,
|
|
22
|
+
}: ForumDisplayModel & { copy: SlotCopy }) {
|
|
23
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.forumDisplay.${key}`)
|
|
24
|
+
|
|
16
25
|
return (
|
|
17
26
|
<div className={`${PAGE} py-4 sm:py-6`}>
|
|
18
27
|
<div className={`${FEED} flex flex-col gap-4`}>
|
|
@@ -30,8 +39,10 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
|
|
|
30
39
|
</h1>
|
|
31
40
|
{forum.type !== 'link' && (
|
|
32
41
|
<p className={`mt-0.5 text-xs text-muted-foreground ${NUMERIC}`}>
|
|
33
|
-
{count(forum.threadCount)}
|
|
34
|
-
{
|
|
42
|
+
{count(forum.threadCount)}{' '}
|
|
43
|
+
{plural(forum.threadCount, c('thread.one'), c('thread.other'))} ·{' '}
|
|
44
|
+
{count(forum.postCount)}{' '}
|
|
45
|
+
{plural(forum.postCount, c('post.one'), c('post.other'))}
|
|
35
46
|
</p>
|
|
36
47
|
)}
|
|
37
48
|
</div>
|
|
@@ -40,7 +51,7 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
|
|
|
40
51
|
{markReadAction !== null && (
|
|
41
52
|
<form action={markReadAction} method="post" className="shrink-0">
|
|
42
53
|
<button type="submit" className={PILL}>
|
|
43
|
-
|
|
54
|
+
{c('markRead')}
|
|
44
55
|
</button>
|
|
45
56
|
</form>
|
|
46
57
|
)}
|
|
@@ -59,10 +70,10 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
|
|
|
59
70
|
href={newThreadHref}
|
|
60
71
|
className="min-w-0 flex-1 truncate rounded-full bg-secondary px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent"
|
|
61
72
|
>
|
|
62
|
-
|
|
73
|
+
{c('startThreadIn')} {forum.title}…
|
|
63
74
|
</a>
|
|
64
75
|
<a href={newThreadHref} className={`hidden sm:inline-flex ${PILL_PRIMARY}`}>
|
|
65
|
-
|
|
76
|
+
{c('newThread')}
|
|
66
77
|
</a>
|
|
67
78
|
</div>
|
|
68
79
|
)}
|
|
@@ -79,15 +90,13 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
|
|
|
79
90
|
|
|
80
91
|
{isEmptyRegion(regions.threads) ? (
|
|
81
92
|
<div className="rounded-lg border border-border bg-card px-4 py-10 text-center shadow-elevation">
|
|
82
|
-
<p className="text-base font-semibold">
|
|
93
|
+
<p className="text-base font-semibold">{c('noThreadsYet')}</p>
|
|
83
94
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
84
|
-
{newThreadHref === null
|
|
85
|
-
? 'Nothing has been posted in this forum.'
|
|
86
|
-
: 'Nothing has been posted in this forum. Yours would be the first.'}
|
|
95
|
+
{newThreadHref === null ? c('emptyForum') : c('emptyForumInvite')}
|
|
87
96
|
</p>
|
|
88
97
|
{newThreadHref !== null && (
|
|
89
98
|
<a href={newThreadHref} className={`mt-4 ${PILL_PRIMARY}`}>
|
|
90
|
-
|
|
99
|
+
{c('startFirstThread')}
|
|
91
100
|
</a>
|
|
92
101
|
)}
|
|
93
102
|
</div>
|
package/src/slots/forum-row.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
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
|
-
import { Circle, MUTED_LINK, NUMERIC, Stamp, UserRef
|
|
4
|
+
import { Circle, count, MUTED_LINK, NUMERIC, plural, Stamp, 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'
|
|
8
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.forumRow.${key}`)
|
|
7
9
|
|
|
8
10
|
return (
|
|
9
11
|
<li
|
|
@@ -28,12 +30,13 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
|
|
|
28
30
|
>
|
|
29
31
|
{forum.title}
|
|
30
32
|
</a>
|
|
31
|
-
{forum.isUnread && <span className="sr-only"> (
|
|
33
|
+
{forum.isUnread && <span className="sr-only"> {c('newPosts')}</span>}
|
|
32
34
|
|
|
33
35
|
{!isLink && (
|
|
34
36
|
<span className={`shrink-0 text-xs text-muted-foreground ${NUMERIC}`}>
|
|
35
|
-
{count(forum.threadCount)}
|
|
36
|
-
{
|
|
37
|
+
{count(forum.threadCount)}{' '}
|
|
38
|
+
{plural(forum.threadCount, c('thread.one'), c('thread.other'))} ·{' '}
|
|
39
|
+
{count(forum.postCount)} {plural(forum.postCount, c('post.one'), c('post.other'))}
|
|
37
40
|
</span>
|
|
38
41
|
)}
|
|
39
42
|
</div>
|
|
@@ -59,7 +62,7 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
|
|
|
59
62
|
{!isLink && (
|
|
60
63
|
<p className="mt-1.5 truncate text-xs text-muted-foreground">
|
|
61
64
|
{forum.lastPost === null ? (
|
|
62
|
-
<span className="text-forum-read">
|
|
65
|
+
<span className="text-forum-read">{c('noPostsYet')}</span>
|
|
63
66
|
) : (
|
|
64
67
|
<>
|
|
65
68
|
<a href={forum.lastPost.href} className={`font-semibold ${MUTED_LINK}`}>
|
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 } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import { PAGE } from '../shared'
|
|
4
5
|
|
|
@@ -35,7 +36,16 @@ function BoardMark({ boardTitle, logo }: Pick<HeaderModel, 'boardTitle' | 'logo'
|
|
|
35
36
|
)
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export function Header({
|
|
39
|
+
export function Header({
|
|
40
|
+
boardTitle,
|
|
41
|
+
homeHref,
|
|
42
|
+
navigation,
|
|
43
|
+
logo,
|
|
44
|
+
children,
|
|
45
|
+
copy,
|
|
46
|
+
}: HeaderModel & { copy: SlotCopy }) {
|
|
47
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.header.${key}`)
|
|
48
|
+
|
|
39
49
|
return (
|
|
40
50
|
<header className="sticky top-0 z-30 border-b border-border bg-card shadow-elevation">
|
|
41
51
|
<div className={`${PAGE} flex h-14 items-center justify-between gap-3`}>
|
|
@@ -50,7 +60,10 @@ export function Header({ boardTitle, homeHref, navigation, logo, children }: Hea
|
|
|
50
60
|
</a>
|
|
51
61
|
|
|
52
62
|
{navigation.length > 0 && (
|
|
53
|
-
<nav
|
|
63
|
+
<nav
|
|
64
|
+
aria-label={c('sectionsAriaLabel')}
|
|
65
|
+
className="hidden min-w-0 flex-1 justify-center lg:flex"
|
|
66
|
+
>
|
|
54
67
|
<ul className="flex items-center gap-1">
|
|
55
68
|
{navigation.map((item) => (
|
|
56
69
|
<li key={item.href}>
|
|
@@ -70,7 +83,7 @@ export function Header({ boardTitle, homeHref, navigation, logo, children }: Hea
|
|
|
70
83
|
</div>
|
|
71
84
|
|
|
72
85
|
{navigation.length > 0 && (
|
|
73
|
-
<nav aria-label=
|
|
86
|
+
<nav aria-label={c('sectionsAriaLabel')} className="border-t border-border lg:hidden">
|
|
74
87
|
<ul className={`${PAGE} flex items-center gap-1 overflow-x-auto py-1`}>
|
|
75
88
|
{navigation.map((item) => (
|
|
76
89
|
<li key={item.href} className="shrink-0">
|
|
@@ -1,11 +1,14 @@
|
|
|
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 { Circle, MUTED_LINK, NUMERIC, Rail, Stamp, 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, `phasebook.latestPosts.${key}`)
|
|
8
|
+
|
|
6
9
|
return (
|
|
7
10
|
<Rail
|
|
8
|
-
title=
|
|
11
|
+
title={c('title')}
|
|
9
12
|
titleId="latest-posts-heading"
|
|
10
13
|
action={
|
|
11
14
|
<span className={`text-xs text-muted-foreground ${NUMERIC}`}>
|
|
@@ -14,13 +17,14 @@ export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
|
|
|
14
17
|
}
|
|
15
18
|
>
|
|
16
19
|
{posts.length === 0 ? (
|
|
17
|
-
<p className="px-3 pt-1 pb-3 text-xs text-muted-foreground">
|
|
18
|
-
The newest posts you can see will appear here.
|
|
19
|
-
</p>
|
|
20
|
+
<p className="px-3 pt-1 pb-3 text-xs text-muted-foreground">{c('empty')}</p>
|
|
20
21
|
) : (
|
|
21
22
|
<ul className="px-1 pt-1 pb-1">
|
|
22
23
|
{posts.map((post) => (
|
|
23
|
-
<li
|
|
24
|
+
<li
|
|
25
|
+
key={post.href}
|
|
26
|
+
className="rounded-lg px-2 py-1.5 transition-colors hover:bg-accent"
|
|
27
|
+
>
|
|
24
28
|
<div className="flex items-start gap-2.5">
|
|
25
29
|
<Circle name={post.author.username} size={32} className="mt-0.5" />
|
|
26
30
|
|
|
@@ -40,7 +44,7 @@ export function LatestPosts({ posts, capturedAt }: LatestPostsModel) {
|
|
|
40
44
|
|
|
41
45
|
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
42
46
|
<UserRef user={post.author} className="text-xs font-medium" />
|
|
43
|
-
{'
|
|
47
|
+
{` ${c('in')} `}
|
|
44
48
|
<a href={post.forum.href} className={MUTED_LINK}>
|
|
45
49
|
{post.forum.label}
|
|
46
50
|
</a>
|
|
@@ -1,11 +1,18 @@
|
|
|
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
|
-
import { Circle, MUTED_LINK, NUMERIC, Rail, Stamp, UserRef
|
|
4
|
+
import { Circle, count, MUTED_LINK, NUMERIC, plural, Rail, Stamp, UserRef } from '../shared'
|
|
5
|
+
|
|
6
|
+
export function LatestThreads({
|
|
7
|
+
threads,
|
|
8
|
+
capturedAt,
|
|
9
|
+
copy,
|
|
10
|
+
}: LatestThreadsModel & { copy: SlotCopy }) {
|
|
11
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.latestThreads.${key}`)
|
|
4
12
|
|
|
5
|
-
export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
|
|
6
13
|
return (
|
|
7
14
|
<Rail
|
|
8
|
-
title=
|
|
15
|
+
title={c('title')}
|
|
9
16
|
titleId="latest-threads-heading"
|
|
10
17
|
action={
|
|
11
18
|
<span className={`text-xs text-muted-foreground ${NUMERIC}`}>
|
|
@@ -14,13 +21,14 @@ export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
|
|
|
14
21
|
}
|
|
15
22
|
>
|
|
16
23
|
{threads.length === 0 ? (
|
|
17
|
-
<p className="px-3 pt-1 pb-3 text-xs text-muted-foreground">
|
|
18
|
-
The newest threads you can see will appear here.
|
|
19
|
-
</p>
|
|
24
|
+
<p className="px-3 pt-1 pb-3 text-xs text-muted-foreground">{c('empty')}</p>
|
|
20
25
|
) : (
|
|
21
26
|
<ul className="px-1 pt-1 pb-1">
|
|
22
27
|
{threads.map((thread) => (
|
|
23
|
-
<li
|
|
28
|
+
<li
|
|
29
|
+
key={thread.href}
|
|
30
|
+
className="rounded-lg px-2 py-1.5 transition-colors hover:bg-accent"
|
|
31
|
+
>
|
|
24
32
|
<div className="flex items-start gap-2.5">
|
|
25
33
|
<Circle name={thread.author.username} size={32} className="mt-0.5" />
|
|
26
34
|
|
|
@@ -41,7 +49,8 @@ export function LatestThreads({ threads, capturedAt }: LatestThreadsModel) {
|
|
|
41
49
|
</p>
|
|
42
50
|
|
|
43
51
|
<p className={`mt-0.5 truncate text-xs text-muted-foreground ${NUMERIC}`}>
|
|
44
|
-
{count(thread.replyCount)}
|
|
52
|
+
{count(thread.replyCount)}{' '}
|
|
53
|
+
{plural(thread.replyCount, c('reply.one'), c('reply.other'))} ·{' '}
|
|
45
54
|
<Stamp at={thread.startedAt} />
|
|
46
55
|
</p>
|
|
47
56
|
</div>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { MemberProfileModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
1
3
|
import { cn } from '@meith/ui'
|
|
2
|
-
import type { MemberProfileModel } from '@meith/theme-kit'
|
|
3
4
|
|
|
4
|
-
import { Circle, FEED, NUMERIC, PAGE, PILL, PILL_PRIMARY, Stamp
|
|
5
|
+
import { Circle, count, FEED, NUMERIC, PAGE, PILL, PILL_PRIMARY, Stamp } from '../shared'
|
|
5
6
|
|
|
6
7
|
export function MemberProfile({
|
|
7
8
|
user,
|
|
@@ -14,7 +15,10 @@ export function MemberProfile({
|
|
|
14
15
|
fields,
|
|
15
16
|
actions,
|
|
16
17
|
regions,
|
|
17
|
-
|
|
18
|
+
copy,
|
|
19
|
+
}: MemberProfileModel & { copy: SlotCopy }) {
|
|
20
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.memberProfile.${key}`)
|
|
21
|
+
|
|
18
22
|
return (
|
|
19
23
|
<div className={`${PAGE} py-4 sm:py-6`}>
|
|
20
24
|
<div className={`${FEED} flex flex-col gap-4`}>
|
|
@@ -45,7 +49,7 @@ export function MemberProfile({
|
|
|
45
49
|
|
|
46
50
|
{actions.length > 0 && (
|
|
47
51
|
<nav
|
|
48
|
-
aria-label=
|
|
52
|
+
aria-label={c('actionsAriaLabel')}
|
|
49
53
|
className="flex shrink-0 flex-wrap items-center justify-center gap-2"
|
|
50
54
|
>
|
|
51
55
|
{actions.map((action, index) => (
|
|
@@ -63,22 +67,20 @@ export function MemberProfile({
|
|
|
63
67
|
|
|
64
68
|
<dl className="grid grid-cols-3 gap-2 border-t border-border px-4 py-3 text-center">
|
|
65
69
|
<div>
|
|
66
|
-
<dd className={`text-lg font-bold text-foreground ${NUMERIC}`}>
|
|
67
|
-
|
|
68
|
-
</dd>
|
|
69
|
-
<dt className="text-xs text-muted-foreground">Posts</dt>
|
|
70
|
+
<dd className={`text-lg font-bold text-foreground ${NUMERIC}`}>{count(postCount)}</dd>
|
|
71
|
+
<dt className="text-xs text-muted-foreground">{c('posts')}</dt>
|
|
70
72
|
</div>
|
|
71
73
|
<div>
|
|
72
74
|
<dd className="text-sm font-semibold text-foreground">
|
|
73
75
|
<Stamp at={joinedAt} />
|
|
74
76
|
</dd>
|
|
75
|
-
<dt className="text-xs text-muted-foreground">
|
|
77
|
+
<dt className="text-xs text-muted-foreground">{c('joined')}</dt>
|
|
76
78
|
</div>
|
|
77
79
|
<div>
|
|
78
80
|
<dd className="text-sm font-semibold text-foreground">
|
|
79
|
-
{lastVisitAt === null ? '
|
|
81
|
+
{lastVisitAt === null ? c('never') : <Stamp at={lastVisitAt} />}
|
|
80
82
|
</dd>
|
|
81
|
-
<dt className="text-xs text-muted-foreground">
|
|
83
|
+
<dt className="text-xs text-muted-foreground">{c('lastVisit')}</dt>
|
|
82
84
|
</div>
|
|
83
85
|
</dl>
|
|
84
86
|
</section>
|
|
@@ -92,7 +94,7 @@ export function MemberProfile({
|
|
|
92
94
|
id="profile-fields-heading"
|
|
93
95
|
className="px-4 pt-3 pb-1 text-[1.0625rem] font-bold tracking-tight"
|
|
94
96
|
>
|
|
95
|
-
|
|
97
|
+
{c('about')}
|
|
96
98
|
</h2>
|
|
97
99
|
<dl className="grid gap-x-6 gap-y-3 px-4 pt-2 pb-4 sm:grid-cols-2">
|
|
98
100
|
{fields.map((field) => (
|
|
@@ -114,7 +116,7 @@ export function MemberProfile({
|
|
|
114
116
|
id="profile-signature-heading"
|
|
115
117
|
className="px-4 pt-3 pb-1 text-[1.0625rem] font-bold tracking-tight"
|
|
116
118
|
>
|
|
117
|
-
|
|
119
|
+
{c('signature')}
|
|
118
120
|
</h2>
|
|
119
121
|
<div
|
|
120
122
|
className="prose-md px-4 pt-2 pb-4 text-sm text-muted-foreground"
|
package/src/slots/navigation.tsx
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import type { NavigationModel } from '@meith/theme-kit'
|
|
1
|
+
import type { NavigationModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import { PAGE } from '../shared'
|
|
4
5
|
|
|
5
|
-
export function Navigation({ items }: NavigationModel) {
|
|
6
|
+
export function Navigation({ items, copy }: NavigationModel & { copy: SlotCopy }) {
|
|
6
7
|
if (items.length === 0) return null
|
|
7
8
|
|
|
9
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.navigation.${key}`)
|
|
10
|
+
|
|
8
11
|
return (
|
|
9
|
-
<nav aria-label=
|
|
12
|
+
<nav aria-label={c('ariaLabel')} className="bg-background">
|
|
10
13
|
<ol
|
|
11
14
|
className={`${PAGE} flex items-center gap-1 overflow-x-auto pt-3 text-sm whitespace-nowrap text-muted-foreground`}
|
|
12
15
|
>
|
package/src/slots/notice.tsx
CHANGED
|
@@ -1,31 +1,33 @@
|
|
|
1
|
+
import type { NoticeModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
1
3
|
import { cn } from '@meith/ui'
|
|
2
|
-
import type { NoticeModel } from '@meith/theme-kit'
|
|
3
4
|
|
|
4
5
|
const KIND = {
|
|
5
6
|
info: {
|
|
6
|
-
|
|
7
|
+
copyKey: 'info',
|
|
7
8
|
className: 'border-primary/30 bg-primary/8 text-foreground',
|
|
8
9
|
mark: 'bg-primary text-primary-foreground',
|
|
9
10
|
},
|
|
10
11
|
success: {
|
|
11
|
-
|
|
12
|
+
copyKey: 'success',
|
|
12
13
|
className: 'border-moderation-approved/30 bg-moderation-approved/8 text-foreground',
|
|
13
14
|
mark: 'bg-moderation-approved text-card',
|
|
14
15
|
},
|
|
15
16
|
warning: {
|
|
16
|
-
|
|
17
|
+
copyKey: 'warning',
|
|
17
18
|
className: 'border-thread-pinned/30 bg-thread-pinned/8 text-foreground',
|
|
18
19
|
mark: 'bg-thread-pinned text-card',
|
|
19
20
|
},
|
|
20
21
|
error: {
|
|
21
|
-
|
|
22
|
+
copyKey: 'error',
|
|
22
23
|
className: 'border-destructive/30 bg-destructive/8 text-foreground',
|
|
23
24
|
mark: 'bg-destructive text-destructive-foreground',
|
|
24
25
|
},
|
|
25
26
|
} as const
|
|
26
27
|
|
|
27
|
-
export function Notice({ kind, message, dismissHref }: NoticeModel) {
|
|
28
|
+
export function Notice({ kind, message, dismissHref, copy }: NoticeModel & { copy: SlotCopy }) {
|
|
28
29
|
const tone = KIND[kind]
|
|
30
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.notice.${key}`)
|
|
29
31
|
|
|
30
32
|
return (
|
|
31
33
|
<div
|
|
@@ -46,7 +48,7 @@ export function Notice({ kind, message, dismissHref }: NoticeModel) {
|
|
|
46
48
|
</span>
|
|
47
49
|
|
|
48
50
|
<p className="min-w-0 flex-1">
|
|
49
|
-
<span className="font-semibold">{tone.
|
|
51
|
+
<span className="font-semibold">{c(tone.copyKey)}:</span> {message}
|
|
50
52
|
</p>
|
|
51
53
|
|
|
52
54
|
{dismissHref !== null && (
|
|
@@ -54,7 +56,7 @@ export function Notice({ kind, message, dismissHref }: NoticeModel) {
|
|
|
54
56
|
href={dismissHref}
|
|
55
57
|
className="shrink-0 rounded-full px-2 py-1 text-xs font-semibold text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
56
58
|
>
|
|
57
|
-
|
|
59
|
+
{c('dismiss')}
|
|
58
60
|
</a>
|
|
59
61
|
)}
|
|
60
62
|
</div>
|
package/src/slots/pagination.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { PaginationModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
1
3
|
import { cn } from '@meith/ui'
|
|
2
|
-
import type { PaginationModel } from '@meith/theme-kit'
|
|
3
4
|
|
|
4
5
|
import { NUMERIC, PILL } from '../shared'
|
|
5
6
|
|
|
@@ -10,23 +11,25 @@ export function Pagination({
|
|
|
10
11
|
pages,
|
|
11
12
|
previousHref,
|
|
12
13
|
nextHref,
|
|
13
|
-
|
|
14
|
+
copy,
|
|
15
|
+
}: PaginationModel & { copy: SlotCopy }) {
|
|
14
16
|
if (pageCount <= 1 && previousHref === null && nextHref === null) return null
|
|
15
17
|
|
|
16
18
|
const disabled = cn(PILL, 'pointer-events-none opacity-40')
|
|
19
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.pagination.${key}`)
|
|
17
20
|
|
|
18
21
|
return (
|
|
19
22
|
<nav
|
|
20
|
-
aria-label=
|
|
23
|
+
aria-label={c('ariaLabel')}
|
|
21
24
|
className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border bg-card px-3 py-2.5 shadow-elevation"
|
|
22
25
|
>
|
|
23
26
|
{previousHref === null ? (
|
|
24
27
|
<span className={disabled} aria-hidden="true">
|
|
25
|
-
|
|
28
|
+
{c('previous')}
|
|
26
29
|
</span>
|
|
27
30
|
) : (
|
|
28
31
|
<a href={previousHref} rel="prev" className={PILL}>
|
|
29
|
-
|
|
32
|
+
{c('previous')}
|
|
30
33
|
</a>
|
|
31
34
|
)}
|
|
32
35
|
|
|
@@ -45,7 +48,7 @@ export function Pagination({
|
|
|
45
48
|
<a
|
|
46
49
|
href={entry.href}
|
|
47
50
|
aria-current={entry.isCurrent ? 'page' : undefined}
|
|
48
|
-
aria-label={
|
|
51
|
+
aria-label={`${c('page')} ${entry.page}`}
|
|
49
52
|
className={cn(
|
|
50
53
|
'inline-flex size-9 items-center justify-center rounded-full text-sm font-semibold transition-colors',
|
|
51
54
|
NUMERIC,
|
|
@@ -61,18 +64,18 @@ export function Pagination({
|
|
|
61
64
|
})}
|
|
62
65
|
|
|
63
66
|
<li className={`ml-1 text-xs whitespace-nowrap text-muted-foreground ${NUMERIC}`}>
|
|
64
|
-
<span className="sr-only">
|
|
65
|
-
{pageCountIsExact ? `${page} of ${pageCount}` :
|
|
67
|
+
<span className="sr-only">{c('page')} </span>
|
|
68
|
+
{pageCountIsExact ? `${page} ${c('of')} ${pageCount}` : `${c('page')} ${page}`}
|
|
66
69
|
</li>
|
|
67
70
|
</ol>
|
|
68
71
|
|
|
69
72
|
{nextHref === null ? (
|
|
70
73
|
<span className={disabled} aria-hidden="true">
|
|
71
|
-
|
|
74
|
+
{c('next')}
|
|
72
75
|
</span>
|
|
73
76
|
) : (
|
|
74
77
|
<a href={nextHref} rel="next" className={PILL}>
|
|
75
|
-
|
|
78
|
+
{c('next')}
|
|
76
79
|
</a>
|
|
77
80
|
)}
|
|
78
81
|
</nav>
|