@meith/theme-default 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.
- package/package.json +3 -3
- package/src/copy.ts +316 -0
- package/src/index.ts +6 -6
- package/src/messages/en.json +189 -0
- package/src/messages/index.ts +5 -0
- package/src/panel-icons.tsx +1 -2
- package/src/shared.tsx +4 -6
- package/src/slots/announcement.tsx +15 -9
- package/src/slots/auth-page.tsx +3 -1
- package/src/slots/board-index.tsx +11 -4
- package/src/slots/board-stats.tsx +14 -12
- package/src/slots/category-block.tsx +1 -1
- package/src/slots/discovery-view.tsx +16 -15
- package/src/slots/error-notice.tsx +17 -9
- package/src/slots/footer.tsx +15 -4
- package/src/slots/forum-display.tsx +25 -20
- package/src/slots/forum-jump.tsx +1 -1
- package/src/slots/forum-row.tsx +16 -15
- package/src/slots/header.tsx +13 -3
- package/src/slots/latest-posts.tsx +21 -12
- package/src/slots/latest-threads.tsx +27 -16
- package/src/slots/member-profile.tsx +19 -15
- package/src/slots/navigation.tsx +7 -4
- package/src/slots/notice.tsx +12 -9
- package/src/slots/pagination.tsx +16 -10
- package/src/slots/panel-nav.tsx +37 -16
- package/src/slots/panel-page.tsx +15 -3
- package/src/slots/panel-shell.tsx +1 -7
- package/src/slots/post-actions.tsx +18 -11
- package/src/slots/post-bit.tsx +42 -20
- package/src/slots/post-form.tsx +7 -3
- package/src/slots/redirect-notice.tsx +16 -7
- package/src/slots/search-form.tsx +13 -9
- package/src/slots/search-results.tsx +27 -26
- package/src/slots/shell.tsx +4 -3
- package/src/slots/subforum-list.tsx +12 -9
- package/src/slots/thread-row.tsx +19 -18
- package/src/slots/thread-view.tsx +23 -13
- package/src/slots/user-panel.tsx +19 -14
- package/src/slots/who-is-online.tsx +35 -19
- package/src/theme.ts +90 -19
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { MemberProfileModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
|
+
import { Avatar, buttonVariants, Card, CardContent, CardHeader, CardTitle, cn } from '@meith/ui'
|
|
3
4
|
|
|
4
5
|
import { NUMERIC, PAGE_BODY, Stamp } from '../shared'
|
|
5
6
|
|
|
@@ -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, `default.memberProfile.${key}`)
|
|
21
|
+
|
|
18
22
|
return (
|
|
19
23
|
<div className={PAGE_BODY}>
|
|
20
24
|
<Card>
|
|
@@ -24,10 +28,7 @@ export function MemberProfile({
|
|
|
24
28
|
|
|
25
29
|
<div className="min-w-0">
|
|
26
30
|
<h1
|
|
27
|
-
className={cn(
|
|
28
|
-
'text-2xl font-semibold tracking-tight break-words',
|
|
29
|
-
user.nameClass,
|
|
30
|
-
)}
|
|
31
|
+
className={cn('text-2xl font-semibold tracking-tight break-words', user.nameClass)}
|
|
31
32
|
>
|
|
32
33
|
{user.username}
|
|
33
34
|
</h1>
|
|
@@ -36,7 +37,10 @@ export function MemberProfile({
|
|
|
36
37
|
</div>
|
|
37
38
|
|
|
38
39
|
{actions.length > 0 && (
|
|
39
|
-
<nav
|
|
40
|
+
<nav
|
|
41
|
+
aria-label={c('memberActions')}
|
|
42
|
+
className="flex shrink-0 flex-wrap items-center gap-2"
|
|
43
|
+
>
|
|
40
44
|
{actions.map((action, index) => (
|
|
41
45
|
<a
|
|
42
46
|
key={action.href}
|
|
@@ -56,21 +60,21 @@ export function MemberProfile({
|
|
|
56
60
|
<dl className="grid grid-cols-3 gap-3 border-t border-border px-5 py-4">
|
|
57
61
|
<div>
|
|
58
62
|
<dd className={`text-lg font-semibold text-foreground ${NUMERIC}`}>
|
|
59
|
-
{postCount.
|
|
63
|
+
{postCount.label}
|
|
60
64
|
</dd>
|
|
61
|
-
<dt className="text-xs text-muted-foreground">
|
|
65
|
+
<dt className="text-xs text-muted-foreground">{c('postsLabel')}</dt>
|
|
62
66
|
</div>
|
|
63
67
|
<div>
|
|
64
68
|
<dd className="text-sm font-medium text-foreground">
|
|
65
69
|
<Stamp at={joinedAt} />
|
|
66
70
|
</dd>
|
|
67
|
-
<dt className="text-xs text-muted-foreground">
|
|
71
|
+
<dt className="text-xs text-muted-foreground">{c('joinedLabel')}</dt>
|
|
68
72
|
</div>
|
|
69
73
|
<div>
|
|
70
74
|
<dd className="text-sm font-medium text-foreground">
|
|
71
|
-
{lastVisitAt === null ? '
|
|
75
|
+
{lastVisitAt === null ? c('never') : <Stamp at={lastVisitAt} />}
|
|
72
76
|
</dd>
|
|
73
|
-
<dt className="text-xs text-muted-foreground">
|
|
77
|
+
<dt className="text-xs text-muted-foreground">{c('lastVisitLabel')}</dt>
|
|
74
78
|
</div>
|
|
75
79
|
</dl>
|
|
76
80
|
</Card>
|
|
@@ -79,7 +83,7 @@ export function MemberProfile({
|
|
|
79
83
|
<Card aria-labelledby="profile-fields-heading">
|
|
80
84
|
<CardHeader>
|
|
81
85
|
<CardTitle id="profile-fields-heading" className="text-sm">
|
|
82
|
-
|
|
86
|
+
{c('about')}
|
|
83
87
|
</CardTitle>
|
|
84
88
|
</CardHeader>
|
|
85
89
|
<CardContent>
|
|
@@ -99,7 +103,7 @@ export function MemberProfile({
|
|
|
99
103
|
<Card aria-labelledby="profile-signature-heading">
|
|
100
104
|
<CardHeader>
|
|
101
105
|
<CardTitle id="profile-signature-heading" className="text-sm">
|
|
102
|
-
|
|
106
|
+
{c('signature')}
|
|
103
107
|
</CardTitle>
|
|
104
108
|
</CardHeader>
|
|
105
109
|
<CardContent>
|
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 { MUTED_LINK, 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, `default.navigation.${key}`)
|
|
10
|
+
|
|
8
11
|
return (
|
|
9
|
-
<nav aria-label=
|
|
12
|
+
<nav aria-label={c('breadcrumb')} className="border-b border-border bg-card/40">
|
|
10
13
|
<ol
|
|
11
14
|
className={`${PAGE} flex items-center gap-1.5 overflow-x-auto py-2 text-xs whitespace-nowrap text-muted-foreground`}
|
|
12
15
|
>
|
|
@@ -16,7 +19,7 @@ export function Navigation({ items }: NavigationModel) {
|
|
|
16
19
|
<li key={item.href} className="flex shrink-0 items-center gap-1.5">
|
|
17
20
|
{index > 0 && (
|
|
18
21
|
<span aria-hidden="true" className="text-border select-none">
|
|
19
|
-
|
|
22
|
+
{c('separator')}
|
|
20
23
|
</span>
|
|
21
24
|
)}
|
|
22
25
|
{isLast ? (
|
package/src/slots/notice.tsx
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
|
+
import type { NoticeModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
1
3
|
import { Alert, AlertDescription, AlertTitle } from '@meith/ui'
|
|
2
|
-
import type { NoticeModel } from '@meith/theme-kit'
|
|
3
4
|
|
|
4
5
|
import { MUTED_LINK } from '../shared'
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
info: '
|
|
8
|
-
success: '
|
|
9
|
-
warning: '
|
|
10
|
-
error: '
|
|
7
|
+
const KIND_KEYS: Record<NoticeModel['kind'], string> = {
|
|
8
|
+
info: 'info',
|
|
9
|
+
success: 'success',
|
|
10
|
+
warning: 'warning',
|
|
11
|
+
error: 'error',
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export function Notice({ kind, message, dismissHref }: NoticeModel) {
|
|
14
|
+
export function Notice({ kind, message, dismissHref, copy }: NoticeModel & { copy: SlotCopy }) {
|
|
15
|
+
const c = (key: string) => fromSlotCopy(copy, `default.notice.${key}`)
|
|
16
|
+
|
|
14
17
|
return (
|
|
15
18
|
<Alert tone={kind}>
|
|
16
19
|
<AlertDescription>
|
|
17
|
-
<AlertTitle>{
|
|
20
|
+
<AlertTitle>{c(KIND_KEYS[kind])}</AlertTitle> {message}
|
|
18
21
|
</AlertDescription>
|
|
19
22
|
|
|
20
23
|
{dismissHref !== null && (
|
|
21
24
|
<a href={dismissHref} className={`shrink-0 text-xs ${MUTED_LINK}`}>
|
|
22
|
-
|
|
25
|
+
{c('dismiss')}
|
|
23
26
|
</a>
|
|
24
27
|
)}
|
|
25
28
|
</Alert>
|
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 { buttonVariants, cn } from '@meith/ui'
|
|
2
|
-
import type { PaginationModel } from '@meith/theme-kit'
|
|
3
4
|
|
|
4
5
|
import { NUMERIC } from '../shared'
|
|
5
6
|
|
|
@@ -10,21 +11,24 @@ 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
|
|
|
18
|
+
const c = (key: string) => fromSlotCopy(copy, `default.pagination.${key}`)
|
|
19
|
+
|
|
16
20
|
const step = cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'min-w-20')
|
|
17
21
|
const stepDisabled = cn(step, 'pointer-events-none opacity-40')
|
|
18
22
|
|
|
19
23
|
return (
|
|
20
|
-
<nav aria-label=
|
|
24
|
+
<nav aria-label={c('nav')} className="flex flex-wrap items-center justify-between gap-3">
|
|
21
25
|
{previousHref === null ? (
|
|
22
26
|
<span className={stepDisabled} aria-hidden="true">
|
|
23
|
-
|
|
27
|
+
{c('previous')}
|
|
24
28
|
</span>
|
|
25
29
|
) : (
|
|
26
30
|
<a href={previousHref} rel="prev" className={step}>
|
|
27
|
-
|
|
31
|
+
{c('previous')}
|
|
28
32
|
</a>
|
|
29
33
|
)}
|
|
30
34
|
|
|
@@ -37,13 +41,13 @@ export function Pagination({
|
|
|
37
41
|
<li key={entry.page} className="flex items-center gap-1">
|
|
38
42
|
{gap && (
|
|
39
43
|
<span aria-hidden="true" className="px-1 text-sm text-muted-foreground">
|
|
40
|
-
|
|
44
|
+
{c('ellipsis')}
|
|
41
45
|
</span>
|
|
42
46
|
)}
|
|
43
47
|
<a
|
|
44
48
|
href={entry.href}
|
|
45
49
|
aria-current={entry.isCurrent ? 'page' : undefined}
|
|
46
|
-
aria-label={
|
|
50
|
+
aria-label={`${c('page')} ${entry.page}`}
|
|
47
51
|
className={cn(
|
|
48
52
|
buttonVariants({
|
|
49
53
|
variant: entry.isCurrent ? 'primary' : 'ghost',
|
|
@@ -62,18 +66,20 @@ export function Pagination({
|
|
|
62
66
|
|
|
63
67
|
<li className="ml-1 text-xs whitespace-nowrap text-muted-foreground">
|
|
64
68
|
<span className={NUMERIC}>
|
|
65
|
-
{pageCountIsExact
|
|
69
|
+
{pageCountIsExact
|
|
70
|
+
? `${c('page')} ${page} ${c('of')} ${pageCount}`
|
|
71
|
+
: `${c('page')} ${page}`}
|
|
66
72
|
</span>
|
|
67
73
|
</li>
|
|
68
74
|
</ol>
|
|
69
75
|
|
|
70
76
|
{nextHref === null ? (
|
|
71
77
|
<span className={stepDisabled} aria-hidden="true">
|
|
72
|
-
|
|
78
|
+
{c('next')}
|
|
73
79
|
</span>
|
|
74
80
|
) : (
|
|
75
81
|
<a href={nextHref} rel="next" className={step}>
|
|
76
|
-
|
|
82
|
+
{c('next')}
|
|
77
83
|
</a>
|
|
78
84
|
)}
|
|
79
85
|
</nav>
|
package/src/slots/panel-nav.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { PanelNavItemModel, PanelNavModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
|
+
import { cn, Disclosure } from '@meith/ui'
|
|
3
4
|
|
|
4
5
|
import { PanelIcon } from '../panel-icons'
|
|
5
6
|
|
|
@@ -11,21 +12,33 @@ const HERE =
|
|
|
11
12
|
const OPEN = 'font-semibold text-foreground hover:bg-accent'
|
|
12
13
|
const ELSEWHERE = 'text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
13
14
|
|
|
14
|
-
function Count({ count }: { count: number }) {
|
|
15
|
+
function Count({ count, copy }: { count: number; copy: SlotCopy }) {
|
|
16
|
+
const c = (key: string) => fromSlotCopy(copy, `default.panelNav.${key}`)
|
|
17
|
+
|
|
15
18
|
return (
|
|
16
19
|
<>
|
|
17
20
|
<span
|
|
18
21
|
aria-hidden="true"
|
|
19
22
|
className="ml-auto min-w-5 rounded-full bg-primary px-1.5 py-0.5 text-center text-xs font-semibold tabular-nums text-primary-foreground"
|
|
20
23
|
>
|
|
21
|
-
{count > 99 ? '
|
|
24
|
+
{count > 99 ? c('countCap') : count}
|
|
25
|
+
</span>
|
|
26
|
+
<span className="sr-only">
|
|
27
|
+
({count} {c('waiting')})
|
|
22
28
|
</span>
|
|
23
|
-
<span className="sr-only">({count} waiting)</span>
|
|
24
29
|
</>
|
|
25
30
|
)
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
function Item({
|
|
33
|
+
function Item({
|
|
34
|
+
item,
|
|
35
|
+
className,
|
|
36
|
+
copy,
|
|
37
|
+
}: {
|
|
38
|
+
item: PanelNavItemModel
|
|
39
|
+
className: string
|
|
40
|
+
copy: SlotCopy
|
|
41
|
+
}) {
|
|
29
42
|
const body = (
|
|
30
43
|
<>
|
|
31
44
|
{item.icon !== null && (
|
|
@@ -34,7 +47,7 @@ function Item({ item, className }: { item: PanelNavItemModel; className: string
|
|
|
34
47
|
</span>
|
|
35
48
|
)}
|
|
36
49
|
<span className="min-w-0 flex-1 truncate">{item.title}</span>
|
|
37
|
-
{item.count !== null && <Count count={item.count} />}
|
|
50
|
+
{item.count !== null && <Count count={item.count} copy={copy} />}
|
|
38
51
|
</>
|
|
39
52
|
)
|
|
40
53
|
|
|
@@ -53,8 +66,7 @@ function Item({ item, className }: { item: PanelNavItemModel; className: string
|
|
|
53
66
|
{...(item.current === null
|
|
54
67
|
? {}
|
|
55
68
|
: {
|
|
56
|
-
'aria-current':
|
|
57
|
-
item.current === 'here' ? ('page' as const) : ('true' as const),
|
|
69
|
+
'aria-current': item.current === 'here' ? ('page' as const) : ('true' as const),
|
|
58
70
|
})}
|
|
59
71
|
>
|
|
60
72
|
{body}
|
|
@@ -62,7 +74,11 @@ function Item({ item, className }: { item: PanelNavItemModel; className: string
|
|
|
62
74
|
)
|
|
63
75
|
}
|
|
64
76
|
|
|
65
|
-
function Sections({
|
|
77
|
+
function Sections({
|
|
78
|
+
label,
|
|
79
|
+
sections,
|
|
80
|
+
copy,
|
|
81
|
+
}: Pick<PanelNavModel, 'label' | 'sections'> & { copy: SlotCopy }) {
|
|
66
82
|
return (
|
|
67
83
|
<nav aria-label={label} className="text-sm">
|
|
68
84
|
<ul className="flex flex-col gap-0.5 pl-2.5">
|
|
@@ -73,9 +89,8 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
|
|
|
73
89
|
>
|
|
74
90
|
<Item
|
|
75
91
|
item={section}
|
|
76
|
-
className={
|
|
77
|
-
|
|
78
|
-
}
|
|
92
|
+
className={section.current === 'here' ? HERE : section.isOpen ? OPEN : ELSEWHERE}
|
|
93
|
+
copy={copy}
|
|
79
94
|
/>
|
|
80
95
|
|
|
81
96
|
{section.isOpen && section.children.length > 0 && (
|
|
@@ -85,6 +100,7 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
|
|
|
85
100
|
<Item
|
|
86
101
|
item={child}
|
|
87
102
|
className={child.current === 'here' ? HERE : ELSEWHERE}
|
|
103
|
+
copy={copy}
|
|
88
104
|
/>
|
|
89
105
|
</li>
|
|
90
106
|
))}
|
|
@@ -97,7 +113,12 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
|
|
|
97
113
|
)
|
|
98
114
|
}
|
|
99
115
|
|
|
100
|
-
export function PanelNav({
|
|
116
|
+
export function PanelNav({
|
|
117
|
+
label,
|
|
118
|
+
sections,
|
|
119
|
+
currentTitle,
|
|
120
|
+
copy,
|
|
121
|
+
}: PanelNavModel & { copy: SlotCopy }) {
|
|
101
122
|
return (
|
|
102
123
|
<>
|
|
103
124
|
<div className="sticky top-0 z-20 -mx-4 bg-background/95 px-4 py-2 backdrop-blur-sm sm:-mx-6 sm:px-6 lg:hidden">
|
|
@@ -106,12 +127,12 @@ export function PanelNav({ label, sections, currentTitle }: PanelNavModel) {
|
|
|
106
127
|
contentClassName="p-2"
|
|
107
128
|
{...(currentTitle === null ? {} : { aside: currentTitle })}
|
|
108
129
|
>
|
|
109
|
-
<Sections label={label} sections={sections} />
|
|
130
|
+
<Sections label={label} sections={sections} copy={copy} />
|
|
110
131
|
</Disclosure>
|
|
111
132
|
</div>
|
|
112
133
|
|
|
113
134
|
<div className="hidden rounded-xl border border-border bg-card p-2 shadow-elevation lg:block">
|
|
114
|
-
<Sections label={label} sections={sections} />
|
|
135
|
+
<Sections label={label} sections={sections} copy={copy} />
|
|
115
136
|
</div>
|
|
116
137
|
</>
|
|
117
138
|
)
|
package/src/slots/panel-page.tsx
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
import type { PanelPageModel, PanelSectionModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
1
3
|
import { cn } from '@meith/ui'
|
|
2
|
-
import type { PanelPageModel, PanelSectionModel } from '@meith/theme-kit'
|
|
3
4
|
|
|
4
5
|
import { MUTED_LINK, PAGE, pageAt } from '../shared'
|
|
5
6
|
|
|
6
|
-
export function PanelPage({
|
|
7
|
+
export function PanelPage({
|
|
8
|
+
title,
|
|
9
|
+
back,
|
|
10
|
+
frame,
|
|
11
|
+
width,
|
|
12
|
+
gap,
|
|
13
|
+
regions,
|
|
14
|
+
children,
|
|
15
|
+
copy,
|
|
16
|
+
}: PanelPageModel & { copy: SlotCopy }) {
|
|
17
|
+
const c = (key: string) => fromSlotCopy(copy, `default.panelPage.${key}`)
|
|
18
|
+
|
|
7
19
|
return (
|
|
8
20
|
<main
|
|
9
21
|
id="board-content"
|
|
@@ -20,7 +32,7 @@ export function PanelPage({ title, back, frame, width, gap, regions, children }:
|
|
|
20
32
|
<div className="flex min-w-0 flex-col gap-1">
|
|
21
33
|
{back !== null && (
|
|
22
34
|
<a href={back.href} className={`text-sm font-medium ${MUTED_LINK}`}>
|
|
23
|
-
|
|
35
|
+
{c('back')} {back.label}
|
|
24
36
|
</a>
|
|
25
37
|
)}
|
|
26
38
|
|
|
@@ -5,13 +5,7 @@ import { MUTED_LINK } from '../shared'
|
|
|
5
5
|
const RAIL =
|
|
6
6
|
'flex flex-col gap-4 px-4 pt-4 sm:px-6 lg:sticky lg:w-60 lg:shrink-0 lg:self-start lg:py-8 lg:pr-0'
|
|
7
7
|
|
|
8
|
-
export function PanelShell({
|
|
9
|
-
panel,
|
|
10
|
-
links,
|
|
11
|
-
linksLabel,
|
|
12
|
-
regions,
|
|
13
|
-
children,
|
|
14
|
-
}: PanelShellModel) {
|
|
8
|
+
export function PanelShell({ panel, links, linksLabel, regions, children }: PanelShellModel) {
|
|
15
9
|
return (
|
|
16
10
|
<div className="mx-auto flex w-full max-w-6xl flex-1 flex-col lg:flex-row">
|
|
17
11
|
<aside className={`${RAIL} ${panel === 'admincp' ? 'lg:top-14' : 'lg:top-6'}`}>
|
|
@@ -1,29 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { PostActionsSlotModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
|
+
import { buttonVariants, Separator } from '@meith/ui'
|
|
3
4
|
|
|
4
5
|
interface Action {
|
|
5
6
|
readonly href: string
|
|
6
7
|
readonly label: string
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export function PostActions({
|
|
10
|
+
export function PostActions({
|
|
11
|
+
actions,
|
|
12
|
+
children,
|
|
13
|
+
copy,
|
|
14
|
+
}: PostActionsSlotModel & { copy: SlotCopy }) {
|
|
15
|
+
const c = (key: string) => fromSlotCopy(copy, `default.postActions.${key}`)
|
|
16
|
+
|
|
10
17
|
const reader: Action[] = [
|
|
11
|
-
actions.quoteHref === null ? null : { href: actions.quoteHref, label: '
|
|
12
|
-
actions.editHref === null ? null : { href: actions.editHref, label: '
|
|
13
|
-
actions.rateHref === null ? null : { href: actions.rateHref, label: '
|
|
14
|
-
actions.reportHref === null ? null : { href: actions.reportHref, label: '
|
|
18
|
+
actions.quoteHref === null ? null : { href: actions.quoteHref, label: c('quote') },
|
|
19
|
+
actions.editHref === null ? null : { href: actions.editHref, label: c('edit') },
|
|
20
|
+
actions.rateHref === null ? null : { href: actions.rateHref, label: c('rate') },
|
|
21
|
+
actions.reportHref === null ? null : { href: actions.reportHref, label: c('report') },
|
|
15
22
|
].filter((action): action is Action => action !== null)
|
|
16
23
|
|
|
17
24
|
const staff: Action[] = [
|
|
18
|
-
actions.restoreHref === null ? null : { href: actions.restoreHref, label: '
|
|
19
|
-
actions.warnHref === null ? null : { href: actions.warnHref, label: '
|
|
20
|
-
actions.moderateHref === null ? null : { href: actions.moderateHref, label: '
|
|
25
|
+
actions.restoreHref === null ? null : { href: actions.restoreHref, label: c('restore') },
|
|
26
|
+
actions.warnHref === null ? null : { href: actions.warnHref, label: c('warn') },
|
|
27
|
+
actions.moderateHref === null ? null : { href: actions.moderateHref, label: c('moderate') },
|
|
21
28
|
].filter((action): action is Action => action !== null)
|
|
22
29
|
|
|
23
30
|
if (reader.length === 0 && staff.length === 0 && children === undefined) return null
|
|
24
31
|
|
|
25
32
|
return (
|
|
26
|
-
<nav aria-label=
|
|
33
|
+
<nav aria-label={c('nav')} className="flex flex-wrap items-center gap-x-1 gap-y-1">
|
|
27
34
|
{reader.map((action) => (
|
|
28
35
|
<a
|
|
29
36
|
key={action.href}
|
package/src/slots/post-bit.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { PostBitSlotModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
1
3
|
import { Alert, AlertDescription, AlertTitle, Avatar, Card } from '@meith/ui'
|
|
2
|
-
import type { PostBitSlotModel } from '@meith/theme-kit'
|
|
3
4
|
|
|
4
5
|
import { LINK, MUTED_LINK, NUMERIC, Stamp, UserRef } from '../shared'
|
|
5
6
|
|
|
@@ -9,9 +10,17 @@ const VISIBILITY_TINT = {
|
|
|
9
10
|
deleted: 'border-thread-deleted/50 bg-destructive/5',
|
|
10
11
|
} as const
|
|
11
12
|
|
|
12
|
-
function StatusBanner({
|
|
13
|
+
function StatusBanner({
|
|
14
|
+
visibility,
|
|
15
|
+
copy,
|
|
16
|
+
}: {
|
|
17
|
+
visibility: PostBitSlotModel['post']['visibility']
|
|
18
|
+
copy: SlotCopy
|
|
19
|
+
}) {
|
|
13
20
|
if (visibility === 'visible') return null
|
|
14
21
|
|
|
22
|
+
const c = (key: string) => fromSlotCopy(copy, `default.postBit.${key}`)
|
|
23
|
+
|
|
15
24
|
return (
|
|
16
25
|
<Alert
|
|
17
26
|
tone={visibility === 'deleted' ? 'error' : 'warning'}
|
|
@@ -19,15 +28,19 @@ function StatusBanner({ visibility }: { visibility: PostBitSlotModel['post']['vi
|
|
|
19
28
|
>
|
|
20
29
|
<AlertDescription>
|
|
21
30
|
<AlertTitle>
|
|
22
|
-
{visibility === 'deleted' ? '
|
|
31
|
+
{visibility === 'deleted' ? c('deletedPost') : c('waitingApproval')}
|
|
23
32
|
</AlertTitle>{' '}
|
|
24
|
-
|
|
33
|
+
{c('staffOnly')}
|
|
25
34
|
</AlertDescription>
|
|
26
35
|
</Alert>
|
|
27
36
|
)
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
function GroupBadge({
|
|
39
|
+
function GroupBadge({
|
|
40
|
+
badge,
|
|
41
|
+
}: {
|
|
42
|
+
badge: NonNullable<PostBitSlotModel['post']['author']['badge']>
|
|
43
|
+
}) {
|
|
31
44
|
const image = (
|
|
32
45
|
<img
|
|
33
46
|
src={badge.src}
|
|
@@ -52,10 +65,14 @@ function GroupBadge({ badge }: { badge: NonNullable<PostBitSlotModel['post']['au
|
|
|
52
65
|
function AuthorBlock({
|
|
53
66
|
author,
|
|
54
67
|
badges,
|
|
68
|
+
copy,
|
|
55
69
|
}: {
|
|
56
70
|
author: PostBitSlotModel['post']['author']
|
|
57
71
|
badges: React.ReactNode
|
|
72
|
+
copy: SlotCopy
|
|
58
73
|
}) {
|
|
74
|
+
const c = (key: string) => fromSlotCopy(copy, `default.postBit.${key}`)
|
|
75
|
+
|
|
59
76
|
return (
|
|
60
77
|
<div className="flex gap-3 sm:flex-col sm:gap-2 sm:text-center">
|
|
61
78
|
<Avatar src={author.avatarUrl} name={author.username} size={48} className="sm:self-center" />
|
|
@@ -78,7 +95,7 @@ function AuthorBlock({
|
|
|
78
95
|
{author.isOnline && (
|
|
79
96
|
<p className="mt-1 flex items-center gap-1.5 text-xs text-moderation-approved sm:justify-center">
|
|
80
97
|
<span aria-hidden="true" className="size-1.5 rounded-full bg-moderation-approved" />
|
|
81
|
-
|
|
98
|
+
{c('online')}
|
|
82
99
|
</p>
|
|
83
100
|
)}
|
|
84
101
|
|
|
@@ -86,23 +103,26 @@ function AuthorBlock({
|
|
|
86
103
|
|
|
87
104
|
<dl className={`mt-1.5 text-xs text-muted-foreground ${NUMERIC}`}>
|
|
88
105
|
<div className="flex gap-1 sm:justify-center">
|
|
89
|
-
<dt className="sr-only">
|
|
106
|
+
<dt className="sr-only">{c('postsLabel')}</dt>
|
|
90
107
|
<dd>
|
|
91
|
-
{author.postCount.
|
|
108
|
+
{author.postCount.label}{' '}
|
|
109
|
+
{author.postCount.value === 1 ? c('post.one') : c('post.other')}
|
|
92
110
|
</dd>
|
|
93
111
|
</div>
|
|
94
112
|
{author.joinedAt !== null && (
|
|
95
113
|
<div className="flex gap-1 sm:justify-center">
|
|
96
|
-
<dt className="sr-only">
|
|
114
|
+
<dt className="sr-only">{c('joined')}</dt>
|
|
97
115
|
<dd>
|
|
98
|
-
|
|
116
|
+
{c('joined')} <Stamp at={author.joinedAt} />
|
|
99
117
|
</dd>
|
|
100
118
|
</div>
|
|
101
119
|
)}
|
|
102
120
|
{author.reputation != null && (
|
|
103
121
|
<div className="flex gap-1 sm:justify-center">
|
|
104
|
-
<dt className="sr-only">
|
|
105
|
-
<dd>
|
|
122
|
+
<dt className="sr-only">{c('reputationLabel')}</dt>
|
|
123
|
+
<dd>
|
|
124
|
+
{author.reputation.label} {c('reputation')}
|
|
125
|
+
</dd>
|
|
106
126
|
</div>
|
|
107
127
|
)}
|
|
108
128
|
</dl>
|
|
@@ -122,7 +142,9 @@ function AuthorBlock({
|
|
|
122
142
|
)
|
|
123
143
|
}
|
|
124
144
|
|
|
125
|
-
export function PostBit({ post, select, regions }: PostBitSlotModel) {
|
|
145
|
+
export function PostBit({ post, select, regions, copy }: PostBitSlotModel & { copy: SlotCopy }) {
|
|
146
|
+
const c = (key: string) => fromSlotCopy(copy, `default.postBit.${key}`)
|
|
147
|
+
|
|
126
148
|
return (
|
|
127
149
|
<Card
|
|
128
150
|
as="article"
|
|
@@ -131,11 +153,11 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
|
|
|
131
153
|
data-visibility={post.visibility}
|
|
132
154
|
className={VISIBILITY_TINT[post.visibility]}
|
|
133
155
|
>
|
|
134
|
-
<StatusBanner visibility={post.visibility} />
|
|
156
|
+
<StatusBanner visibility={post.visibility} copy={copy} />
|
|
135
157
|
|
|
136
158
|
<div className="grid grid-cols-1 sm:grid-cols-[11rem_minmax(0,1fr)]">
|
|
137
159
|
<div className="border-b border-border px-4 py-3 sm:border-r sm:border-b-0 sm:bg-muted/40">
|
|
138
|
-
<AuthorBlock author={post.author} badges={regions.pluginBadges} />
|
|
160
|
+
<AuthorBlock author={post.author} badges={regions.pluginBadges} copy={copy} />
|
|
139
161
|
</div>
|
|
140
162
|
|
|
141
163
|
<div className="min-w-0">
|
|
@@ -163,11 +185,11 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
|
|
|
163
185
|
|
|
164
186
|
{post.ignored !== null ? (
|
|
165
187
|
<div className="px-4 py-5 text-sm text-muted-foreground">
|
|
166
|
-
|
|
167
|
-
<span className="font-medium text-foreground">{post.ignored.authorUsername}</span>.
|
|
168
|
-
|
|
188
|
+
{c('ignoringPrefix')}{' '}
|
|
189
|
+
<span className="font-medium text-foreground">{post.ignored.authorUsername}</span>.{' '}
|
|
190
|
+
{c('hiddenNotice')}{' '}
|
|
169
191
|
<a href={post.ignored.revealHref} className={`font-medium text-foreground ${LINK}`}>
|
|
170
|
-
|
|
192
|
+
{c('showAnyway')}
|
|
171
193
|
</a>
|
|
172
194
|
</div>
|
|
173
195
|
) : (
|
|
@@ -189,7 +211,7 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
|
|
|
189
211
|
<div className="border-t border-border px-4 py-3">
|
|
190
212
|
<h4 className="mb-2 text-xs font-semibold tracking-wide text-muted-foreground uppercase">
|
|
191
213
|
{post.attachments.length}{' '}
|
|
192
|
-
{post.attachments.length === 1 ? 'attachment' : '
|
|
214
|
+
{post.attachments.length === 1 ? c('attachment.one') : c('attachment.other')}
|
|
193
215
|
</h4>
|
|
194
216
|
<ul className="flex flex-wrap gap-3">
|
|
195
217
|
{post.attachments.map((file) => (
|
package/src/slots/post-form.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { PostFormModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
1
3
|
import { Alert, AlertDescription, AlertTitle, Card } from '@meith/ui'
|
|
2
|
-
import type { PostFormModel } from '@meith/theme-kit'
|
|
3
4
|
|
|
4
5
|
import { MUTED_LINK, pageAt } from '../shared'
|
|
5
6
|
|
|
@@ -9,7 +10,10 @@ export function PostForm({
|
|
|
9
10
|
cancelLabel,
|
|
10
11
|
errorMessage,
|
|
11
12
|
regions,
|
|
12
|
-
|
|
13
|
+
copy,
|
|
14
|
+
}: PostFormModel & { copy: SlotCopy }) {
|
|
15
|
+
const c = (key: string) => fromSlotCopy(copy, `default.postForm.${key}`)
|
|
16
|
+
|
|
13
17
|
return (
|
|
14
18
|
<div className={`${pageAt('max-w-3xl')} flex w-full flex-col gap-5 py-6 sm:py-8`}>
|
|
15
19
|
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-2">
|
|
@@ -22,7 +26,7 @@ export function PostForm({
|
|
|
22
26
|
{errorMessage !== null && (
|
|
23
27
|
<Alert tone="error">
|
|
24
28
|
<AlertDescription>
|
|
25
|
-
<AlertTitle>
|
|
29
|
+
<AlertTitle>{c('cannotPost')}</AlertTitle> {errorMessage}
|
|
26
30
|
</AlertDescription>
|
|
27
31
|
</Alert>
|
|
28
32
|
)}
|