@meith/theme-raidframe 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/theme-raidframe",
3
- "version": "0.30.1",
3
+ "version": "0.32.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.30.1",
24
- "@meith/theme-kit": "^0.30.1",
25
- "@meith/ui": "^0.30.1"
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
@@ -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
 
@@ -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",
@@ -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 runTag(textareaId: string, tag: EditorTag, placeholder: string | null): void {
33
+ function runButton(textareaId: string, button: EditorToolbarButtonModel): void {
29
34
  const field = document.getElementById(textareaId)
30
- if (field instanceof HTMLTextAreaElement) applyEditorTag(field, tag, placeholder)
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
- key={button.tag}
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={() => runTag(textareaId, button.tag, button.placeholder)}
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">{GLYPHS[button.tag]}</span>
70
+ <span aria-hidden="true">{glyphFor(button)}</span>
54
71
  </button>
55
72
  ))}
56
73
 
@@ -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="block px-3 py-1.5 text-muted-foreground hover:bg-muted hover:text-foreground"
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
- aria-label={c('sectionsAriaLabel')}
82
- className="flex flex-wrap border-b border-border bg-card/60"
83
- >
84
- {navigation.map((item) => (
85
- <span key={item.href} className="group relative">
86
- <a
87
- href={item.href}
88
- {...linkTarget(item)}
89
- 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 hover:border-t-primary hover:bg-secondary hover:text-primary"
90
- >
91
- {item.label}
92
- </a>
93
- <Submenu items={item.submenu} />
94
- </span>
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>
@@ -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({ thread, select, copy }: ThreadRowSlotModel & { copy: SlotCopy }) {
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,6 +70,7 @@ 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`}>
@@ -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
- {reply}
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
@@ -73,7 +73,7 @@ import { WhoIsOnline } from './slots/who-is-online'
73
73
  export const raidframeTheme = defineTheme({
74
74
  key: 'raidframe',
75
75
  title: 'Raidframe',
76
- version: '0.30.1',
76
+ version: '0.32.0',
77
77
  extends: defaultTheme,
78
78
  slots: {
79
79
  Shell,