@meith/theme-default 0.31.0 → 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 +3 -3
- 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 +83 -3
- 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-default",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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.32.0",
|
|
24
|
+
"@meith/ui": "^0.32.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",
|
|
@@ -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/header.tsx
CHANGED
|
@@ -35,7 +35,7 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
|
|
|
35
35
|
<a
|
|
36
36
|
href={child.href}
|
|
37
37
|
{...linkTarget(child)}
|
|
38
|
-
className="
|
|
38
|
+
className="flex items-center px-3 py-1.5 text-muted-foreground pointer-coarse:min-h-11 hover:bg-muted hover:text-foreground"
|
|
39
39
|
>
|
|
40
40
|
{child.label}
|
|
41
41
|
</a>
|
|
@@ -45,6 +45,71 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
|
|
|
45
45
|
)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function Chevron({ className }: { className: string }) {
|
|
49
|
+
return (
|
|
50
|
+
<svg
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
viewBox="0 0 12 12"
|
|
53
|
+
className={className}
|
|
54
|
+
fill="none"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
strokeWidth="1.5"
|
|
57
|
+
strokeLinecap="round"
|
|
58
|
+
strokeLinejoin="round"
|
|
59
|
+
>
|
|
60
|
+
<path d="m4.5 3 3 3-3 3" />
|
|
61
|
+
</svg>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function MobileNavItem({ item }: { item: HeaderModel['navigation'][number] }) {
|
|
66
|
+
if (item.submenu === undefined || item.submenu.length === 0) {
|
|
67
|
+
return (
|
|
68
|
+
<li>
|
|
69
|
+
<a
|
|
70
|
+
href={item.href}
|
|
71
|
+
{...linkTarget(item)}
|
|
72
|
+
className="flex min-h-11 items-center rounded-md px-2.5 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
73
|
+
>
|
|
74
|
+
{item.label}
|
|
75
|
+
</a>
|
|
76
|
+
</li>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<li>
|
|
82
|
+
<details data-nav-disclosure name="default-header-submenu" className="group/submenu">
|
|
83
|
+
<summary className="flex min-h-11 cursor-default list-none items-center justify-between gap-2 rounded-md px-2.5 text-sm font-medium text-muted-foreground select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none hover:bg-muted hover:text-foreground">
|
|
84
|
+
<a
|
|
85
|
+
href={item.href}
|
|
86
|
+
{...linkTarget(item)}
|
|
87
|
+
className="min-w-0 flex-1 truncate hover:underline"
|
|
88
|
+
>
|
|
89
|
+
{item.label}
|
|
90
|
+
</a>
|
|
91
|
+
<span className="flex size-11 shrink-0 items-center justify-center">
|
|
92
|
+
<Chevron className="size-3 text-muted-foreground transition-transform group-open/submenu:rotate-90" />
|
|
93
|
+
</span>
|
|
94
|
+
</summary>
|
|
95
|
+
<ul className="ml-2 flex flex-col gap-0.5 border-l border-border py-1 pl-2.5">
|
|
96
|
+
{item.submenu.map((child) => (
|
|
97
|
+
<li key={child.href}>
|
|
98
|
+
<a
|
|
99
|
+
href={child.href}
|
|
100
|
+
{...linkTarget(child)}
|
|
101
|
+
className="flex min-h-11 items-center rounded-md px-2.5 text-sm text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
102
|
+
>
|
|
103
|
+
{child.label}
|
|
104
|
+
</a>
|
|
105
|
+
</li>
|
|
106
|
+
))}
|
|
107
|
+
</ul>
|
|
108
|
+
</details>
|
|
109
|
+
</li>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
48
113
|
export function Header({
|
|
49
114
|
boardTitle,
|
|
50
115
|
homeHref,
|
|
@@ -73,7 +138,8 @@ export function Header({
|
|
|
73
138
|
<nav aria-label={c('sections')} className="border-t border-border bg-surface">
|
|
74
139
|
<div className={PAGE}>
|
|
75
140
|
<ul
|
|
76
|
-
|
|
141
|
+
data-nav-view="desktop"
|
|
142
|
+
className={`-mx-4 hidden items-stretch gap-1 px-4 text-sm sm:-mx-6 sm:px-6 lg:flex ${
|
|
77
143
|
opensMenus
|
|
78
144
|
? 'flex-wrap'
|
|
79
145
|
: 'overflow-x-auto [mask-image:linear-gradient(to_right,black_calc(100%-1.5rem),transparent)] sm:[mask-image:none]'
|
|
@@ -84,7 +150,7 @@ export function Header({
|
|
|
84
150
|
<a
|
|
85
151
|
href={item.href}
|
|
86
152
|
{...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"
|
|
153
|
+
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 pointer-coarse:h-11 hover:border-border hover:text-foreground"
|
|
88
154
|
>
|
|
89
155
|
{item.label}
|
|
90
156
|
</a>
|
|
@@ -92,6 +158,20 @@ export function Header({
|
|
|
92
158
|
</li>
|
|
93
159
|
))}
|
|
94
160
|
</ul>
|
|
161
|
+
|
|
162
|
+
<div data-nav-view="mobile" className="-mx-4 px-4 py-1 sm:-mx-6 sm:px-6 lg:hidden">
|
|
163
|
+
<details data-nav-disclosure className="group">
|
|
164
|
+
<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">
|
|
165
|
+
<Chevron className="size-3 shrink-0 transition-transform group-open:rotate-90" />
|
|
166
|
+
{c('sections')}
|
|
167
|
+
</summary>
|
|
168
|
+
<ul className="flex flex-col gap-0.5 pb-2">
|
|
169
|
+
{navigation.map((item) => (
|
|
170
|
+
<MobileNavItem key={item.href} item={item} />
|
|
171
|
+
))}
|
|
172
|
+
</ul>
|
|
173
|
+
</details>
|
|
174
|
+
</div>
|
|
95
175
|
</div>
|
|
96
176
|
</nav>
|
|
97
177
|
)}
|
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'
|
|
@@ -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">
|
|
@@ -9,6 +9,7 @@ export function ThreadView({
|
|
|
9
9
|
forum,
|
|
10
10
|
replyHref,
|
|
11
11
|
markReadAction,
|
|
12
|
+
watch,
|
|
12
13
|
regions,
|
|
13
14
|
copy,
|
|
14
15
|
}: ThreadViewModel & { copy: SlotCopy }) {
|
|
@@ -52,6 +53,19 @@ export function ThreadView({
|
|
|
52
53
|
</div>
|
|
53
54
|
|
|
54
55
|
<div className="flex shrink-0 items-center gap-2">
|
|
56
|
+
{watch != null && (
|
|
57
|
+
<form action={watch.action} method="post">
|
|
58
|
+
<button type="submit" className={buttonVariants({ variant: 'ghost' })}>
|
|
59
|
+
{watch.subscribed ? (
|
|
60
|
+
<>
|
|
61
|
+
{c('watching')} <span aria-hidden="true">✓</span>
|
|
62
|
+
</>
|
|
63
|
+
) : (
|
|
64
|
+
c('watch')
|
|
65
|
+
)}
|
|
66
|
+
</button>
|
|
67
|
+
</form>
|
|
68
|
+
)}
|
|
55
69
|
{markReadAction !== null && (
|
|
56
70
|
<form action={markReadAction} method="post">
|
|
57
71
|
<button type="submit" className={buttonVariants({ variant: 'ghost' })}>
|