@meith/theme-phasebook 0.30.1 → 0.32.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/slots/editor-toolbar.tsx +23 -6
- package/src/slots/header.tsx +85 -22
- package/src/slots/thread-row.tsx +7 -1
- package/src/slots/thread-view.tsx +14 -0
- package/src/theme.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/theme-phasebook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "A theme for Meith with a familiar social shape, 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.32.0",
|
|
24
|
+
"@meith/theme-kit": "^0.32.0",
|
|
25
|
+
"@meith/ui": "^0.32.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": "^19.2.0"
|
package/src/copy.ts
CHANGED
|
@@ -245,6 +245,8 @@ export function threadViewCopy(t: Translator): SlotCopy {
|
|
|
245
245
|
'phasebook.threadView.replyAction',
|
|
246
246
|
'phasebook.threadView.view.one',
|
|
247
247
|
'phasebook.threadView.view.other',
|
|
248
|
+
'phasebook.threadView.watch',
|
|
249
|
+
'phasebook.threadView.watching',
|
|
248
250
|
])
|
|
249
251
|
}
|
|
250
252
|
|
package/src/messages/en.json
CHANGED
|
@@ -129,6 +129,8 @@
|
|
|
129
129
|
"phasebook.threadView.replyAction": "Reply",
|
|
130
130
|
"phasebook.threadView.view.one": "view",
|
|
131
131
|
"phasebook.threadView.view.other": "views",
|
|
132
|
+
"phasebook.threadView.watch": "Watch",
|
|
133
|
+
"phasebook.threadView.watching": "Watching",
|
|
132
134
|
"phasebook.userPanel.accountAriaLabel": "Your account",
|
|
133
135
|
"phasebook.userPanel.messages": "Messages",
|
|
134
136
|
"phasebook.userPanel.notifications": "Notifications",
|
|
@@ -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
|
'inline-flex h-8 min-w-8 items-center justify-center rounded-full px-2.5 text-xs font-semibold text-muted-foreground transition-colors duration-100 hover:bg-accent hover:text-foreground'
|
|
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-1 border-b border-border bg-secondary/40 px-3 py-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/header.tsx
CHANGED
|
@@ -46,7 +46,7 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
|
|
|
46
46
|
<a
|
|
47
47
|
href={child.href}
|
|
48
48
|
{...linkTarget(child)}
|
|
49
|
-
className="
|
|
49
|
+
className="flex items-center px-4 py-2 text-muted-foreground pointer-coarse:min-h-11 hover:bg-accent hover:text-foreground"
|
|
50
50
|
>
|
|
51
51
|
{child.label}
|
|
52
52
|
</a>
|
|
@@ -56,6 +56,71 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
|
|
|
56
56
|
)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function Chevron({ className }: { className: string }) {
|
|
60
|
+
return (
|
|
61
|
+
<svg
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
viewBox="0 0 12 12"
|
|
64
|
+
className={className}
|
|
65
|
+
fill="none"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
strokeWidth="1.5"
|
|
68
|
+
strokeLinecap="round"
|
|
69
|
+
strokeLinejoin="round"
|
|
70
|
+
>
|
|
71
|
+
<path d="m4.5 3 3 3-3 3" />
|
|
72
|
+
</svg>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function MobileNavItem({ item }: { item: HeaderModel['navigation'][number] }) {
|
|
77
|
+
if (item.submenu === undefined || item.submenu.length === 0) {
|
|
78
|
+
return (
|
|
79
|
+
<li>
|
|
80
|
+
<a
|
|
81
|
+
href={item.href}
|
|
82
|
+
{...linkTarget(item)}
|
|
83
|
+
className="flex min-h-11 items-center rounded-lg px-3 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
84
|
+
>
|
|
85
|
+
{item.label}
|
|
86
|
+
</a>
|
|
87
|
+
</li>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<li>
|
|
93
|
+
<details data-nav-disclosure name="phasebook-header-submenu" className="group/submenu">
|
|
94
|
+
<summary className="flex min-h-11 cursor-default list-none items-center justify-between gap-2 rounded-lg px-3 text-sm font-medium text-muted-foreground select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none hover:bg-accent hover:text-foreground">
|
|
95
|
+
<a
|
|
96
|
+
href={item.href}
|
|
97
|
+
{...linkTarget(item)}
|
|
98
|
+
className="min-w-0 flex-1 truncate hover:underline"
|
|
99
|
+
>
|
|
100
|
+
{item.label}
|
|
101
|
+
</a>
|
|
102
|
+
<span className="flex size-11 shrink-0 items-center justify-center">
|
|
103
|
+
<Chevron className="size-3 text-muted-foreground transition-transform group-open/submenu:rotate-90" />
|
|
104
|
+
</span>
|
|
105
|
+
</summary>
|
|
106
|
+
<ul className="ml-2 flex flex-col gap-0.5 border-l border-border py-1 pl-2.5">
|
|
107
|
+
{item.submenu.map((child) => (
|
|
108
|
+
<li key={child.href}>
|
|
109
|
+
<a
|
|
110
|
+
href={child.href}
|
|
111
|
+
{...linkTarget(child)}
|
|
112
|
+
className="flex min-h-11 items-center rounded-lg px-3 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
113
|
+
>
|
|
114
|
+
{child.label}
|
|
115
|
+
</a>
|
|
116
|
+
</li>
|
|
117
|
+
))}
|
|
118
|
+
</ul>
|
|
119
|
+
</details>
|
|
120
|
+
</li>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
59
124
|
export function Header({
|
|
60
125
|
boardTitle,
|
|
61
126
|
homeHref,
|
|
@@ -66,8 +131,6 @@ export function Header({
|
|
|
66
131
|
}: HeaderModel & { copy: SlotCopy }) {
|
|
67
132
|
const c = (key: string) => fromSlotCopy(copy, `phasebook.header.${key}`)
|
|
68
133
|
|
|
69
|
-
const opensMenus = navigation.some((item) => item.submenu !== undefined)
|
|
70
|
-
|
|
71
134
|
return (
|
|
72
135
|
<header className="sticky top-0 z-30 border-b border-border bg-card shadow-elevation">
|
|
73
136
|
<div className={`${PAGE} flex h-14 items-center justify-between gap-3`}>
|
|
@@ -84,6 +147,7 @@ export function Header({
|
|
|
84
147
|
{navigation.length > 0 && (
|
|
85
148
|
<nav
|
|
86
149
|
aria-label={c('sectionsAriaLabel')}
|
|
150
|
+
data-nav-view="desktop"
|
|
87
151
|
className="hidden min-w-0 flex-1 justify-center lg:flex"
|
|
88
152
|
>
|
|
89
153
|
<ul className="flex items-center gap-1">
|
|
@@ -107,25 +171,24 @@ export function Header({
|
|
|
107
171
|
</div>
|
|
108
172
|
|
|
109
173
|
{navigation.length > 0 && (
|
|
110
|
-
<nav
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
>
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{item.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</ul>
|
|
174
|
+
<nav
|
|
175
|
+
aria-label={c('sectionsAriaLabel')}
|
|
176
|
+
data-nav-view="mobile"
|
|
177
|
+
className="border-t border-border lg:hidden"
|
|
178
|
+
>
|
|
179
|
+
<div className={`${PAGE} py-1`}>
|
|
180
|
+
<details data-nav-disclosure className="group">
|
|
181
|
+
<summary className="flex min-h-11 cursor-default list-none items-center gap-2 text-sm font-medium text-foreground select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none">
|
|
182
|
+
<Chevron className="size-3 shrink-0 transition-transform group-open:rotate-90" />
|
|
183
|
+
{c('sectionsAriaLabel')}
|
|
184
|
+
</summary>
|
|
185
|
+
<ul className="flex flex-col gap-0.5 pb-2">
|
|
186
|
+
{navigation.map((item) => (
|
|
187
|
+
<MobileNavItem key={item.href} item={item} />
|
|
188
|
+
))}
|
|
189
|
+
</ul>
|
|
190
|
+
</details>
|
|
191
|
+
</div>
|
|
129
192
|
</nav>
|
|
130
193
|
)}
|
|
131
194
|
</header>
|
package/src/slots/thread-row.tsx
CHANGED
|
@@ -3,7 +3,12 @@ import { fromSlotCopy } from '@meith/theme-kit'
|
|
|
3
3
|
|
|
4
4
|
import { Circle, count, MUTED_LINK, NUMERIC, Prefix, plural, Stamp, Tag, UserRef } from '../shared'
|
|
5
5
|
|
|
6
|
-
export function ThreadRow({
|
|
6
|
+
export function ThreadRow({
|
|
7
|
+
thread,
|
|
8
|
+
select,
|
|
9
|
+
regions,
|
|
10
|
+
copy,
|
|
11
|
+
}: ThreadRowSlotModel & { copy: SlotCopy }) {
|
|
7
12
|
const c = (key: string) => fromSlotCopy(copy, `phasebook.threadRow.${key}`)
|
|
8
13
|
|
|
9
14
|
const hidden = thread.visibility === 'deleted' || thread.visibility === 'unapproved'
|
|
@@ -49,6 +54,7 @@ export function ThreadRow({ thread, select, copy }: ThreadRowSlotModel & { copy:
|
|
|
49
54
|
<Tag token="thread-unapproved">{c('unapproved')}</Tag>
|
|
50
55
|
)}
|
|
51
56
|
{thread.visibility === 'deleted' && <Tag token="thread-deleted">{c('deleted')}</Tag>}
|
|
57
|
+
{regions?.pluginBadges}
|
|
52
58
|
</div>
|
|
53
59
|
|
|
54
60
|
<a
|
|
@@ -19,6 +19,7 @@ export function ThreadView({
|
|
|
19
19
|
forum,
|
|
20
20
|
replyHref,
|
|
21
21
|
markReadAction,
|
|
22
|
+
watch,
|
|
22
23
|
regions,
|
|
23
24
|
copy,
|
|
24
25
|
}: ThreadViewModel & { copy: SlotCopy }) {
|
|
@@ -54,6 +55,19 @@ export function ThreadView({
|
|
|
54
55
|
</div>
|
|
55
56
|
|
|
56
57
|
<div className="flex shrink-0 items-center gap-2">
|
|
58
|
+
{watch != null && (
|
|
59
|
+
<form action={watch.action} method="post">
|
|
60
|
+
<button type="submit" className={PILL}>
|
|
61
|
+
{watch.subscribed ? (
|
|
62
|
+
<>
|
|
63
|
+
{c('watching')} <span aria-hidden="true">✓</span>
|
|
64
|
+
</>
|
|
65
|
+
) : (
|
|
66
|
+
c('watch')
|
|
67
|
+
)}
|
|
68
|
+
</button>
|
|
69
|
+
</form>
|
|
70
|
+
)}
|
|
57
71
|
{markReadAction !== null && (
|
|
58
72
|
<form action={markReadAction} method="post">
|
|
59
73
|
<button type="submit" className={PILL}>
|
package/src/theme.ts
CHANGED