@meith/theme-clubhouse 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/theme-clubhouse",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "A sports club theme for Meith — GAA, soccer, basketball — painted from the club's own two colours.",
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.31.0",
24
- "@meith/theme-kit": "^0.31.0",
25
- "@meith/ui": "^0.31.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
@@ -199,6 +199,8 @@ export function threadViewCopy(t: Translator): SlotCopy {
199
199
  'clubhouse.threadView.replies',
200
200
  'clubhouse.threadView.reply',
201
201
  'clubhouse.threadView.views',
202
+ 'clubhouse.threadView.watch',
203
+ 'clubhouse.threadView.watching',
202
204
  ])
203
205
  }
204
206
 
@@ -107,6 +107,8 @@
107
107
  "clubhouse.threadView.replies": "replies",
108
108
  "clubhouse.threadView.reply": "Reply",
109
109
  "clubhouse.threadView.views": "views",
110
+ "clubhouse.threadView.watch": "Watch",
111
+ "clubhouse.threadView.watching": "Watching",
110
112
  "clubhouse.userPanel.new": "new",
111
113
  "clubhouse.userPanel.signedIn": "Signed in",
112
114
  "clubhouse.userPanel.unread": "unread",
package/src/shared.tsx CHANGED
@@ -192,8 +192,8 @@ export function ReadMark({ unread }: { unread: boolean }) {
192
192
  }
193
193
 
194
194
  export function isEmptyRegion(node: React.ReactNode): boolean {
195
- if (node === null || node === undefined || node === false) return true
196
- return Array.isArray(node) && node.length === 0
195
+ if (node === null || node === undefined || node === false || node === '') return true
196
+ return Array.isArray(node) && node.every((child) => isEmptyRegion(child))
197
197
  }
198
198
 
199
199
  const PREFIX_TONES = {
@@ -44,7 +44,7 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
44
44
  <a
45
45
  href={child.href}
46
46
  {...linkTarget(child)}
47
- className="block px-3 py-1.5 text-muted-foreground hover:bg-muted hover:text-foreground"
47
+ className="flex items-center px-3 py-1.5 text-muted-foreground pointer-coarse:min-h-11 hover:bg-muted hover:text-foreground"
48
48
  >
49
49
  {child.label}
50
50
  </a>
@@ -54,6 +54,73 @@ function Submenu({ items }: { items: HeaderModel['navigation'][number]['submenu'
54
54
  )
55
55
  }
56
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 MobileNavItem({ item }: { item: HeaderModel['navigation'][number] }) {
75
+ if (item.submenu === undefined || item.submenu.length === 0) {
76
+ return (
77
+ <li>
78
+ <a
79
+ href={item.href}
80
+ {...linkTarget(item)}
81
+ className={`${HEADING} flex min-h-11 items-center px-2.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground`}
82
+ >
83
+ {item.label}
84
+ </a>
85
+ </li>
86
+ )
87
+ }
88
+
89
+ return (
90
+ <li>
91
+ <details data-nav-disclosure name="clubhouse-header-submenu" className="group/submenu">
92
+ <summary
93
+ className={`${HEADING} flex min-h-11 cursor-default list-none items-center justify-between gap-2 px-2.5 text-sm text-muted-foreground select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none hover:bg-accent hover:text-foreground`}
94
+ >
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 px-2.5 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
+
57
124
  export function Header({
58
125
  boardTitle,
59
126
  homeHref,
@@ -84,7 +151,8 @@ export function Header({
84
151
  <nav aria-label={c('boardSections')} className="border-b border-border bg-surface">
85
152
  <div className={PAGE}>
86
153
  <ul
87
- className={`-mx-4 flex items-stretch px-4 sm:-mx-6 sm:px-6 ${
154
+ data-nav-view="desktop"
155
+ className={`-mx-4 hidden items-stretch px-4 sm:-mx-6 sm:px-6 lg:flex ${
88
156
  opensMenus ? 'flex-wrap' : 'overflow-x-auto'
89
157
  }`}
90
158
  >
@@ -101,6 +169,22 @@ export function Header({
101
169
  </li>
102
170
  ))}
103
171
  </ul>
172
+
173
+ <div data-nav-view="mobile" className="-mx-4 px-4 py-1 sm:-mx-6 sm:px-6 lg:hidden">
174
+ <details data-nav-disclosure className="group">
175
+ <summary
176
+ className={`${HEADING} flex min-h-11 cursor-default list-none items-center gap-2 text-sm text-foreground select-none [&::-webkit-details-marker]:hidden [&::marker]:content-none`}
177
+ >
178
+ <Chevron className="size-3 shrink-0 transition-transform group-open:rotate-90" />
179
+ {c('boardSections')}
180
+ </summary>
181
+ <ul className="flex flex-col gap-0.5 pb-2">
182
+ {navigation.map((item) => (
183
+ <MobileNavItem key={item.href} item={item} />
184
+ ))}
185
+ </ul>
186
+ </details>
187
+ </div>
104
188
  </div>
105
189
  </nav>
106
190
  )}
@@ -74,7 +74,7 @@ function GroupBadge({
74
74
 
75
75
  function StatLine({ label, children }: { label: string; children: React.ReactNode }) {
76
76
  return (
77
- <div className="flex items-baseline justify-between gap-2">
77
+ <div className="flex items-baseline gap-1.5 sm:justify-between sm:gap-2">
78
78
  <dt className={MICRO}>{label}</dt>
79
79
  <dd className={`${NUMERIC} text-xs font-semibold text-foreground`}>{children}</dd>
80
80
  </div>
@@ -94,43 +94,45 @@ function AuthorBlock({
94
94
 
95
95
  return (
96
96
  <>
97
- <div aria-hidden="true" className="h-12 border-b-2 border-b-secondary bg-primary" />
97
+ <div aria-hidden="true" className="h-8 border-b-2 border-b-secondary bg-primary sm:h-12" />
98
98
 
99
- <div className="relative z-10 -mt-7 px-4">
99
+ <div className="relative z-10 -mt-5 flex items-end gap-3 px-4 sm:-mt-7 sm:block">
100
100
  <Avatar
101
101
  src={author.avatarUrl}
102
102
  name={author.username}
103
103
  size={52}
104
- className="rounded-sm border-2 border-card"
104
+ className="shrink-0 rounded-sm border-2 border-card bg-card"
105
105
  />
106
106
 
107
- <p className="mt-1.5 truncate">
108
- <UserRef user={author} className={`${HEADING} text-sm`} />
109
- </p>
110
-
111
- {groupTags(author.groups, author.title).map((group) => (
112
- <p key={group.title} className={`${MICRO} mt-0.5 truncate`}>
113
- <span className={group.nameClass ?? undefined}>{group.title}</span>
107
+ <div className="min-w-0 flex-1 sm:flex-none">
108
+ <p className="truncate sm:mt-1.5">
109
+ <UserRef user={author} className={`${HEADING} text-sm`} />
114
110
  </p>
115
- ))}
116
111
 
117
- {author.isOnline && (
118
- <p className="mt-1 flex items-center gap-1.5 text-xs text-moderation-approved">
119
- <span aria-hidden="true" className="size-1.5 rounded-full bg-moderation-approved" />
120
- {c('online')}
121
- </p>
122
- )}
112
+ {groupTags(author.groups, author.title).map((group) => (
113
+ <p key={group.title} className={`${MICRO} mt-0.5 truncate`}>
114
+ <span className={group.nameClass ?? undefined}>{group.title}</span>
115
+ </p>
116
+ ))}
123
117
 
124
- {author.badge != null && (
125
- <p className="mt-2">
126
- <GroupBadge badge={author.badge} />
127
- </p>
128
- )}
118
+ {author.isOnline && (
119
+ <p className="mt-1 flex items-center gap-1.5 text-xs text-moderation-approved">
120
+ <span aria-hidden="true" className="size-1.5 rounded-full bg-moderation-approved" />
121
+ {c('online')}
122
+ </p>
123
+ )}
129
124
 
130
- {badges}
125
+ {author.badge != null && (
126
+ <p className="mt-2">
127
+ <GroupBadge badge={author.badge} />
128
+ </p>
129
+ )}
130
+
131
+ {badges}
132
+ </div>
131
133
  </div>
132
134
 
133
- <dl className="mt-2.5 space-y-1 border-t border-border px-4 py-2.5">
135
+ <dl className="mt-2 flex flex-wrap gap-x-4 gap-y-1 border-t border-border px-4 py-2 sm:mt-2.5 sm:block sm:space-y-1 sm:py-2.5">
134
136
  <StatLine label={c('posts')}>{author.postCount.label}</StatLine>
135
137
  {author.reputation != null && (
136
138
  <StatLine label={c('rep')}>{author.reputation.label}</StatLine>
@@ -4,7 +4,12 @@ import { Badge } from '@meith/ui'
4
4
 
5
5
  import { HEADING, LINK, MICRO, Prefix, ReadMark, Stamp, TABLE_ROW, Tally, UserRef } from '../shared'
6
6
 
7
- export function ThreadRow({ thread, select, copy }: ThreadRowSlotModel & { copy: SlotCopy }) {
7
+ export function ThreadRow({
8
+ thread,
9
+ select,
10
+ regions,
11
+ copy,
12
+ }: ThreadRowSlotModel & { copy: SlotCopy }) {
8
13
  const c = (key: string) => fromSlotCopy(copy, `clubhouse.threadRow.${key}`)
9
14
 
10
15
  const hidden = thread.visibility === 'deleted' || thread.visibility === 'unapproved'
@@ -49,6 +54,7 @@ export function ThreadRow({ thread, select, copy }: ThreadRowSlotModel & { copy:
49
54
  {thread.title}
50
55
  </a>
51
56
  {thread.isUnread && <span className="sr-only"> {c('newPosts')}</span>}
57
+ {regions?.pluginBadges}
52
58
  </div>
53
59
 
54
60
  <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 }) {
@@ -19,6 +20,25 @@ export function ThreadView({
19
20
  <PageHead
20
21
  actions={
21
22
  <>
23
+ {watch != null && (
24
+ <form action={watch.action} method="post">
25
+ <button
26
+ type="submit"
27
+ className={cn(
28
+ BUTTON,
29
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
30
+ )}
31
+ >
32
+ {watch.subscribed ? (
33
+ <>
34
+ {c('watching')} <span aria-hidden="true">✓</span>
35
+ </>
36
+ ) : (
37
+ c('watch')
38
+ )}
39
+ </button>
40
+ </form>
41
+ )}
22
42
  {markReadAction !== null && (
23
43
  <form action={markReadAction} method="post">
24
44
  <button
package/src/theme.ts CHANGED
@@ -51,7 +51,7 @@ import { WhoIsOnline } from './slots/who-is-online'
51
51
  export const clubhouseTheme = defineTheme({
52
52
  key: 'clubhouse',
53
53
  title: 'Clubhouse',
54
- version: '0.31.0',
54
+ version: '0.33.0',
55
55
  extends: defaultTheme,
56
56
  slots: {
57
57
  Shell,