@meith/theme-phasebook 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +50 -7
- 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
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 { Chip } from '../shared'
|
|
5
6
|
|
|
@@ -10,14 +11,24 @@ const HERE = 'bg-primary/12 text-primary'
|
|
|
10
11
|
const OPEN = 'text-foreground hover:bg-accent'
|
|
11
12
|
const ELSEWHERE = 'text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
12
13
|
|
|
13
|
-
function Item({
|
|
14
|
+
function Item({
|
|
15
|
+
item,
|
|
16
|
+
className,
|
|
17
|
+
copy,
|
|
18
|
+
}: {
|
|
19
|
+
item: PanelNavItemModel
|
|
20
|
+
className: string
|
|
21
|
+
copy: SlotCopy
|
|
22
|
+
}) {
|
|
23
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.panelNav.${key}`)
|
|
24
|
+
|
|
14
25
|
const body = (
|
|
15
26
|
<>
|
|
16
27
|
<span className="min-w-0 flex-1 truncate">{item.title}</span>
|
|
17
28
|
{item.count !== null && (
|
|
18
29
|
<Chip className="bg-primary/12 text-primary">
|
|
19
30
|
{item.count > 99 ? '99+' : item.count}
|
|
20
|
-
<span className="sr-only"> waiting</span>
|
|
31
|
+
<span className="sr-only"> {c('waiting')}</span>
|
|
21
32
|
</Chip>
|
|
22
33
|
)}
|
|
23
34
|
</>
|
|
@@ -44,7 +55,11 @@ function Item({ item, className }: { item: PanelNavItemModel; className: string
|
|
|
44
55
|
)
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
function Sections({
|
|
58
|
+
function Sections({
|
|
59
|
+
label,
|
|
60
|
+
sections,
|
|
61
|
+
copy,
|
|
62
|
+
}: Pick<PanelNavModel, 'label' | 'sections'> & { copy: SlotCopy }) {
|
|
48
63
|
return (
|
|
49
64
|
<nav aria-label={label}>
|
|
50
65
|
<ul className="flex flex-col">
|
|
@@ -56,6 +71,7 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
|
|
|
56
71
|
<Item
|
|
57
72
|
item={section}
|
|
58
73
|
className={section.current === 'here' ? HERE : section.isOpen ? OPEN : ELSEWHERE}
|
|
74
|
+
copy={copy}
|
|
59
75
|
/>
|
|
60
76
|
|
|
61
77
|
{section.isOpen && section.children.length > 0 && (
|
|
@@ -68,6 +84,7 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
|
|
|
68
84
|
'text-[0.8125rem]',
|
|
69
85
|
child.current === 'here' ? HERE : ELSEWHERE,
|
|
70
86
|
)}
|
|
87
|
+
copy={copy}
|
|
71
88
|
/>
|
|
72
89
|
</li>
|
|
73
90
|
))}
|
|
@@ -80,7 +97,12 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
|
|
|
80
97
|
)
|
|
81
98
|
}
|
|
82
99
|
|
|
83
|
-
export function PanelNav({
|
|
100
|
+
export function PanelNav({
|
|
101
|
+
label,
|
|
102
|
+
sections,
|
|
103
|
+
currentTitle,
|
|
104
|
+
copy,
|
|
105
|
+
}: PanelNavModel & { copy: SlotCopy }) {
|
|
84
106
|
return (
|
|
85
107
|
<>
|
|
86
108
|
<Disclosure
|
|
@@ -89,11 +111,11 @@ export function PanelNav({ label, sections, currentTitle }: PanelNavModel) {
|
|
|
89
111
|
contentClassName="p-2"
|
|
90
112
|
{...(currentTitle === null ? {} : { aside: currentTitle })}
|
|
91
113
|
>
|
|
92
|
-
<Sections label={label} sections={sections} />
|
|
114
|
+
<Sections label={label} sections={sections} copy={copy} />
|
|
93
115
|
</Disclosure>
|
|
94
116
|
|
|
95
117
|
<div className="hidden rounded-lg border border-border bg-card p-2 shadow-elevation lg:block">
|
|
96
|
-
<Sections label={label} sections={sections} />
|
|
118
|
+
<Sections label={label} sections={sections} copy={copy} />
|
|
97
119
|
</div>
|
|
98
120
|
</>
|
|
99
121
|
)
|
package/src/slots/panel-page.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { cn } from '@meith/ui'
|
|
2
1
|
import type { PanelPageModel, PanelSectionModel } from '@meith/theme-kit'
|
|
2
|
+
import { cn } from '@meith/ui'
|
|
3
3
|
|
|
4
4
|
import { MUTED_LINK } from '../shared'
|
|
5
5
|
|
|
@@ -29,9 +29,7 @@ export function PanelPage({ title, back, frame, width, gap, regions, children }:
|
|
|
29
29
|
</a>
|
|
30
30
|
)}
|
|
31
31
|
|
|
32
|
-
<h1 className="text-xl leading-tight font-bold tracking-tight text-balance">
|
|
33
|
-
{title}
|
|
34
|
-
</h1>
|
|
32
|
+
<h1 className="text-xl leading-tight font-bold tracking-tight text-balance">{title}</h1>
|
|
35
33
|
|
|
36
34
|
{regions.lede !== undefined && (
|
|
37
35
|
<p className="text-sm text-muted-foreground">{regions.lede}</p>
|
|
@@ -57,10 +55,7 @@ export function PanelSection({ title, headingId, regions, children }: PanelSecti
|
|
|
57
55
|
<section aria-labelledby={headingId} className="flex flex-col gap-3">
|
|
58
56
|
<div className="flex flex-wrap items-start justify-between gap-x-4 gap-y-1 px-1">
|
|
59
57
|
<div className="flex min-w-0 flex-col gap-0.5">
|
|
60
|
-
<h2
|
|
61
|
-
id={headingId}
|
|
62
|
-
className="text-[0.9375rem] font-semibold text-muted-foreground"
|
|
63
|
-
>
|
|
58
|
+
<h2 id={headingId} className="text-[0.9375rem] font-semibold text-muted-foreground">
|
|
64
59
|
{title}
|
|
65
60
|
</h2>
|
|
66
61
|
{regions.description !== undefined && (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { PostActionsSlotModel } from '@meith/theme-kit'
|
|
1
|
+
import type { PostActionsSlotModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
interface Action {
|
|
4
5
|
readonly href: string
|
|
@@ -9,24 +10,30 @@ const ACTION =
|
|
|
9
10
|
'inline-flex h-8 flex-1 items-center justify-center gap-1.5 rounded-lg px-3 text-sm font-semibold ' +
|
|
10
11
|
'text-muted-foreground transition-colors hover:bg-accent hover:text-foreground'
|
|
11
12
|
|
|
12
|
-
export function PostActions({
|
|
13
|
+
export function PostActions({
|
|
14
|
+
actions,
|
|
15
|
+
children,
|
|
16
|
+
copy,
|
|
17
|
+
}: PostActionsSlotModel & { copy: SlotCopy }) {
|
|
18
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.postActions.${key}`)
|
|
19
|
+
|
|
13
20
|
const reader: Action[] = [
|
|
14
|
-
actions.quoteHref === null ? null : { href: actions.quoteHref, label: '
|
|
15
|
-
actions.editHref === null ? null : { href: actions.editHref, label: '
|
|
16
|
-
actions.rateHref === null ? null : { href: actions.rateHref, label: '
|
|
17
|
-
actions.reportHref === null ? null : { href: actions.reportHref, label: '
|
|
21
|
+
actions.quoteHref === null ? null : { href: actions.quoteHref, label: c('quote') },
|
|
22
|
+
actions.editHref === null ? null : { href: actions.editHref, label: c('edit') },
|
|
23
|
+
actions.rateHref === null ? null : { href: actions.rateHref, label: c('rate') },
|
|
24
|
+
actions.reportHref === null ? null : { href: actions.reportHref, label: c('report') },
|
|
18
25
|
].filter((action): action is Action => action !== null)
|
|
19
26
|
|
|
20
27
|
const staff: Action[] = [
|
|
21
|
-
actions.restoreHref === null ? null : { href: actions.restoreHref, label: '
|
|
22
|
-
actions.warnHref === null ? null : { href: actions.warnHref, label: '
|
|
23
|
-
actions.moderateHref === null ? null : { href: actions.moderateHref, label: '
|
|
28
|
+
actions.restoreHref === null ? null : { href: actions.restoreHref, label: c('restore') },
|
|
29
|
+
actions.warnHref === null ? null : { href: actions.warnHref, label: c('warn') },
|
|
30
|
+
actions.moderateHref === null ? null : { href: actions.moderateHref, label: c('moderate') },
|
|
24
31
|
].filter((action): action is Action => action !== null)
|
|
25
32
|
|
|
26
33
|
if (reader.length === 0 && staff.length === 0 && children === undefined) return null
|
|
27
34
|
|
|
28
35
|
return (
|
|
29
|
-
<nav aria-label=
|
|
36
|
+
<nav aria-label={c('ariaLabel')} className="flex flex-wrap items-center gap-1">
|
|
30
37
|
{reader.map((action) => (
|
|
31
38
|
<a key={action.href} href={action.href} className={ACTION}>
|
|
32
39
|
{action.label}
|
package/src/slots/post-bit.tsx
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import type { PostBitSlotModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
1
3
|
import { cn } from '@meith/ui'
|
|
2
|
-
import type { PostBitSlotModel } from '@meith/theme-kit'
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
Circle,
|
|
7
|
+
count,
|
|
6
8
|
LINK,
|
|
7
9
|
MUTED_LINK,
|
|
8
10
|
NUMERIC,
|
|
9
11
|
OnlineDot,
|
|
12
|
+
plural,
|
|
10
13
|
Stamp,
|
|
11
14
|
Tag,
|
|
12
15
|
UserRef,
|
|
13
|
-
count,
|
|
14
|
-
plural,
|
|
15
16
|
} from '../shared'
|
|
16
17
|
|
|
17
18
|
type Post = PostBitSlotModel['post']
|
|
@@ -22,10 +23,11 @@ const VISIBILITY_TINT = {
|
|
|
22
23
|
deleted: 'border-thread-deleted/50 bg-destructive/5',
|
|
23
24
|
} as const
|
|
24
25
|
|
|
25
|
-
function StatusBanner({ visibility }: { visibility: Post['visibility'] }) {
|
|
26
|
+
function StatusBanner({ visibility, copy }: { visibility: Post['visibility']; copy: SlotCopy }) {
|
|
26
27
|
if (visibility === 'visible') return null
|
|
27
28
|
|
|
28
29
|
const deleted = visibility === 'deleted'
|
|
30
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.postBit.${key}`)
|
|
29
31
|
|
|
30
32
|
return (
|
|
31
33
|
<p
|
|
@@ -36,8 +38,8 @@ function StatusBanner({ visibility }: { visibility: Post['visibility'] }) {
|
|
|
36
38
|
: 'border-thread-unapproved/30 text-thread-unapproved',
|
|
37
39
|
)}
|
|
38
40
|
>
|
|
39
|
-
{deleted ? '
|
|
40
|
-
<span className="font-normal text-muted-foreground">
|
|
41
|
+
{deleted ? c('deletedPost') : c('waitingApproval')}
|
|
42
|
+
<span className="font-normal text-muted-foreground">{c('moderatorsOnly')}</span>
|
|
41
43
|
</p>
|
|
42
44
|
)
|
|
43
45
|
}
|
|
@@ -114,8 +116,9 @@ function Attachments({ files }: { files: Post['attachments'] }) {
|
|
|
114
116
|
)
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
export function PostBit({ post, select, regions }: PostBitSlotModel) {
|
|
119
|
+
export function PostBit({ post, select, regions, copy }: PostBitSlotModel & { copy: SlotCopy }) {
|
|
118
120
|
const { author } = post
|
|
121
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.postBit.${key}`)
|
|
119
122
|
|
|
120
123
|
return (
|
|
121
124
|
<article
|
|
@@ -127,7 +130,7 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
|
|
|
127
130
|
VISIBILITY_TINT[post.visibility],
|
|
128
131
|
)}
|
|
129
132
|
>
|
|
130
|
-
<StatusBanner visibility={post.visibility} />
|
|
133
|
+
<StatusBanner visibility={post.visibility} copy={copy} />
|
|
131
134
|
|
|
132
135
|
<header className="flex items-start gap-3.5 px-5 pt-4">
|
|
133
136
|
{select !== null && (
|
|
@@ -164,7 +167,7 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
|
|
|
164
167
|
{author.isOnline && (
|
|
165
168
|
<>
|
|
166
169
|
<span aria-hidden="true">·</span>
|
|
167
|
-
<span className="text-moderation-approved">
|
|
170
|
+
<span className="text-moderation-approved">{c('online')}</span>
|
|
168
171
|
</>
|
|
169
172
|
)}
|
|
170
173
|
</p>
|
|
@@ -176,12 +179,14 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
|
|
|
176
179
|
)}
|
|
177
180
|
</div>
|
|
178
181
|
|
|
179
|
-
<p
|
|
180
|
-
{
|
|
182
|
+
<p
|
|
183
|
+
className={`shrink-0 text-right text-xs leading-relaxed text-muted-foreground ${NUMERIC}`}
|
|
184
|
+
>
|
|
185
|
+
{count(author.postCount)} {plural(author.postCount, c('post.one'), c('post.other'))}
|
|
181
186
|
{author.reputation != null && (
|
|
182
187
|
<>
|
|
183
188
|
<br />
|
|
184
|
-
{count(author.reputation)} rep
|
|
189
|
+
{count(author.reputation)} {c('rep')}
|
|
185
190
|
</>
|
|
186
191
|
)}
|
|
187
192
|
</p>
|
|
@@ -189,11 +194,11 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
|
|
|
189
194
|
|
|
190
195
|
{post.ignored !== null ? (
|
|
191
196
|
<p className="px-5 py-5 text-sm text-muted-foreground">
|
|
192
|
-
|
|
193
|
-
<span className="font-semibold text-foreground">{post.ignored.authorUsername}</span>.
|
|
194
|
-
|
|
197
|
+
{c('ignoring')}{' '}
|
|
198
|
+
<span className="font-semibold text-foreground">{post.ignored.authorUsername}</span>.{' '}
|
|
199
|
+
{c('hiddenNote')}{' '}
|
|
195
200
|
<a href={post.ignored.revealHref} className={LINK}>
|
|
196
|
-
|
|
201
|
+
{c('showAnyway')}
|
|
197
202
|
</a>
|
|
198
203
|
</p>
|
|
199
204
|
) : (
|
package/src/slots/post-form.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { PostFormModel } from '@meith/theme-kit'
|
|
1
|
+
import type { PostFormModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import { FEED, PAGE, PILL } from '../shared'
|
|
4
5
|
|
|
@@ -8,7 +9,10 @@ export function PostForm({
|
|
|
8
9
|
cancelLabel,
|
|
9
10
|
errorMessage,
|
|
10
11
|
regions,
|
|
11
|
-
|
|
12
|
+
copy,
|
|
13
|
+
}: PostFormModel & { copy: SlotCopy }) {
|
|
14
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.postForm.${key}`)
|
|
15
|
+
|
|
12
16
|
return (
|
|
13
17
|
<div className={`${PAGE} py-4 sm:py-6`}>
|
|
14
18
|
<div className={`${FEED} flex flex-col gap-4`}>
|
|
@@ -17,7 +21,7 @@ export function PostForm({
|
|
|
17
21
|
role="alert"
|
|
18
22
|
className="rounded-lg border border-destructive/30 bg-destructive/8 px-4 py-3 text-sm shadow-elevation"
|
|
19
23
|
>
|
|
20
|
-
<span className="font-semibold">
|
|
24
|
+
<span className="font-semibold">{c('cannotPost')}</span> {errorMessage}
|
|
21
25
|
</p>
|
|
22
26
|
)}
|
|
23
27
|
|
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
import type { RedirectNoticeModel } from '@meith/theme-kit'
|
|
1
|
+
import type { RedirectNoticeModel, 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 RedirectNotice({
|
|
6
|
+
export function RedirectNotice({
|
|
7
|
+
message,
|
|
8
|
+
targetHref,
|
|
9
|
+
delaySeconds,
|
|
10
|
+
copy,
|
|
11
|
+
}: RedirectNoticeModel & { copy: SlotCopy }) {
|
|
12
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.redirectNotice.${key}`)
|
|
13
|
+
|
|
6
14
|
return (
|
|
7
15
|
<section className="w-full max-w-lg rounded-lg border border-border bg-card text-card-foreground shadow-elevation">
|
|
8
16
|
<div className="px-5 py-6">
|
|
9
|
-
<p className="text-xs font-semibold text-muted-foreground">
|
|
10
|
-
<h1 className="mt-1 text-2xl leading-tight font-bold tracking-tight">
|
|
17
|
+
<p className="text-xs font-semibold text-muted-foreground">{c('redirecting')}</p>
|
|
18
|
+
<h1 className="mt-1 text-2xl leading-tight font-bold tracking-tight">{c('pleaseWait')}</h1>
|
|
11
19
|
<p className="mt-2 text-sm text-muted-foreground">{message}</p>
|
|
12
20
|
|
|
13
21
|
<div className="mt-5 flex flex-wrap items-center gap-3">
|
|
14
22
|
<a href={targetHref} className={PILL_PRIMARY}>
|
|
15
|
-
|
|
23
|
+
{c('continueNow')}
|
|
16
24
|
</a>
|
|
17
25
|
<span className={`text-xs text-muted-foreground ${NUMERIC}`}>
|
|
18
|
-
|
|
26
|
+
{c('continuing')} {delaySeconds}{' '}
|
|
27
|
+
{delaySeconds === 1 ? c('second.one') : c('second.other')}.
|
|
19
28
|
</span>
|
|
20
29
|
</div>
|
|
21
30
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { OptionModel, SearchAdvancedModel, SearchFormModel } from '@meith/theme-kit'
|
|
1
|
+
import type { OptionModel, SearchAdvancedModel, SearchFormModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import { PILL_PRIMARY } from '../shared'
|
|
4
5
|
|
|
@@ -102,17 +103,23 @@ export function SearchForm({
|
|
|
102
103
|
hint,
|
|
103
104
|
errorMessage,
|
|
104
105
|
advanced,
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
copy,
|
|
107
|
+
}: SearchFormModel & { copy: SlotCopy }) {
|
|
108
|
+
const described = [
|
|
109
|
+
hint === null ? null : 'search-hint',
|
|
110
|
+
errorMessage === null ? null : 'search-error',
|
|
111
|
+
]
|
|
107
112
|
.filter((id): id is string => id !== null)
|
|
108
113
|
.join(' ')
|
|
109
114
|
|
|
115
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.searchForm.${key}`)
|
|
116
|
+
|
|
110
117
|
return (
|
|
111
118
|
<section className="rounded-lg border border-border bg-card text-card-foreground shadow-elevation">
|
|
112
119
|
<form method="get" action={action} className="flex flex-col gap-4 px-4 py-4">
|
|
113
120
|
<div>
|
|
114
121
|
<label htmlFor="search-query" className={LABEL}>
|
|
115
|
-
|
|
122
|
+
{c('searchFor')}
|
|
116
123
|
</label>
|
|
117
124
|
<input
|
|
118
125
|
id="search-query"
|
|
@@ -121,7 +128,7 @@ export function SearchForm({
|
|
|
121
128
|
defaultValue={query}
|
|
122
129
|
maxLength={maxQueryLength}
|
|
123
130
|
autoComplete="off"
|
|
124
|
-
placeholder=
|
|
131
|
+
placeholder={c('placeholder')}
|
|
125
132
|
aria-invalid={errorMessage === null ? undefined : true}
|
|
126
133
|
{...(described === '' ? {} : { 'aria-describedby': described })}
|
|
127
134
|
className="w-full rounded-full border border-input bg-surface px-4 py-2.5 text-sm text-foreground transition-colors focus-visible:border-ring"
|
|
@@ -141,15 +148,15 @@ export function SearchForm({
|
|
|
141
148
|
</div>
|
|
142
149
|
|
|
143
150
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
144
|
-
<Choice id="search-forum" label=
|
|
145
|
-
<Choice id="search-sort" label=
|
|
151
|
+
<Choice id="search-forum" label={c('in')} name={fields.forum} options={forums} />
|
|
152
|
+
<Choice id="search-sort" label={c('sortBy')} name={fields.sort} options={sorts} />
|
|
146
153
|
</div>
|
|
147
154
|
|
|
148
155
|
{advanced !== undefined && <Advanced {...advanced} />}
|
|
149
156
|
|
|
150
157
|
<div>
|
|
151
158
|
<button type="submit" className={PILL_PRIMARY}>
|
|
152
|
-
|
|
159
|
+
{c('search')}
|
|
153
160
|
</button>
|
|
154
161
|
</div>
|
|
155
162
|
</form>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { OptionModel, SearchRefineModel, SearchResultsModel } from '@meith/theme-kit'
|
|
1
|
+
import type { OptionModel, SearchRefineModel, SearchResultsModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
4
|
import { FEED, LINK, PAGE, PILL, PILL_PRIMARY, Stamp } from '../shared'
|
|
4
5
|
|
|
@@ -12,26 +13,27 @@ export function SearchResults({
|
|
|
12
13
|
within,
|
|
13
14
|
refine,
|
|
14
15
|
regions,
|
|
15
|
-
|
|
16
|
+
copy,
|
|
17
|
+
}: SearchResultsModel & { copy: SlotCopy }) {
|
|
18
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.searchResults.${key}`)
|
|
19
|
+
|
|
16
20
|
return (
|
|
17
21
|
<main id="board-content" tabIndex={-1} className={`${PAGE} flex-1 py-4 sm:py-6`}>
|
|
18
22
|
<div className={`${FEED} flex flex-col gap-4`}>
|
|
19
23
|
<header className="rounded-lg border border-border bg-card px-4 py-3.5 shadow-elevation">
|
|
20
24
|
<h1 className="text-xl leading-tight font-bold tracking-tight text-balance">
|
|
21
|
-
|
|
25
|
+
{c('resultsFor')} “{terms}”
|
|
22
26
|
</h1>
|
|
23
27
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
24
|
-
|
|
25
|
-
every time this page is opened, so they can change.
|
|
28
|
+
{c('searched')} <Stamp at={searchedAt} />. {c('searchedNote')}
|
|
26
29
|
</p>
|
|
27
30
|
</header>
|
|
28
31
|
|
|
29
|
-
{refine !== undefined && <Refine {...refine} />}
|
|
32
|
+
{refine !== undefined && <Refine {...refine} copy={copy} />}
|
|
30
33
|
|
|
31
34
|
{hits.length === 0 ? (
|
|
32
35
|
<p className="rounded-lg border border-border bg-card px-4 py-6 text-center text-sm text-muted-foreground shadow-elevation">
|
|
33
|
-
|
|
34
|
-
spelling.
|
|
36
|
+
{c('noHits')}
|
|
35
37
|
</p>
|
|
36
38
|
) : (
|
|
37
39
|
<ul className="flex flex-col gap-3">
|
|
@@ -63,11 +65,7 @@ export function SearchResults({
|
|
|
63
65
|
))}
|
|
64
66
|
|
|
65
67
|
<section className="rounded-lg border border-border bg-card shadow-elevation">
|
|
66
|
-
<form
|
|
67
|
-
method="get"
|
|
68
|
-
action={within.action}
|
|
69
|
-
className="flex flex-col gap-3 px-4 py-4"
|
|
70
|
-
>
|
|
68
|
+
<form method="get" action={within.action} className="flex flex-col gap-3 px-4 py-4">
|
|
71
69
|
{(within.hidden ?? []).map((field) => (
|
|
72
70
|
<input
|
|
73
71
|
key={`${field.name}-${field.value}`}
|
|
@@ -107,7 +105,7 @@ export function SearchResults({
|
|
|
107
105
|
</section>
|
|
108
106
|
|
|
109
107
|
<a href={newSearchHref} className={`text-sm font-semibold ${LINK}`}>
|
|
110
|
-
|
|
108
|
+
{c('newSearch')}
|
|
111
109
|
</a>
|
|
112
110
|
</div>
|
|
113
111
|
</main>
|
|
@@ -125,7 +123,10 @@ function Refine({
|
|
|
125
123
|
submitLabel,
|
|
126
124
|
applied,
|
|
127
125
|
clearHref,
|
|
128
|
-
|
|
126
|
+
copy,
|
|
127
|
+
}: SearchRefineModel & { copy: SlotCopy }) {
|
|
128
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.searchResults.${key}`)
|
|
129
|
+
|
|
129
130
|
return (
|
|
130
131
|
<section
|
|
131
132
|
aria-label={label}
|
|
@@ -162,7 +163,7 @@ function Refine({
|
|
|
162
163
|
>
|
|
163
164
|
{chip.label}
|
|
164
165
|
<span aria-hidden="true">×</span>
|
|
165
|
-
<span className="sr-only"
|
|
166
|
+
<span className="sr-only">{c('removeFilter')}</span>
|
|
166
167
|
</a>
|
|
167
168
|
))}
|
|
168
169
|
</div>
|
|
@@ -182,7 +183,7 @@ function Refine({
|
|
|
182
183
|
|
|
183
184
|
{clearHref !== null && (
|
|
184
185
|
<a href={clearHref} className={`text-xs font-semibold ${LINK}`}>
|
|
185
|
-
|
|
186
|
+
{c('clearFilters')}
|
|
186
187
|
</a>
|
|
187
188
|
)}
|
|
188
189
|
</form>
|
package/src/slots/shell.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ShellModel } from '@meith/theme-kit'
|
|
1
|
+
import type { ShellModel, SlotCopy } from '@meith/theme-kit'
|
|
2
|
+
import { fromSlotCopy } from '@meith/theme-kit'
|
|
2
3
|
|
|
3
|
-
export function Shell({ viewer, children }: ShellModel) {
|
|
4
|
+
export function Shell({ viewer, children, copy }: ShellModel & { copy: SlotCopy }) {
|
|
4
5
|
return (
|
|
5
6
|
<div
|
|
6
7
|
className="flex min-h-dvh flex-col bg-background text-foreground"
|
|
@@ -10,7 +11,7 @@ export function Shell({ viewer, children }: ShellModel) {
|
|
|
10
11
|
href="#board-content"
|
|
11
12
|
className="sr-only focus-visible:not-sr-only focus-visible:absolute focus-visible:top-3 focus-visible:left-3 focus-visible:z-50 focus-visible:inline-flex focus-visible:h-9 focus-visible:items-center focus-visible:rounded-full focus-visible:bg-primary focus-visible:px-4 focus-visible:text-sm focus-visible:font-semibold focus-visible:text-primary-foreground focus-visible:shadow-elevation"
|
|
12
13
|
>
|
|
13
|
-
|
|
14
|
+
{fromSlotCopy(copy, 'phasebook.shell.skipToContent')}
|
|
14
15
|
</a>
|
|
15
16
|
{children}
|
|
16
17
|
</div>
|
|
@@ -1,10 +1,13 @@
|
|
|
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
|
-
import {
|
|
4
|
+
import { count, NUMERIC, plural } from '../shared'
|
|
4
5
|
|
|
5
|
-
export function SubforumList({ forums }: SubforumListModel) {
|
|
6
|
+
export function SubforumList({ forums, copy }: SubforumListModel & { copy: SlotCopy }) {
|
|
6
7
|
if (forums.length === 0) return null
|
|
7
8
|
|
|
9
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.subforumList.${key}`)
|
|
10
|
+
|
|
8
11
|
return (
|
|
9
12
|
<section
|
|
10
13
|
aria-labelledby="subforums-heading"
|
|
@@ -14,7 +17,7 @@ export function SubforumList({ forums }: SubforumListModel) {
|
|
|
14
17
|
id="subforums-heading"
|
|
15
18
|
className="px-4 pt-3 pb-1 text-[0.9375rem] font-semibold text-muted-foreground"
|
|
16
19
|
>
|
|
17
|
-
|
|
20
|
+
{c('title')}
|
|
18
21
|
</h2>
|
|
19
22
|
|
|
20
23
|
<ul className="flex flex-wrap gap-2 px-4 pt-1 pb-3">
|
|
@@ -27,7 +30,8 @@ export function SubforumList({ forums }: SubforumListModel) {
|
|
|
27
30
|
{forum.title}
|
|
28
31
|
{forum.type !== 'link' && (
|
|
29
32
|
<span className={`text-xs font-medium text-muted-foreground ${NUMERIC}`}>
|
|
30
|
-
{count(forum.threadCount)}
|
|
33
|
+
{count(forum.threadCount)}{' '}
|
|
34
|
+
{plural(forum.threadCount, c('thread.one'), c('thread.other'))}
|
|
31
35
|
</span>
|
|
32
36
|
)}
|
|
33
37
|
</a>
|
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
|
-
import { Circle, MUTED_LINK, NUMERIC, Prefix, Stamp, Tag, UserRef
|
|
4
|
+
import { Circle, count, MUTED_LINK, NUMERIC, Prefix, plural, Stamp, Tag, UserRef } from '../shared'
|
|
5
|
+
|
|
6
|
+
export function ThreadRow({ thread, select, copy }: ThreadRowSlotModel & { copy: SlotCopy }) {
|
|
7
|
+
const c = (key: string) => fromSlotCopy(copy, `phasebook.threadRow.${key}`)
|
|
4
8
|
|
|
5
|
-
export function ThreadRow({ thread, select }: ThreadRowSlotModel) {
|
|
6
9
|
return (
|
|
7
10
|
<li
|
|
8
11
|
data-unread={thread.isUnread ? '' : undefined}
|
|
@@ -35,9 +38,9 @@ export function ThreadRow({ thread, select }: ThreadRowSlotModel) {
|
|
|
35
38
|
<div className="min-w-0 flex-1">
|
|
36
39
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
37
40
|
{thread.prefix !== null && <Prefix prefix={thread.prefix} />}
|
|
38
|
-
{thread.isSticky && <Tag token="thread-pinned">
|
|
39
|
-
{thread.isLocked && <Tag token="thread-locked">
|
|
40
|
-
{thread.isMoved && <Tag token="thread-moved">
|
|
41
|
+
{thread.isSticky && <Tag token="thread-pinned">{c('pinned')}</Tag>}
|
|
42
|
+
{thread.isLocked && <Tag token="thread-locked">{c('locked')}</Tag>}
|
|
43
|
+
{thread.isMoved && <Tag token="thread-moved">{c('moved')}</Tag>}
|
|
41
44
|
</div>
|
|
42
45
|
|
|
43
46
|
<a
|
|
@@ -46,29 +49,29 @@ export function ThreadRow({ thread, select }: ThreadRowSlotModel) {
|
|
|
46
49
|
>
|
|
47
50
|
{thread.title}
|
|
48
51
|
</a>
|
|
49
|
-
{thread.isUnread && <span className="sr-only"> (
|
|
52
|
+
{thread.isUnread && <span className="sr-only"> {c('newPosts')}</span>}
|
|
50
53
|
|
|
51
54
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
52
|
-
|
|
55
|
+
{c('startedBy')} <UserRef user={thread.author} className="text-xs font-medium" />
|
|
53
56
|
</p>
|
|
54
57
|
</div>
|
|
55
58
|
</div>
|
|
56
59
|
|
|
57
60
|
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-1 border-t border-border px-4 py-2 text-xs text-muted-foreground">
|
|
58
61
|
<span className={NUMERIC}>
|
|
59
|
-
{count(thread.replyCount)} {plural(thread.replyCount, 'reply', '
|
|
60
|
-
{count(thread.viewCount)} {plural(thread.viewCount, 'view', '
|
|
62
|
+
{count(thread.replyCount)} {plural(thread.replyCount, c('reply.one'), c('reply.other'))} ·{' '}
|
|
63
|
+
{count(thread.viewCount)} {plural(thread.viewCount, c('view.one'), c('view.other'))}
|
|
61
64
|
</span>
|
|
62
65
|
|
|
63
66
|
<span className="min-w-0 truncate">
|
|
64
67
|
{thread.lastPost === null ? (
|
|
65
|
-
<span className="text-thread-moved">
|
|
68
|
+
<span className="text-thread-moved">{c('noRepliesYet')}</span>
|
|
66
69
|
) : (
|
|
67
70
|
<>
|
|
68
71
|
<a href={thread.lastPost.href} className={`font-semibold ${MUTED_LINK}`}>
|
|
69
|
-
|
|
72
|
+
{c('latestReply')}
|
|
70
73
|
</a>
|
|
71
|
-
{'
|
|
74
|
+
{` ${c('by')} `}
|
|
72
75
|
<UserRef user={thread.lastPost.author} className="text-xs font-medium" />
|
|
73
76
|
{' · '}
|
|
74
77
|
<Stamp at={thread.lastPost.at} />
|