@meith/theme-default 0.31.0 → 0.33.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 +2 -0
- package/src/messages/en.json +2 -0
- package/src/shared.tsx +12 -6
- package/src/slots/announcement.tsx +2 -2
- package/src/slots/auth-page.tsx +1 -1
- package/src/slots/board-index.tsx +2 -2
- package/src/slots/board-stats.tsx +1 -1
- package/src/slots/category-block.tsx +3 -3
- package/src/slots/discovery-view.tsx +2 -2
- package/src/slots/editor-toolbar.tsx +23 -6
- package/src/slots/footer.tsx +3 -3
- package/src/slots/forum-display.tsx +7 -3
- package/src/slots/forum-row.tsx +7 -7
- package/src/slots/header.tsx +119 -19
- package/src/slots/latest-threads.tsx +1 -1
- package/src/slots/member-profile.tsx +1 -3
- package/src/slots/navigation.tsx +10 -9
- package/src/slots/panel-nav.tsx +1 -1
- package/src/slots/panel-shell.tsx +1 -1
- package/src/slots/post-actions.tsx +8 -2
- package/src/slots/post-bit.tsx +55 -40
- package/src/slots/thread-row.tsx +44 -33
- package/src/slots/thread-view.tsx +22 -12
- package/src/theme.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/theme-default",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Meith's default theme — every slot and every token, and the reference for what a complete theme fills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@meith/theme-kit": "^0.
|
|
24
|
-
"@meith/ui": "^0.
|
|
23
|
+
"@meith/theme-kit": "^0.33.0",
|
|
24
|
+
"@meith/ui": "^0.33.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"react": "^19.2.0"
|
package/src/copy.ts
CHANGED
package/src/messages/en.json
CHANGED
|
@@ -170,6 +170,8 @@
|
|
|
170
170
|
"default.threadView.view.one": "view",
|
|
171
171
|
"default.threadView.view.other": "views",
|
|
172
172
|
"default.threadView.viewsLabel": "Views",
|
|
173
|
+
"default.threadView.watch": "Watch",
|
|
174
|
+
"default.threadView.watching": "Watching",
|
|
173
175
|
"default.userPanel.new": "new",
|
|
174
176
|
"default.userPanel.signedIn": "Signed in",
|
|
175
177
|
"default.userPanel.unread": "unread",
|
package/src/shared.tsx
CHANGED
|
@@ -17,12 +17,18 @@ export function pageAt(width: 'max-w-3xl' | 'max-w-4xl'): string {
|
|
|
17
17
|
return `mx-auto w-full ${width} px-4 sm:px-6`
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export const LINK =
|
|
20
|
+
export const LINK =
|
|
21
|
+
'transition-colors hover:text-primary hover:underline underline-offset-2 decoration-1 decoration-primary'
|
|
21
22
|
|
|
22
|
-
export const MUTED_LINK = `text-muted-foreground
|
|
23
|
+
export const MUTED_LINK = `text-muted-foreground ${LINK}`
|
|
23
24
|
|
|
24
25
|
export function UserRef({ user, className }: { user: UserRefModel; className?: string }) {
|
|
25
|
-
const classes = cn(
|
|
26
|
+
const classes = cn(
|
|
27
|
+
'font-medium',
|
|
28
|
+
user.nameClass == null && 'text-primary',
|
|
29
|
+
className,
|
|
30
|
+
user.nameClass,
|
|
31
|
+
)
|
|
26
32
|
if (user.profileHref === null) {
|
|
27
33
|
return <span className={classes}>{user.username}</span>
|
|
28
34
|
}
|
|
@@ -50,8 +56,8 @@ export function ReadSpacer() {
|
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
export function isEmptyRegion(node: React.ReactNode): boolean {
|
|
53
|
-
if (node === null || node === undefined || node === false) return true
|
|
54
|
-
return Array.isArray(node) && node.
|
|
59
|
+
if (node === null || node === undefined || node === false || node === '') return true
|
|
60
|
+
return Array.isArray(node) && node.every((child) => isEmptyRegion(child))
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export const NUMERIC = 'tabular-nums'
|
|
@@ -96,7 +102,7 @@ export function Counts({ items, className }: { items: readonly CountItem[]; clas
|
|
|
96
102
|
<div key={item.label}>
|
|
97
103
|
<dt className="sr-only">{item.label}</dt>
|
|
98
104
|
<dd>
|
|
99
|
-
<span className={cn('font-
|
|
105
|
+
<span className={cn('font-semibold text-primary', NUMERIC)}>{item.value.label}</span>{' '}
|
|
100
106
|
{item.value.value === 1 ? item.one : item.many}
|
|
101
107
|
</dd>
|
|
102
108
|
</div>
|
|
@@ -15,7 +15,7 @@ export function Announcement({
|
|
|
15
15
|
const c = (key: string) => fromSlotCopy(copy, `default.announcement.${key}`)
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
|
-
<Card as="article" className="border-l-4 border-l-
|
|
18
|
+
<Card as="article" className="border-l-4 border-l-primary">
|
|
19
19
|
<CardContent className="flex flex-col gap-2 p-4">
|
|
20
20
|
<h2 className="text-lg font-semibold tracking-tight text-balance">{title}</h2>
|
|
21
21
|
|
|
@@ -28,7 +28,7 @@ export function Announcement({
|
|
|
28
28
|
c('posted')
|
|
29
29
|
) : (
|
|
30
30
|
<>
|
|
31
|
-
{c('postedBy')} <UserRef user={postedBy}
|
|
31
|
+
{c('postedBy')} <UserRef user={postedBy} />
|
|
32
32
|
</>
|
|
33
33
|
)}{' '}
|
|
34
34
|
<Stamp at={postedAt} />
|
package/src/slots/auth-page.tsx
CHANGED
|
@@ -9,7 +9,7 @@ export function AuthPage({ title, alert, links, regions }: AuthPageModel) {
|
|
|
9
9
|
tabIndex={-1}
|
|
10
10
|
className="flex flex-1 flex-col items-center justify-center px-6 py-12"
|
|
11
11
|
>
|
|
12
|
-
<div className="flex w-full max-w-sm flex-col gap-6 rounded-lg border border-border bg-card p-6 text-card-foreground shadow-
|
|
12
|
+
<div className="flex w-full max-w-sm flex-col gap-6 rounded-lg border border-border bg-card p-6 text-card-foreground shadow-elevation">
|
|
13
13
|
<div className="flex flex-col gap-1">
|
|
14
14
|
<h1 className="font-heading text-2xl font-semibold text-foreground">{title}</h1>
|
|
15
15
|
{regions.lede !== undefined && (
|
|
@@ -21,8 +21,8 @@ export function BoardIndex({
|
|
|
21
21
|
)}
|
|
22
22
|
|
|
23
23
|
{markAllReadAction !== null && (
|
|
24
|
-
<form action={markAllReadAction} method="post" className="self-end">
|
|
25
|
-
<button type="submit" className={buttonVariants({ variant: '
|
|
24
|
+
<form action={markAllReadAction} method="post" className="-mb-2 self-end">
|
|
25
|
+
<button type="submit" className={buttonVariants({ variant: 'outline', size: 'sm' })}>
|
|
26
26
|
{c('markAllRead')}
|
|
27
27
|
</button>
|
|
28
28
|
</form>
|
|
@@ -8,9 +8,9 @@ export function CategoryBlock({ category, children }: CategoryBlockModel) {
|
|
|
8
8
|
|
|
9
9
|
return (
|
|
10
10
|
<Card aria-labelledby={headingId}>
|
|
11
|
-
<CardHeader className="flex-col items-start gap-0.5">
|
|
12
|
-
<CardTitle id={headingId}>
|
|
13
|
-
<a href={category.href} className={LINK}>
|
|
11
|
+
<CardHeader className="flex-col items-start gap-0.5 border-b-primary/15 bg-primary/6">
|
|
12
|
+
<CardTitle id={headingId} className="text-primary">
|
|
13
|
+
<a href={category.href} className={`${LINK} hover:text-primary-hover`}>
|
|
14
14
|
{category.title}
|
|
15
15
|
</a>
|
|
16
16
|
</CardTitle>
|
|
@@ -21,7 +21,7 @@ function Tabs({ label, tabs }: { label: string; tabs: readonly TabModel[] }) {
|
|
|
21
21
|
className={cn(
|
|
22
22
|
TAB,
|
|
23
23
|
tab.isCurrent
|
|
24
|
-
? 'bg-card font-semibold text-
|
|
24
|
+
? 'bg-card font-semibold text-primary shadow-sm'
|
|
25
25
|
: 'font-medium text-muted-foreground hover:text-foreground',
|
|
26
26
|
)}
|
|
27
27
|
>
|
|
@@ -80,7 +80,7 @@ export function DiscoveryView({
|
|
|
80
80
|
{rows.map((row) => (
|
|
81
81
|
<li
|
|
82
82
|
key={row.threadId}
|
|
83
|
-
className="flex flex-col gap-1.5 px-4 py-3.5 transition-colors hover:bg-
|
|
83
|
+
className="flex flex-col gap-1.5 px-4 py-3.5 transition-colors hover:bg-muted/60 sm:flex-row sm:items-center sm:justify-between sm:gap-6"
|
|
84
84
|
>
|
|
85
85
|
<div className="flex min-w-0 flex-col gap-1">
|
|
86
86
|
<a
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
applyEditorTag,
|
|
5
|
+
applyInsertion,
|
|
5
6
|
type EditorTag,
|
|
7
|
+
type EditorToolbarButtonModel,
|
|
6
8
|
type EditorToolbarModel,
|
|
7
9
|
type SlotCopy,
|
|
8
10
|
} from '@meith/theme-kit'
|
|
@@ -12,20 +14,34 @@ const GLYPHS: Readonly<Record<EditorTag, string>> = {
|
|
|
12
14
|
italic: 'I',
|
|
13
15
|
strikethrough: 'S',
|
|
14
16
|
link: 'Link',
|
|
17
|
+
image: 'Image',
|
|
15
18
|
quote: '“”',
|
|
16
19
|
code: '</>',
|
|
17
20
|
spoiler: 'Spoiler',
|
|
18
21
|
bulletedList: '•',
|
|
19
22
|
numberedList: '1.',
|
|
23
|
+
taskList: '☑',
|
|
20
24
|
heading: 'H',
|
|
25
|
+
table: 'Table',
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
const BUTTON =
|
|
24
29
|
'min-w-8 rounded px-2 py-1 text-xs font-medium text-muted-foreground hover:bg-background hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
25
30
|
|
|
26
|
-
function
|
|
31
|
+
function runButton(textareaId: string, button: EditorToolbarButtonModel): void {
|
|
27
32
|
const field = document.getElementById(textareaId)
|
|
28
|
-
if (field instanceof HTMLTextAreaElement)
|
|
33
|
+
if (!(field instanceof HTMLTextAreaElement)) return
|
|
34
|
+
|
|
35
|
+
if (button.tag !== null) {
|
|
36
|
+
applyEditorTag(field, button.tag, button.placeholder)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (button.insertion !== null) applyInsertion(field, button.insertion)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function glyphFor(button: EditorToolbarButtonModel): string {
|
|
44
|
+
return button.tag === null ? button.label.charAt(0).toUpperCase() : GLYPHS[button.tag]
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
export function EditorToolbar({
|
|
@@ -40,18 +56,19 @@ export function EditorToolbar({
|
|
|
40
56
|
aria-label={groupLabel}
|
|
41
57
|
className="flex flex-wrap gap-0.5 border-b border-border bg-muted/40 p-2"
|
|
42
58
|
>
|
|
43
|
-
{buttons.map((button) => (
|
|
59
|
+
{buttons.map((button, index) => (
|
|
44
60
|
<button
|
|
45
|
-
|
|
61
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: a plugin's button carries no id of its own, so the index disambiguates two sharing a tag or label; the list is server-rendered in order and never reordered on the client
|
|
62
|
+
key={`${button.tag ?? button.label}-${index}`}
|
|
46
63
|
type="button"
|
|
47
64
|
onMouseDown={(event) => event.preventDefault()}
|
|
48
|
-
onClick={() =>
|
|
65
|
+
onClick={() => runButton(textareaId, button)}
|
|
49
66
|
title={button.title}
|
|
50
67
|
aria-label={button.label}
|
|
51
68
|
{...(button.keyShortcut === null ? {} : { 'aria-keyshortcuts': button.keyShortcut })}
|
|
52
69
|
className={BUTTON}
|
|
53
70
|
>
|
|
54
|
-
<span aria-hidden="true">{
|
|
71
|
+
<span aria-hidden="true">{glyphFor(button)}</span>
|
|
55
72
|
</button>
|
|
56
73
|
))}
|
|
57
74
|
|
package/src/slots/footer.tsx
CHANGED
|
@@ -17,15 +17,15 @@ export function Footer({
|
|
|
17
17
|
<footer className="mt-auto border-t border-border bg-card">
|
|
18
18
|
{regions?.controls && (
|
|
19
19
|
<div
|
|
20
|
-
className={`${PAGE} flex flex-wrap items-center justify-end gap-x-6 gap-y-
|
|
20
|
+
className={`${PAGE} flex flex-wrap items-center justify-end gap-x-6 gap-y-3 border-b border-border py-3`}
|
|
21
21
|
>
|
|
22
22
|
{regions.controls}
|
|
23
23
|
</div>
|
|
24
24
|
)}
|
|
25
25
|
<div
|
|
26
|
-
className={`${PAGE} flex flex-col gap-
|
|
26
|
+
className={`${PAGE} flex flex-col gap-3 py-5 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between sm:gap-6`}
|
|
27
27
|
>
|
|
28
|
-
<span className="font-
|
|
28
|
+
<span className="font-semibold text-primary">{boardTitle}</span>
|
|
29
29
|
|
|
30
30
|
{links.length > 0 && (
|
|
31
31
|
<nav aria-label={c('nav')} className="flex flex-wrap gap-x-4 gap-y-1">
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
buttonVariants,
|
|
5
5
|
Card,
|
|
6
6
|
CardRows,
|
|
7
|
+
cn,
|
|
7
8
|
Empty,
|
|
8
9
|
EmptyAction,
|
|
9
10
|
EmptyDescription,
|
|
@@ -50,16 +51,19 @@ export function ForumDisplay({
|
|
|
50
51
|
)}
|
|
51
52
|
</div>
|
|
52
53
|
|
|
53
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
54
|
+
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
|
54
55
|
{markReadAction !== null && (
|
|
55
56
|
<form action={markReadAction} method="post">
|
|
56
|
-
<button type="submit" className={buttonVariants({ variant: '
|
|
57
|
+
<button type="submit" className={buttonVariants({ variant: 'outline' })}>
|
|
57
58
|
{c('markRead')}
|
|
58
59
|
</button>
|
|
59
60
|
</form>
|
|
60
61
|
)}
|
|
61
62
|
{newThreadHref !== null && (
|
|
62
|
-
<a
|
|
63
|
+
<a
|
|
64
|
+
href={newThreadHref}
|
|
65
|
+
className={cn(buttonVariants({ variant: 'primary' }), 'ms-auto sm:ms-0')}
|
|
66
|
+
>
|
|
63
67
|
{c('newThread')}
|
|
64
68
|
</a>
|
|
65
69
|
)}
|
package/src/slots/forum-row.tsx
CHANGED
|
@@ -12,7 +12,7 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
|
|
|
12
12
|
<li
|
|
13
13
|
data-unread={forum.isUnread ? '' : undefined}
|
|
14
14
|
className={
|
|
15
|
-
'grid grid-cols-[auto_minmax(0,1fr)] gap-x-2.5 gap-y-
|
|
15
|
+
'grid grid-cols-[auto_minmax(0,1fr)] gap-x-2.5 gap-y-1.5 px-4 py-3 transition-colors hover:bg-muted/60' +
|
|
16
16
|
(isLink ? '' : ' md:grid-cols-[auto_minmax(0,1fr)_9rem_15rem] md:items-center md:gap-x-4')
|
|
17
17
|
}
|
|
18
18
|
>
|
|
@@ -47,9 +47,9 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
|
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
49
|
{!isLink && (
|
|
50
|
-
|
|
50
|
+
<div className="col-start-2 flex min-w-0 flex-wrap items-baseline gap-x-4 gap-y-1 text-xs text-muted-foreground md:contents">
|
|
51
51
|
<Counts
|
|
52
|
-
className="
|
|
52
|
+
className="md:col-start-3 md:justify-end"
|
|
53
53
|
items={[
|
|
54
54
|
{
|
|
55
55
|
label: c('threadsLabel'),
|
|
@@ -66,25 +66,25 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
|
|
|
66
66
|
]}
|
|
67
67
|
/>
|
|
68
68
|
|
|
69
|
-
<div className="
|
|
69
|
+
<div className="flex min-w-0 max-w-full flex-wrap gap-x-1 md:col-start-4 md:block">
|
|
70
70
|
{forum.lastPost === null ? (
|
|
71
71
|
<span className="text-forum-read">{c('noPostsYet')}</span>
|
|
72
72
|
) : (
|
|
73
73
|
<>
|
|
74
74
|
<a
|
|
75
75
|
href={forum.lastPost.href}
|
|
76
|
-
className={`
|
|
76
|
+
className={`max-w-full truncate font-medium text-foreground md:block ${LINK}`}
|
|
77
77
|
>
|
|
78
78
|
{forum.lastPost.threadTitle}
|
|
79
79
|
</a>
|
|
80
|
-
<span className="mt-0.5 block truncate">
|
|
80
|
+
<span className="md:mt-0.5 md:block md:truncate">
|
|
81
81
|
{c('by')} <UserRef user={forum.lastPost.author} className="font-normal" />{' '}
|
|
82
82
|
{c('dot')} <Stamp at={forum.lastPost.at} />
|
|
83
83
|
</span>
|
|
84
84
|
</>
|
|
85
85
|
)}
|
|
86
86
|
</div>
|
|
87
|
-
|
|
87
|
+
</div>
|
|
88
88
|
)}
|
|
89
89
|
</li>
|
|
90
90
|
)
|
package/src/slots/header.tsx
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { HeaderModel, SlotCopy } from '@meith/theme-kit'
|
|
2
2
|
import { fromSlotCopy, linkTarget } from '@meith/theme-kit'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { PAGE } from '../shared'
|
|
5
|
+
|
|
6
|
+
const DESKTOP_LINK =
|
|
7
|
+
'inline-flex h-10 items-center rounded-md px-3 font-medium text-muted-foreground transition-colors pointer-coarse:h-11 hover:bg-primary/8 hover:text-primary'
|
|
8
|
+
|
|
9
|
+
const MOBILE_LINK =
|
|
10
|
+
'flex min-h-11 items-center rounded-md px-3 text-sm font-medium text-muted-foreground hover:bg-primary/8 hover:text-primary'
|
|
11
|
+
|
|
12
|
+
const SUMMARY =
|
|
13
|
+
'cursor-default list-none select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none'
|
|
5
14
|
|
|
6
15
|
function BoardMark({ boardTitle, logo }: Pick<HeaderModel, 'boardTitle' | 'logo'>) {
|
|
7
16
|
if (logo === undefined) return <>{boardTitle}</>
|
|
@@ -29,13 +38,13 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
|
|
|
29
38
|
if (items === undefined || items.length === 0) return null
|
|
30
39
|
|
|
31
40
|
return (
|
|
32
|
-
<ul className="invisible absolute top-full left-0 z-30 min-w-
|
|
41
|
+
<ul className="invisible absolute top-full left-0 z-30 min-w-48 rounded-md border border-border bg-card p-1 opacity-0 shadow-elevation transition-opacity group-focus-within:visible group-focus-within:opacity-100 group-hover:visible group-hover:opacity-100">
|
|
33
42
|
{items.map((child) => (
|
|
34
43
|
<li key={child.href}>
|
|
35
44
|
<a
|
|
36
45
|
href={child.href}
|
|
37
46
|
{...linkTarget(child)}
|
|
38
|
-
className="
|
|
47
|
+
className="flex items-center rounded-sm px-2.5 py-1.5 text-muted-foreground pointer-coarse:min-h-11 hover:bg-primary/8 hover:text-primary"
|
|
39
48
|
>
|
|
40
49
|
{child.label}
|
|
41
50
|
</a>
|
|
@@ -45,6 +54,82 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
|
|
|
45
54
|
)
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
function Chevron({ className }: { className: string }) {
|
|
58
|
+
return (
|
|
59
|
+
<svg
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
viewBox="0 0 12 12"
|
|
62
|
+
className={className}
|
|
63
|
+
fill="none"
|
|
64
|
+
stroke="currentColor"
|
|
65
|
+
strokeWidth="1.5"
|
|
66
|
+
strokeLinecap="round"
|
|
67
|
+
strokeLinejoin="round"
|
|
68
|
+
>
|
|
69
|
+
<path d="m4.5 3 3 3-3 3" />
|
|
70
|
+
</svg>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function MenuGlyph() {
|
|
75
|
+
return (
|
|
76
|
+
<svg
|
|
77
|
+
aria-hidden="true"
|
|
78
|
+
viewBox="0 0 20 20"
|
|
79
|
+
className="size-5"
|
|
80
|
+
fill="none"
|
|
81
|
+
stroke="currentColor"
|
|
82
|
+
strokeWidth="1.75"
|
|
83
|
+
strokeLinecap="round"
|
|
84
|
+
>
|
|
85
|
+
<path className="group-open/menu:hidden" d="M3 5.5h14M3 10h14M3 14.5h14" />
|
|
86
|
+
<path className="hidden group-open/menu:block" d="m5 5 10 10M15 5 5 15" />
|
|
87
|
+
</svg>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function MobileNavItem({ item }: { item: HeaderModel['navigation'][number] }) {
|
|
92
|
+
if (item.submenu === undefined || item.submenu.length === 0) {
|
|
93
|
+
return (
|
|
94
|
+
<li>
|
|
95
|
+
<a href={item.href} {...linkTarget(item)} className={MOBILE_LINK}>
|
|
96
|
+
{item.label}
|
|
97
|
+
</a>
|
|
98
|
+
</li>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<li>
|
|
104
|
+
<details data-nav-disclosure name="default-header-submenu" className="group/submenu">
|
|
105
|
+
<summary
|
|
106
|
+
className={`${SUMMARY} flex min-h-11 items-center justify-between gap-2 rounded-md pl-3 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground`}
|
|
107
|
+
>
|
|
108
|
+
<a
|
|
109
|
+
href={item.href}
|
|
110
|
+
{...linkTarget(item)}
|
|
111
|
+
className="min-w-0 flex-1 truncate hover:underline"
|
|
112
|
+
>
|
|
113
|
+
{item.label}
|
|
114
|
+
</a>
|
|
115
|
+
<span className="flex size-11 shrink-0 items-center justify-center">
|
|
116
|
+
<Chevron className="size-3 text-muted-foreground transition-transform group-open/submenu:rotate-90" />
|
|
117
|
+
</span>
|
|
118
|
+
</summary>
|
|
119
|
+
<ul className="mb-1 ml-3 flex flex-col gap-0.5 border-l border-border py-1 pl-2">
|
|
120
|
+
{item.submenu.map((child) => (
|
|
121
|
+
<li key={child.href}>
|
|
122
|
+
<a href={child.href} {...linkTarget(child)} className={MOBILE_LINK}>
|
|
123
|
+
{child.label}
|
|
124
|
+
</a>
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
</details>
|
|
129
|
+
</li>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
48
133
|
export function Header({
|
|
49
134
|
boardTitle,
|
|
50
135
|
homeHref,
|
|
@@ -55,37 +140,52 @@ export function Header({
|
|
|
55
140
|
}: HeaderModel & { copy: SlotCopy }) {
|
|
56
141
|
const c = (key: string) => fromSlotCopy(copy, `default.header.${key}`)
|
|
57
142
|
|
|
58
|
-
const
|
|
143
|
+
const hasNavigation = navigation.length > 0
|
|
59
144
|
|
|
60
145
|
return (
|
|
61
|
-
<header className="border-b border-border bg-card">
|
|
62
|
-
<div className={`${PAGE} flex flex-wrap items-center
|
|
146
|
+
<header className="relative border-t-4 border-b border-t-primary border-b-border bg-card">
|
|
147
|
+
<div className={`${PAGE} flex flex-wrap items-center gap-x-4 gap-y-3 py-3`}>
|
|
63
148
|
<a
|
|
64
149
|
href={homeHref}
|
|
65
|
-
className=
|
|
150
|
+
className="me-auto inline-flex min-h-9 min-w-0 items-center text-xl font-semibold tracking-tight text-primary transition-colors hover:text-primary-hover"
|
|
66
151
|
>
|
|
67
152
|
<BoardMark boardTitle={boardTitle} logo={logo} />
|
|
68
153
|
</a>
|
|
154
|
+
|
|
69
155
|
{children}
|
|
156
|
+
|
|
157
|
+
{hasNavigation && (
|
|
158
|
+
<nav aria-label={c('sections')} data-nav-view="mobile" className="lg:hidden">
|
|
159
|
+
<details data-nav-disclosure className="group/menu">
|
|
160
|
+
<summary
|
|
161
|
+
className={`${SUMMARY} flex size-11 items-center justify-center rounded-md border border-border text-foreground transition-colors hover:border-primary/40 hover:text-primary group-open/menu:border-primary/40 group-open/menu:bg-primary/8 group-open/menu:text-primary`}
|
|
162
|
+
>
|
|
163
|
+
<MenuGlyph />
|
|
164
|
+
<span className="sr-only">{c('sections')}</span>
|
|
165
|
+
</summary>
|
|
166
|
+
|
|
167
|
+
<div className="absolute inset-x-0 top-full z-30 max-h-[calc(100dvh-5rem)] overflow-y-auto border-b border-border bg-card shadow-elevation">
|
|
168
|
+
<ul className={`${PAGE} flex flex-col gap-0.5 py-2`}>
|
|
169
|
+
{navigation.map((item) => (
|
|
170
|
+
<MobileNavItem key={item.href} item={item} />
|
|
171
|
+
))}
|
|
172
|
+
</ul>
|
|
173
|
+
</div>
|
|
174
|
+
</details>
|
|
175
|
+
</nav>
|
|
176
|
+
)}
|
|
70
177
|
</div>
|
|
71
178
|
|
|
72
|
-
{
|
|
73
|
-
<nav aria-label={c('sections')} className="border-t border-border
|
|
179
|
+
{hasNavigation && (
|
|
180
|
+
<nav aria-label={c('sections')} className="hidden border-t border-border lg:block">
|
|
74
181
|
<div className={PAGE}>
|
|
75
182
|
<ul
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
? 'flex-wrap'
|
|
79
|
-
: 'overflow-x-auto [mask-image:linear-gradient(to_right,black_calc(100%-1.5rem),transparent)] sm:[mask-image:none]'
|
|
80
|
-
}`}
|
|
183
|
+
data-nav-view="desktop"
|
|
184
|
+
className="-mx-3 flex flex-wrap items-center gap-0.5 py-1 text-sm"
|
|
81
185
|
>
|
|
82
186
|
{navigation.map((item) => (
|
|
83
187
|
<li key={item.href} className="group relative shrink-0">
|
|
84
|
-
<a
|
|
85
|
-
href={item.href}
|
|
86
|
-
{...linkTarget(item)}
|
|
87
|
-
className="inline-flex h-10 items-center rounded-t-md border-b-2 border-transparent px-2.5 font-medium text-muted-foreground transition-colors hover:border-border hover:text-foreground"
|
|
88
|
-
>
|
|
188
|
+
<a href={item.href} {...linkTarget(item)} className={DESKTOP_LINK}>
|
|
89
189
|
{item.label}
|
|
90
190
|
</a>
|
|
91
191
|
<Submenu items={item.submenu} />
|
|
@@ -46,7 +46,7 @@ export function LatestThreads({
|
|
|
46
46
|
{thread.title}
|
|
47
47
|
</a>
|
|
48
48
|
<span className={`shrink-0 text-xs text-muted-foreground ${NUMERIC}`}>
|
|
49
|
-
{thread.replyCount.label}{' '}
|
|
49
|
+
<span className="font-medium text-primary">{thread.replyCount.label}</span>{' '}
|
|
50
50
|
{thread.replyCount.value === 1 ? c('reply.one') : c('reply.other')}
|
|
51
51
|
</span>
|
|
52
52
|
</div>
|
|
@@ -64,9 +64,7 @@ export function MemberProfile({
|
|
|
64
64
|
|
|
65
65
|
<dl className="grid grid-cols-3 gap-3 border-t border-border px-5 py-4">
|
|
66
66
|
<div>
|
|
67
|
-
<dd className={`text-lg font-semibold text-
|
|
68
|
-
{postCount.label}
|
|
69
|
-
</dd>
|
|
67
|
+
<dd className={`text-lg font-semibold text-primary ${NUMERIC}`}>{postCount.label}</dd>
|
|
70
68
|
<dt className="text-xs text-muted-foreground">{c('postsLabel')}</dt>
|
|
71
69
|
</div>
|
|
72
70
|
<div>
|
package/src/slots/navigation.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NavigationModel, SlotCopy } from '@meith/theme-kit'
|
|
2
2
|
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { PAGE } from '../shared'
|
|
5
5
|
|
|
6
6
|
export function Navigation({ items, copy }: NavigationModel & { copy: SlotCopy }) {
|
|
7
7
|
if (items.length === 0) return null
|
|
@@ -9,28 +9,29 @@ export function Navigation({ items, copy }: NavigationModel & { copy: SlotCopy }
|
|
|
9
9
|
const c = (key: string) => fromSlotCopy(copy, `default.navigation.${key}`)
|
|
10
10
|
|
|
11
11
|
return (
|
|
12
|
-
<nav aria-label={c('breadcrumb')} className=
|
|
13
|
-
<ol
|
|
14
|
-
className={`${PAGE} flex items-center gap-1.5 overflow-x-auto py-2 text-xs whitespace-nowrap text-muted-foreground`}
|
|
15
|
-
>
|
|
12
|
+
<nav aria-label={c('breadcrumb')} className={`${PAGE} pt-4`}>
|
|
13
|
+
<ol className="-mx-1.5 flex items-center overflow-x-auto py-0.5 text-xs whitespace-nowrap text-muted-foreground">
|
|
16
14
|
{items.map((item, index) => {
|
|
17
15
|
const isLast = index === items.length - 1
|
|
18
16
|
return (
|
|
19
|
-
<li key={item.href} className="flex shrink-0 items-center
|
|
17
|
+
<li key={item.href} className="flex shrink-0 items-center">
|
|
20
18
|
{index > 0 && (
|
|
21
|
-
<span aria-hidden="true" className="text-border select-none">
|
|
19
|
+
<span aria-hidden="true" className="px-0.5 text-border select-none">
|
|
22
20
|
{c('separator')}
|
|
23
21
|
</span>
|
|
24
22
|
)}
|
|
25
23
|
{isLast ? (
|
|
26
24
|
<span
|
|
27
25
|
aria-current="page"
|
|
28
|
-
className="max-w-[24ch] truncate font-medium text-foreground sm:max-w-none"
|
|
26
|
+
className="max-w-[24ch] truncate px-1.5 py-1 font-medium text-foreground sm:max-w-none"
|
|
29
27
|
>
|
|
30
28
|
{item.label}
|
|
31
29
|
</span>
|
|
32
30
|
) : (
|
|
33
|
-
<a
|
|
31
|
+
<a
|
|
32
|
+
href={item.href}
|
|
33
|
+
className="rounded-md px-1.5 py-1 transition-colors hover:bg-primary/8 hover:text-primary"
|
|
34
|
+
>
|
|
34
35
|
{item.label}
|
|
35
36
|
</a>
|
|
36
37
|
)}
|
package/src/slots/panel-nav.tsx
CHANGED
|
@@ -131,7 +131,7 @@ export function PanelNav({
|
|
|
131
131
|
</Disclosure>
|
|
132
132
|
</div>
|
|
133
133
|
|
|
134
|
-
<div className="hidden rounded-
|
|
134
|
+
<div className="hidden rounded-lg border border-border bg-card p-2 shadow-elevation lg:block">
|
|
135
135
|
<Sections label={label} sections={sections} copy={copy} />
|
|
136
136
|
</div>
|
|
137
137
|
</>
|
|
@@ -14,7 +14,7 @@ export function PanelShell({ panel, links, linksLabel, regions, children }: Pane
|
|
|
14
14
|
{links.length > 0 && (
|
|
15
15
|
<nav
|
|
16
16
|
aria-label={linksLabel}
|
|
17
|
-
className="hidden rounded-
|
|
17
|
+
className="hidden rounded-lg border border-border bg-card p-2 shadow-elevation lg:block"
|
|
18
18
|
>
|
|
19
19
|
<p className="px-2.5 pt-1 pb-1.5 text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
20
20
|
{linksLabel}
|
|
@@ -2,6 +2,8 @@ import type { PostActionsSlotModel, SlotCopy } from '@meith/theme-kit'
|
|
|
2
2
|
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
3
|
import { buttonVariants, Separator } from '@meith/ui'
|
|
4
4
|
|
|
5
|
+
import { isEmptyRegion } from '../shared'
|
|
6
|
+
|
|
5
7
|
interface Action {
|
|
6
8
|
readonly href: string
|
|
7
9
|
readonly label: string
|
|
@@ -28,7 +30,9 @@ export function PostActions({
|
|
|
28
30
|
actions.moderateHref === null ? null : { href: actions.moderateHref, label: c('moderate') },
|
|
29
31
|
].filter((action): action is Action => action !== null)
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
const extra = isEmptyRegion(children) ? null : children
|
|
34
|
+
|
|
35
|
+
if (reader.length === 0 && staff.length === 0 && extra === null) return null
|
|
32
36
|
|
|
33
37
|
return (
|
|
34
38
|
<nav aria-label={c('nav')} className="flex flex-wrap items-center gap-x-1 gap-y-1">
|
|
@@ -56,7 +60,9 @@ export function PostActions({
|
|
|
56
60
|
</a>
|
|
57
61
|
))}
|
|
58
62
|
|
|
59
|
-
{
|
|
63
|
+
{extra !== null && (
|
|
64
|
+
<span className="ms-auto flex flex-wrap items-center gap-x-1 gap-y-1">{extra}</span>
|
|
65
|
+
)}
|
|
60
66
|
</nav>
|
|
61
67
|
)
|
|
62
68
|
}
|
package/src/slots/post-bit.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { PostBitSlotModel, SlotCopy } from '@meith/theme-kit'
|
|
2
2
|
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
|
-
import { Alert, AlertDescription, AlertTitle, Avatar, Card } from '@meith/ui'
|
|
3
|
+
import { Alert, AlertDescription, AlertTitle, Avatar, Card, cn } from '@meith/ui'
|
|
4
4
|
|
|
5
|
-
import { groupTags, LINK, MUTED_LINK, NUMERIC, Stamp, UserRef } from '../shared'
|
|
5
|
+
import { groupTags, isEmptyRegion, LINK, MUTED_LINK, NUMERIC, Stamp, UserRef } from '../shared'
|
|
6
6
|
|
|
7
7
|
const VISIBILITY_TINT = {
|
|
8
8
|
visible: '',
|
|
@@ -10,6 +10,8 @@ const VISIBILITY_TINT = {
|
|
|
10
10
|
deleted: 'border-thread-deleted/50 bg-destructive/5',
|
|
11
11
|
} as const
|
|
12
12
|
|
|
13
|
+
const BODY_X = 'px-4 sm:px-5'
|
|
14
|
+
|
|
13
15
|
function StatusBanner({
|
|
14
16
|
visibility,
|
|
15
17
|
copy,
|
|
@@ -74,12 +76,12 @@ function AuthorBlock({
|
|
|
74
76
|
const c = (key: string) => fromSlotCopy(copy, `default.postBit.${key}`)
|
|
75
77
|
|
|
76
78
|
return (
|
|
77
|
-
<div className="flex gap-3 sm:flex-col sm:gap-2 sm:text-center">
|
|
79
|
+
<div className="flex items-center gap-3 sm:flex-col sm:items-stretch sm:gap-2 sm:text-center">
|
|
78
80
|
<Avatar src={author.avatarUrl} name={author.username} size={48} className="sm:self-center" />
|
|
79
81
|
|
|
80
82
|
<div className="min-w-0 flex-1 sm:flex-none">
|
|
81
83
|
<p className="truncate text-sm">
|
|
82
|
-
<UserRef user={author} className="font-semibold
|
|
84
|
+
<UserRef user={author} className="font-semibold" />
|
|
83
85
|
</p>
|
|
84
86
|
|
|
85
87
|
{author.badge != null && (
|
|
@@ -103,7 +105,9 @@ function AuthorBlock({
|
|
|
103
105
|
|
|
104
106
|
{badges}
|
|
105
107
|
|
|
106
|
-
<dl
|
|
108
|
+
<dl
|
|
109
|
+
className={`mt-1 flex flex-wrap gap-x-2 text-xs text-muted-foreground sm:mt-2 sm:flex-col sm:gap-x-0 sm:gap-y-0.5 ${NUMERIC}`}
|
|
110
|
+
>
|
|
107
111
|
<div className="flex gap-1 sm:justify-center">
|
|
108
112
|
<dt className="sr-only">{c('postsLabel')}</dt>
|
|
109
113
|
<dd>
|
|
@@ -130,9 +134,9 @@ function AuthorBlock({
|
|
|
130
134
|
</dl>
|
|
131
135
|
|
|
132
136
|
{author.fields.length > 0 && (
|
|
133
|
-
<dl className="mt-1
|
|
137
|
+
<dl className="mt-1 flex flex-wrap gap-x-3 gap-y-0.5 text-xs text-muted-foreground sm:mt-1.5 sm:flex-col sm:gap-x-0">
|
|
134
138
|
{author.fields.map((field) => (
|
|
135
|
-
<div key={field.label} className="flex
|
|
139
|
+
<div key={field.label} className="flex min-w-0 gap-1 sm:justify-center">
|
|
136
140
|
<dt className="font-medium">{field.label}:</dt>
|
|
137
141
|
<dd className="truncate">{field.value}</dd>
|
|
138
142
|
</div>
|
|
@@ -147,46 +151,57 @@ function AuthorBlock({
|
|
|
147
151
|
export function PostBit({ post, select, regions, copy }: PostBitSlotModel & { copy: SlotCopy }) {
|
|
148
152
|
const c = (key: string) => fromSlotCopy(copy, `default.postBit.${key}`)
|
|
149
153
|
|
|
154
|
+
const actions = regions.actions !== null && !isEmptyRegion(regions.actions)
|
|
155
|
+
const pluginFooter = !isEmptyRegion(regions.pluginFooter)
|
|
156
|
+
|
|
150
157
|
return (
|
|
151
158
|
<Card
|
|
152
159
|
as="article"
|
|
153
160
|
id={`post-${post.number}`}
|
|
154
161
|
data-post-id={post.id}
|
|
155
162
|
data-visibility={post.visibility}
|
|
156
|
-
className={
|
|
163
|
+
className={cn(
|
|
164
|
+
'target:border-primary/60 target:ring-2 target:ring-primary/20',
|
|
165
|
+
VISIBILITY_TINT[post.visibility],
|
|
166
|
+
)}
|
|
157
167
|
>
|
|
158
168
|
<StatusBanner visibility={post.visibility} copy={copy} />
|
|
159
169
|
|
|
160
|
-
<div className="grid grid-cols-
|
|
161
|
-
<div className="border-b border-border
|
|
170
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto] sm:grid-cols-[10.5rem_minmax(0,1fr)] sm:grid-rows-[auto_minmax(0,1fr)]">
|
|
171
|
+
<div className="col-start-1 row-start-1 min-w-0 border-b border-border py-3 pl-4 sm:row-span-2 sm:border-r sm:border-b-0 sm:bg-surface/60 sm:px-4 sm:py-4">
|
|
162
172
|
<AuthorBlock author={post.author} badges={regions.pluginBadges} copy={copy} />
|
|
163
173
|
</div>
|
|
164
174
|
|
|
165
|
-
<div
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
</
|
|
180
|
-
|
|
175
|
+
<div
|
|
176
|
+
className={`col-start-2 row-start-1 flex flex-col-reverse items-end justify-center gap-1.5 border-b border-border py-3 text-xs text-muted-foreground sm:flex-row sm:items-center sm:justify-between sm:py-1.5 ${BODY_X}`}
|
|
177
|
+
>
|
|
178
|
+
{select === null ? (
|
|
179
|
+
<span className="hidden sm:block" />
|
|
180
|
+
) : (
|
|
181
|
+
<label className="flex min-h-6 items-center gap-2">
|
|
182
|
+
<input
|
|
183
|
+
type="checkbox"
|
|
184
|
+
name={select.name}
|
|
185
|
+
value={select.value}
|
|
186
|
+
form={select.formId}
|
|
187
|
+
className="size-4 accent-primary"
|
|
188
|
+
/>
|
|
189
|
+
<span className="sr-only">{select.label}</span>
|
|
190
|
+
</label>
|
|
191
|
+
)}
|
|
181
192
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
<a
|
|
194
|
+
href={post.permalink}
|
|
195
|
+
className={`inline-flex min-h-8 items-center gap-2 ${MUTED_LINK} ${NUMERIC}`}
|
|
196
|
+
>
|
|
197
|
+
<Stamp at={post.postedAt} />
|
|
198
|
+
<span className="font-semibold text-primary">#{post.number}</span>
|
|
199
|
+
</a>
|
|
200
|
+
</div>
|
|
187
201
|
|
|
202
|
+
<div className="col-span-2 row-start-2 min-w-0 sm:col-span-1 sm:col-start-2">
|
|
188
203
|
{post.ignored !== null ? (
|
|
189
|
-
<div className=
|
|
204
|
+
<div className={`py-5 text-sm text-muted-foreground ${BODY_X}`}>
|
|
190
205
|
{c('ignoringPrefix')}{' '}
|
|
191
206
|
<span className="font-medium text-foreground">{post.ignored.authorUsername}</span>.{' '}
|
|
192
207
|
{c('hiddenNotice')}{' '}
|
|
@@ -195,9 +210,9 @@ export function PostBit({ post, select, regions, copy }: PostBitSlotModel & { co
|
|
|
195
210
|
</a>
|
|
196
211
|
</div>
|
|
197
212
|
) : (
|
|
198
|
-
<div className=
|
|
213
|
+
<div className={`py-4 sm:py-5 ${BODY_X}`}>
|
|
199
214
|
<div
|
|
200
|
-
className="prose-md text-sm"
|
|
215
|
+
className="prose-md text-[0.9375rem] sm:text-base"
|
|
201
216
|
dangerouslySetInnerHTML={{ __html: post.bodyHtml }}
|
|
202
217
|
/>
|
|
203
218
|
|
|
@@ -210,7 +225,7 @@ export function PostBit({ post, select, regions, copy }: PostBitSlotModel & { co
|
|
|
210
225
|
)}
|
|
211
226
|
|
|
212
227
|
{post.attachments.length > 0 && (
|
|
213
|
-
<div className=
|
|
228
|
+
<div className={`border-t border-border py-3 ${BODY_X}`}>
|
|
214
229
|
<h4 className="mb-2 text-xs font-semibold tracking-wide text-muted-foreground uppercase">
|
|
215
230
|
{post.attachments.length}{' '}
|
|
216
231
|
{post.attachments.length === 1 ? c('attachment.one') : c('attachment.other')}
|
|
@@ -246,19 +261,19 @@ export function PostBit({ post, select, regions, copy }: PostBitSlotModel & { co
|
|
|
246
261
|
|
|
247
262
|
{post.ignored === null && post.author.signatureHtml !== null && (
|
|
248
263
|
<div
|
|
249
|
-
className=
|
|
264
|
+
className={`prose-md border-t border-border py-2.5 text-xs text-muted-foreground ${BODY_X}`}
|
|
250
265
|
dangerouslySetInnerHTML={{ __html: post.author.signatureHtml }}
|
|
251
266
|
/>
|
|
252
267
|
)}
|
|
253
268
|
|
|
254
|
-
{
|
|
255
|
-
<div className=
|
|
269
|
+
{pluginFooter && (
|
|
270
|
+
<div className={`border-t border-border py-2 text-xs empty:hidden ${BODY_X}`}>
|
|
256
271
|
{regions.pluginFooter}
|
|
257
272
|
</div>
|
|
258
273
|
)}
|
|
259
274
|
|
|
260
|
-
{
|
|
261
|
-
<footer className=
|
|
275
|
+
{actions && (
|
|
276
|
+
<footer className={`border-t border-border py-1.5 empty:hidden ${BODY_X}`}>
|
|
262
277
|
{regions.actions}
|
|
263
278
|
</footer>
|
|
264
279
|
)}
|
package/src/slots/thread-row.tsx
CHANGED
|
@@ -4,7 +4,12 @@ import { Badge } from '@meith/ui'
|
|
|
4
4
|
|
|
5
5
|
import { Counts, LINK, Prefix, ReadSpacer, Stamp, UnreadDot, UserRef } from '../shared'
|
|
6
6
|
|
|
7
|
-
export function ThreadRow({
|
|
7
|
+
export function ThreadRow({
|
|
8
|
+
thread,
|
|
9
|
+
select,
|
|
10
|
+
regions,
|
|
11
|
+
copy,
|
|
12
|
+
}: ThreadRowSlotModel & { copy: SlotCopy }) {
|
|
8
13
|
const c = (key: string) => fromSlotCopy(copy, `default.threadRow.${key}`)
|
|
9
14
|
|
|
10
15
|
const hidden = thread.visibility === 'deleted' || thread.visibility === 'unapproved'
|
|
@@ -14,7 +19,7 @@ export function ThreadRow({ thread, select, copy }: ThreadRowSlotModel & { copy:
|
|
|
14
19
|
<li
|
|
15
20
|
data-unread={thread.isUnread ? '' : undefined}
|
|
16
21
|
data-visibility={hidden ? thread.visibility : undefined}
|
|
17
|
-
className={`grid grid-cols-[auto_minmax(0,1fr)] gap-x-2.5 gap-y-
|
|
22
|
+
className={`grid grid-cols-[auto_minmax(0,1fr)] gap-x-2.5 gap-y-1.5 px-4 py-3 transition-colors hover:bg-muted/60 md:grid-cols-[auto_minmax(0,1fr)_9rem_14rem] md:items-center md:gap-x-4 ${hidden ? tint : ''}`}
|
|
18
23
|
>
|
|
19
24
|
<span className="flex items-start gap-2">
|
|
20
25
|
{select !== null && (
|
|
@@ -52,6 +57,7 @@ export function ThreadRow({ thread, select, copy }: ThreadRowSlotModel & { copy:
|
|
|
52
57
|
{thread.title}
|
|
53
58
|
</a>
|
|
54
59
|
{thread.isUnread && <span className="sr-only"> {c('newPosts')}</span>}
|
|
60
|
+
{regions?.pluginBadges}
|
|
55
61
|
</div>
|
|
56
62
|
|
|
57
63
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
@@ -59,38 +65,43 @@ export function ThreadRow({ thread, select, copy }: ThreadRowSlotModel & { copy:
|
|
|
59
65
|
</p>
|
|
60
66
|
</div>
|
|
61
67
|
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
<div className="col-start-2 flex min-w-0 flex-wrap items-baseline gap-x-4 gap-y-1 text-xs text-muted-foreground md:contents">
|
|
69
|
+
<Counts
|
|
70
|
+
className="md:col-start-3 md:justify-end"
|
|
71
|
+
items={[
|
|
72
|
+
{
|
|
73
|
+
label: c('repliesLabel'),
|
|
74
|
+
value: thread.replyCount,
|
|
75
|
+
one: c('reply.one'),
|
|
76
|
+
many: c('reply.other'),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: c('viewsLabel'),
|
|
80
|
+
value: thread.viewCount,
|
|
81
|
+
one: c('view.one'),
|
|
82
|
+
many: c('view.other'),
|
|
83
|
+
},
|
|
84
|
+
]}
|
|
85
|
+
/>
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
<div className="flex min-w-0 max-w-full flex-wrap gap-x-1 md:col-start-4 md:block">
|
|
88
|
+
{thread.lastPost === null ? (
|
|
89
|
+
<span className="text-thread-moved">{c('noRepliesYet')}</span>
|
|
90
|
+
) : (
|
|
91
|
+
<>
|
|
92
|
+
<a
|
|
93
|
+
href={thread.lastPost.href}
|
|
94
|
+
className={`font-medium text-foreground md:block ${LINK}`}
|
|
95
|
+
>
|
|
96
|
+
{c('latestReply')}
|
|
97
|
+
</a>
|
|
98
|
+
<span className="md:mt-0.5 md:block md:truncate">
|
|
99
|
+
{c('by')} <UserRef user={thread.lastPost.author} className="font-normal" />{' '}
|
|
100
|
+
{c('dot')} <Stamp at={thread.lastPost.at} />
|
|
101
|
+
</span>
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
94
105
|
</div>
|
|
95
106
|
</li>
|
|
96
107
|
)
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { SlotCopy, ThreadViewModel } from '@meith/theme-kit'
|
|
2
2
|
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
|
-
import { Badge, buttonVariants } from '@meith/ui'
|
|
3
|
+
import { Badge, buttonVariants, cn } from '@meith/ui'
|
|
4
4
|
|
|
5
|
-
import { Counts,
|
|
5
|
+
import { Counts, PAGE_BODY, Prefix } from '../shared'
|
|
6
6
|
|
|
7
7
|
export function ThreadView({
|
|
8
8
|
thread,
|
|
9
|
-
forum,
|
|
10
9
|
replyHref,
|
|
11
10
|
markReadAction,
|
|
11
|
+
watch,
|
|
12
12
|
regions,
|
|
13
13
|
copy,
|
|
14
14
|
}: ThreadViewModel & { copy: SlotCopy }) {
|
|
@@ -18,13 +18,7 @@ export function ThreadView({
|
|
|
18
18
|
<div className={PAGE_BODY}>
|
|
19
19
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
|
|
20
20
|
<div className="min-w-0">
|
|
21
|
-
<
|
|
22
|
-
{forum.label}
|
|
23
|
-
</a>
|
|
24
|
-
|
|
25
|
-
<h1 className="mt-1 text-2xl font-semibold tracking-tight text-balance sm:mt-0">
|
|
26
|
-
{thread.title}
|
|
27
|
-
</h1>
|
|
21
|
+
<h1 className="text-2xl font-semibold tracking-tight text-balance">{thread.title}</h1>
|
|
28
22
|
|
|
29
23
|
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
30
24
|
{thread.prefix !== null && <Prefix prefix={thread.prefix} />}
|
|
@@ -51,7 +45,20 @@ export function ThreadView({
|
|
|
51
45
|
</div>
|
|
52
46
|
</div>
|
|
53
47
|
|
|
54
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
48
|
+
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
|
49
|
+
{watch != null && (
|
|
50
|
+
<form action={watch.action} method="post">
|
|
51
|
+
<button type="submit" className={buttonVariants({ variant: 'outline' })}>
|
|
52
|
+
{watch.subscribed ? (
|
|
53
|
+
<>
|
|
54
|
+
{c('watching')} <span aria-hidden="true">✓</span>
|
|
55
|
+
</>
|
|
56
|
+
) : (
|
|
57
|
+
c('watch')
|
|
58
|
+
)}
|
|
59
|
+
</button>
|
|
60
|
+
</form>
|
|
61
|
+
)}
|
|
55
62
|
{markReadAction !== null && (
|
|
56
63
|
<form action={markReadAction} method="post">
|
|
57
64
|
<button type="submit" className={buttonVariants({ variant: 'ghost' })}>
|
|
@@ -60,7 +67,10 @@ export function ThreadView({
|
|
|
60
67
|
</form>
|
|
61
68
|
)}
|
|
62
69
|
{replyHref !== null && (
|
|
63
|
-
<a
|
|
70
|
+
<a
|
|
71
|
+
href={replyHref}
|
|
72
|
+
className={cn(buttonVariants({ variant: 'primary' }), 'ms-auto sm:ms-0')}
|
|
73
|
+
>
|
|
64
74
|
{c('replyAction')}
|
|
65
75
|
</a>
|
|
66
76
|
)}
|