@meith/theme-raidframe 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 +4 -4
- package/src/copy.ts +2 -0
- package/src/messages/en.json +2 -0
- package/src/shared.tsx +5 -0
- package/src/slots/editor-toolbar.tsx +23 -6
- package/src/slots/forum-row.tsx +13 -0
- package/src/slots/header.tsx +95 -17
- package/src/slots/post-actions.tsx +6 -2
- package/src/slots/thread-row.tsx +16 -1
- package/src/slots/thread-view.tsx +20 -1
- package/src/theme.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/theme-raidframe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "A gaming theme for Meith — guild, raid and match boards — built on the default theme's slots.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@meith/theme-default": "^0.
|
|
24
|
-
"@meith/theme-kit": "^0.
|
|
25
|
-
"@meith/ui": "^0.
|
|
23
|
+
"@meith/theme-default": "^0.33.0",
|
|
24
|
+
"@meith/theme-kit": "^0.33.0",
|
|
25
|
+
"@meith/ui": "^0.33.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": "^19.2.0"
|
package/src/copy.ts
CHANGED
|
@@ -264,6 +264,8 @@ export function threadViewCopy(t: Translator): SlotCopy {
|
|
|
264
264
|
'raidframe.threadView.replies',
|
|
265
265
|
'raidframe.threadView.views',
|
|
266
266
|
'raidframe.threadView.markRead',
|
|
267
|
+
'raidframe.threadView.watch',
|
|
268
|
+
'raidframe.threadView.watching',
|
|
267
269
|
])
|
|
268
270
|
}
|
|
269
271
|
|
package/src/messages/en.json
CHANGED
|
@@ -133,6 +133,8 @@
|
|
|
133
133
|
"raidframe.threadView.postReply": "post reply",
|
|
134
134
|
"raidframe.threadView.replies": "replies",
|
|
135
135
|
"raidframe.threadView.views": "views",
|
|
136
|
+
"raidframe.threadView.watch": "watch",
|
|
137
|
+
"raidframe.threadView.watching": "watching",
|
|
136
138
|
"raidframe.userPanel.accountLabel": "Your account",
|
|
137
139
|
"raidframe.userPanel.new": "new",
|
|
138
140
|
"raidframe.userPanel.pm": "pm",
|
package/src/shared.tsx
CHANGED
|
@@ -138,3 +138,8 @@ export function groupTags(
|
|
|
138
138
|
if (groups !== undefined && groups.length > 0) return groups
|
|
139
139
|
return title === null ? [] : [{ title }]
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
export function isEmptyRegion(node: React.ReactNode): boolean {
|
|
143
|
+
if (node === null || node === undefined || node === false || node === '') return true
|
|
144
|
+
return Array.isArray(node) && node.every((child) => isEmptyRegion(child))
|
|
145
|
+
}
|
|
@@ -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'
|
|
@@ -14,20 +16,34 @@ const GLYPHS: Readonly<Record<EditorTag, string>> = {
|
|
|
14
16
|
italic: 'I',
|
|
15
17
|
strikethrough: 'S',
|
|
16
18
|
link: 'LINK',
|
|
19
|
+
image: 'IMG',
|
|
17
20
|
quote: 'QUOTE',
|
|
18
21
|
code: 'CODE',
|
|
19
22
|
spoiler: 'SPLR',
|
|
20
23
|
bulletedList: 'UL',
|
|
21
24
|
numberedList: 'OL',
|
|
25
|
+
taskList: 'TASK',
|
|
22
26
|
heading: 'H',
|
|
27
|
+
table: 'TBL',
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
const BUTTON =
|
|
26
31
|
'inline-flex min-w-8 items-center justify-center border border-border bg-secondary px-2 py-1.5 font-mono text-[0.6875rem] font-semibold uppercase tracking-[0.1em] text-secondary-foreground hover:border-primary/70 hover:text-primary'
|
|
27
32
|
|
|
28
|
-
function
|
|
33
|
+
function runButton(textareaId: string, button: EditorToolbarButtonModel): void {
|
|
29
34
|
const field = document.getElementById(textareaId)
|
|
30
|
-
if (field instanceof HTMLTextAreaElement)
|
|
35
|
+
if (!(field instanceof HTMLTextAreaElement)) return
|
|
36
|
+
|
|
37
|
+
if (button.tag !== null) {
|
|
38
|
+
applyEditorTag(field, button.tag, button.placeholder)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (button.insertion !== null) applyInsertion(field, button.insertion)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function glyphFor(button: EditorToolbarButtonModel): string {
|
|
46
|
+
return button.tag === null ? button.label.charAt(0).toUpperCase() : GLYPHS[button.tag]
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
export function EditorToolbar({
|
|
@@ -39,18 +55,19 @@ export function EditorToolbar({
|
|
|
39
55
|
return (
|
|
40
56
|
<>
|
|
41
57
|
<div role="group" aria-label={groupLabel} className="flex flex-wrap gap-1 px-4 py-3">
|
|
42
|
-
{buttons.map((button) => (
|
|
58
|
+
{buttons.map((button, index) => (
|
|
43
59
|
<button
|
|
44
|
-
|
|
60
|
+
// 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
|
|
61
|
+
key={`${button.tag ?? button.label}-${index}`}
|
|
45
62
|
type="button"
|
|
46
63
|
onMouseDown={(event) => event.preventDefault()}
|
|
47
|
-
onClick={() =>
|
|
64
|
+
onClick={() => runButton(textareaId, button)}
|
|
48
65
|
title={button.title}
|
|
49
66
|
aria-label={button.label}
|
|
50
67
|
{...(button.keyShortcut === null ? {} : { 'aria-keyshortcuts': button.keyShortcut })}
|
|
51
68
|
className={BUTTON}
|
|
52
69
|
>
|
|
53
|
-
<span aria-hidden="true">{
|
|
70
|
+
<span aria-hidden="true">{glyphFor(button)}</span>
|
|
54
71
|
</button>
|
|
55
72
|
))}
|
|
56
73
|
|
package/src/slots/forum-row.tsx
CHANGED
|
@@ -26,6 +26,19 @@ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }
|
|
|
26
26
|
<p className="mt-0.5 text-xs text-muted-foreground">{forum.description}</p>
|
|
27
27
|
)}
|
|
28
28
|
|
|
29
|
+
{!isLink && forum.lastPost !== null && (
|
|
30
|
+
<p className="mt-1.5 text-xs text-muted-foreground sm:hidden">
|
|
31
|
+
<a href={forum.lastPost.href} className="text-foreground hover:text-primary">
|
|
32
|
+
{forum.lastPost.threadTitle}
|
|
33
|
+
</a>
|
|
34
|
+
<span className={`${MICRO} mt-0.5 block normal-case`}>
|
|
35
|
+
<UserRef user={forum.lastPost.author} className="hover:text-primary" />
|
|
36
|
+
{c('separator')}
|
|
37
|
+
<Stamp at={forum.lastPost.at} />
|
|
38
|
+
</span>
|
|
39
|
+
</p>
|
|
40
|
+
)}
|
|
41
|
+
|
|
29
42
|
{forum.subforums.length > 0 && (
|
|
30
43
|
<p className="mt-1.5 flex flex-wrap gap-1.5">
|
|
31
44
|
{forum.subforums.map((sub) => (
|
package/src/slots/header.tsx
CHANGED
|
@@ -41,7 +41,7 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
|
|
|
41
41
|
<a
|
|
42
42
|
href={child.href}
|
|
43
43
|
{...linkTarget(child)}
|
|
44
|
-
className="
|
|
44
|
+
className="flex items-center px-3 py-1.5 text-muted-foreground pointer-coarse:min-h-11 hover:bg-muted hover:text-foreground"
|
|
45
45
|
>
|
|
46
46
|
{child.label}
|
|
47
47
|
</a>
|
|
@@ -51,6 +51,71 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
|
|
|
51
51
|
)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function Chevron({ className }: { className: string }) {
|
|
55
|
+
return (
|
|
56
|
+
<svg
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
viewBox="0 0 12 12"
|
|
59
|
+
className={className}
|
|
60
|
+
fill="none"
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
strokeWidth="1.5"
|
|
63
|
+
strokeLinecap="round"
|
|
64
|
+
strokeLinejoin="round"
|
|
65
|
+
>
|
|
66
|
+
<path d="m4.5 3 3 3-3 3" />
|
|
67
|
+
</svg>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function MobileNavItem({ item }: { item: HeaderModel['navigation'][number] }) {
|
|
72
|
+
if (item.submenu === undefined || item.submenu.length === 0) {
|
|
73
|
+
return (
|
|
74
|
+
<li>
|
|
75
|
+
<a
|
|
76
|
+
href={item.href}
|
|
77
|
+
{...linkTarget(item)}
|
|
78
|
+
className="flex min-h-11 items-center px-3.5 font-mono text-[0.6875rem] font-semibold tracking-[0.14em] text-muted-foreground uppercase hover:bg-secondary hover:text-primary"
|
|
79
|
+
>
|
|
80
|
+
{item.label}
|
|
81
|
+
</a>
|
|
82
|
+
</li>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<li>
|
|
88
|
+
<details data-nav-disclosure name="raidframe-header-submenu" className="group/submenu">
|
|
89
|
+
<summary className="flex min-h-11 cursor-default list-none items-center justify-between gap-2 px-3.5 font-mono text-[0.6875rem] font-semibold tracking-[0.14em] text-muted-foreground uppercase select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none hover:bg-secondary hover:text-primary">
|
|
90
|
+
<a
|
|
91
|
+
href={item.href}
|
|
92
|
+
{...linkTarget(item)}
|
|
93
|
+
className="min-w-0 flex-1 truncate normal-case tracking-normal hover:underline"
|
|
94
|
+
>
|
|
95
|
+
{item.label}
|
|
96
|
+
</a>
|
|
97
|
+
<span className="flex size-11 shrink-0 items-center justify-center">
|
|
98
|
+
<Chevron className="size-3 transition-transform group-open/submenu:rotate-90" />
|
|
99
|
+
</span>
|
|
100
|
+
</summary>
|
|
101
|
+
<ul className="ml-3 flex flex-col gap-0.5 border-l border-border py-1 pl-2.5">
|
|
102
|
+
{item.submenu.map((child) => (
|
|
103
|
+
<li key={child.href}>
|
|
104
|
+
<a
|
|
105
|
+
href={child.href}
|
|
106
|
+
{...linkTarget(child)}
|
|
107
|
+
className="flex min-h-11 items-center px-3.5 font-mono text-[0.6875rem] font-semibold tracking-[0.14em] text-muted-foreground uppercase hover:bg-secondary hover:text-primary"
|
|
108
|
+
>
|
|
109
|
+
{child.label}
|
|
110
|
+
</a>
|
|
111
|
+
</li>
|
|
112
|
+
))}
|
|
113
|
+
</ul>
|
|
114
|
+
</details>
|
|
115
|
+
</li>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
54
119
|
export function Header({
|
|
55
120
|
boardTitle,
|
|
56
121
|
homeHref,
|
|
@@ -77,22 +142,35 @@ export function Header({
|
|
|
77
142
|
<div className={RULE} aria-hidden="true" />
|
|
78
143
|
|
|
79
144
|
{navigation.length > 0 && (
|
|
80
|
-
<nav
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{item.
|
|
92
|
-
</
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
145
|
+
<nav aria-label={c('sectionsAriaLabel')} className="border-b border-border bg-card/60">
|
|
146
|
+
<div data-nav-view="desktop" className="hidden flex-wrap lg:flex">
|
|
147
|
+
{navigation.map((item) => (
|
|
148
|
+
<span key={item.href} className="group relative">
|
|
149
|
+
<a
|
|
150
|
+
href={item.href}
|
|
151
|
+
{...linkTarget(item)}
|
|
152
|
+
className="block border-t-2 border-r border-t-transparent border-r-border px-3.5 py-2 font-mono text-[0.6875rem] font-semibold tracking-[0.14em] text-muted-foreground uppercase pointer-coarse:flex pointer-coarse:min-h-11 pointer-coarse:items-center hover:border-t-primary hover:bg-secondary hover:text-primary"
|
|
153
|
+
>
|
|
154
|
+
{item.label}
|
|
155
|
+
</a>
|
|
156
|
+
<Submenu items={item.submenu} />
|
|
157
|
+
</span>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div data-nav-view="mobile" className="px-4 py-1 lg:hidden">
|
|
162
|
+
<details data-nav-disclosure className="group">
|
|
163
|
+
<summary className="flex min-h-11 cursor-default list-none items-center gap-2 font-mono text-[0.6875rem] font-semibold tracking-[0.14em] text-foreground uppercase select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none">
|
|
164
|
+
<Chevron className="size-3 shrink-0 transition-transform group-open:rotate-90" />
|
|
165
|
+
{c('sectionsAriaLabel')}
|
|
166
|
+
</summary>
|
|
167
|
+
<ul className="flex flex-col gap-0.5 pb-2">
|
|
168
|
+
{navigation.map((item) => (
|
|
169
|
+
<MobileNavItem key={item.href} item={item} />
|
|
170
|
+
))}
|
|
171
|
+
</ul>
|
|
172
|
+
</details>
|
|
173
|
+
</div>
|
|
96
174
|
</nav>
|
|
97
175
|
)}
|
|
98
176
|
</header>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PostActionsSlotModel, SlotCopy } from '@meith/theme-kit'
|
|
2
2
|
import { fromSlotCopy } from '@meith/theme-kit'
|
|
3
3
|
|
|
4
|
+
import { isEmptyRegion } from '../shared'
|
|
5
|
+
|
|
4
6
|
const ACTION =
|
|
5
7
|
'font-mono text-[0.625rem] font-semibold uppercase tracking-[0.14em] text-muted-foreground hover:text-primary'
|
|
6
8
|
|
|
@@ -22,7 +24,9 @@ export function PostActions({
|
|
|
22
24
|
{ href: actions.moderateHref, label: c('moderate') },
|
|
23
25
|
].filter((item): item is { href: string; label: string } => item.href != null)
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
const extra = isEmptyRegion(children) ? null : children
|
|
28
|
+
|
|
29
|
+
if (items.length === 0 && extra === null) return null
|
|
26
30
|
|
|
27
31
|
return (
|
|
28
32
|
<nav aria-label={c('ariaLabel')} className="flex flex-wrap items-center gap-x-4 gap-y-1">
|
|
@@ -31,7 +35,7 @@ export function PostActions({
|
|
|
31
35
|
{item.label}
|
|
32
36
|
</a>
|
|
33
37
|
))}
|
|
34
|
-
{
|
|
38
|
+
{extra}
|
|
35
39
|
</nav>
|
|
36
40
|
)
|
|
37
41
|
}
|
package/src/slots/thread-row.tsx
CHANGED
|
@@ -5,7 +5,12 @@ import { MICRO, NUMERIC, ReadPip, Stamp, UserRef } from '../shared'
|
|
|
5
5
|
|
|
6
6
|
const TAG = 'border px-1.5 py-px font-mono text-[0.625rem] font-bold uppercase tracking-[0.12em]'
|
|
7
7
|
|
|
8
|
-
export function ThreadRow({
|
|
8
|
+
export function ThreadRow({
|
|
9
|
+
thread,
|
|
10
|
+
select,
|
|
11
|
+
regions,
|
|
12
|
+
copy,
|
|
13
|
+
}: ThreadRowSlotModel & { copy: SlotCopy }) {
|
|
9
14
|
const c = (key: string) => fromSlotCopy(copy, `raidframe.threadRow.${key}`)
|
|
10
15
|
|
|
11
16
|
const hidden = thread.visibility === 'deleted' || thread.visibility === 'unapproved'
|
|
@@ -65,12 +70,22 @@ export function ThreadRow({ thread, select, copy }: ThreadRowSlotModel & { copy:
|
|
|
65
70
|
{c('deleted')}
|
|
66
71
|
</span>
|
|
67
72
|
)}
|
|
73
|
+
{regions?.pluginBadges}
|
|
68
74
|
</div>
|
|
69
75
|
|
|
70
76
|
<p className={`${MICRO} mt-1 normal-case`}>
|
|
71
77
|
<span className="uppercase">{c('startedBy')}</span>{' '}
|
|
72
78
|
<UserRef user={thread.author} className="hover:text-primary" />
|
|
73
79
|
</p>
|
|
80
|
+
{thread.lastPost !== null && (
|
|
81
|
+
<p className={`${MICRO} mt-1 normal-case sm:hidden`}>
|
|
82
|
+
<a href={thread.lastPost.href} className="hover:text-primary">
|
|
83
|
+
<Stamp at={thread.lastPost.at} className="text-foreground" />
|
|
84
|
+
{c('byPrefix')}
|
|
85
|
+
<UserRef user={thread.lastPost.author} linked={false} />
|
|
86
|
+
</a>
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
74
89
|
</td>
|
|
75
90
|
|
|
76
91
|
<td className={`w-16 px-2 py-2.5 text-right text-xs ${NUMERIC} text-foreground`}>
|
|
@@ -10,6 +10,7 @@ export function ThreadView({
|
|
|
10
10
|
forum,
|
|
11
11
|
replyHref,
|
|
12
12
|
markReadAction,
|
|
13
|
+
watch,
|
|
13
14
|
regions,
|
|
14
15
|
copy,
|
|
15
16
|
}: ThreadViewModel & { copy: SlotCopy }) {
|
|
@@ -22,6 +23,21 @@ export function ThreadView({
|
|
|
22
23
|
</a>
|
|
23
24
|
)
|
|
24
25
|
|
|
26
|
+
const watchToggle =
|
|
27
|
+
watch == null ? null : (
|
|
28
|
+
<form action={watch.action} method="post">
|
|
29
|
+
<button type="submit" className={BUTTON}>
|
|
30
|
+
{watch.subscribed ? (
|
|
31
|
+
<>
|
|
32
|
+
{c('watching')} <span aria-hidden="true">✓</span>
|
|
33
|
+
</>
|
|
34
|
+
) : (
|
|
35
|
+
c('watch')
|
|
36
|
+
)}
|
|
37
|
+
</button>
|
|
38
|
+
</form>
|
|
39
|
+
)
|
|
40
|
+
|
|
25
41
|
return (
|
|
26
42
|
<div className="flex flex-col gap-4 p-4">
|
|
27
43
|
<div className="border border-border bg-card shadow-elevation">
|
|
@@ -53,7 +69,10 @@ export function ThreadView({
|
|
|
53
69
|
</p>
|
|
54
70
|
</div>
|
|
55
71
|
|
|
56
|
-
|
|
72
|
+
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
|
73
|
+
{watchToggle}
|
|
74
|
+
{reply}
|
|
75
|
+
</div>
|
|
57
76
|
</div>
|
|
58
77
|
<div className={RULE} aria-hidden="true" />
|
|
59
78
|
</div>
|
package/src/theme.ts
CHANGED