@meith/web 0.25.0 → 0.26.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.
Files changed (62) hide show
  1. package/app/(board)/member/[id]/reputation/page.tsx +2 -2
  2. package/app/(board)/messages/[id]/page.tsx +28 -20
  3. package/app/(board)/messages/page.tsx +2 -2
  4. package/app/(board)/notifications/page.tsx +3 -1
  5. package/app/(board)/subscriptions/page.tsx +6 -2
  6. package/app/(board)/usercp/contacts/page.tsx +3 -7
  7. package/app/admin/api-tokens/page.tsx +3 -2
  8. package/app/admin/forums/[id]/page.tsx +25 -22
  9. package/app/admin/groups/memberships/page.tsx +6 -6
  10. package/app/admin/groups/promotions/page.tsx +14 -16
  11. package/app/admin/log/page.tsx +5 -3
  12. package/app/admin/plugins/page.tsx +1 -1
  13. package/app/admin/security/page.tsx +4 -3
  14. package/app/admin/system/page.tsx +13 -10
  15. package/app/admin/themes/page.tsx +1 -1
  16. package/bin/forum-web.mjs +3 -3
  17. package/bin/forum-web.test.ts +2 -2
  18. package/package.json +48 -48
  19. package/src/components/account/active-sessions.tsx +9 -6
  20. package/src/components/account/credential-proof-form.tsx +6 -7
  21. package/src/components/account/logout-form.tsx +5 -3
  22. package/src/components/account/notification-forms.tsx +9 -9
  23. package/src/components/account/relation-forms.tsx +5 -5
  24. package/src/components/account/reputation-forms.tsx +10 -10
  25. package/src/components/account/sign-in-methods.tsx +12 -6
  26. package/src/components/account/subscription-forms.tsx +7 -7
  27. package/src/components/account/two-factor-forms.tsx +12 -12
  28. package/src/components/account/usercp-forms.tsx +13 -13
  29. package/src/components/admin/admin-forms.tsx +7 -7
  30. package/src/components/admin/badge-forms.tsx +3 -3
  31. package/src/components/admin/ban-filter-forms.tsx +4 -4
  32. package/src/components/admin/branding-forms.tsx +3 -3
  33. package/src/components/admin/content-forms.tsx +15 -15
  34. package/src/components/admin/forum-forms.tsx +7 -7
  35. package/src/components/admin/group-forms.tsx +5 -5
  36. package/src/components/admin/mail-test-card.tsx +3 -3
  37. package/src/components/admin/marketplace-forms.tsx +4 -4
  38. package/src/components/admin/navigation-forms.tsx +3 -3
  39. package/src/components/admin/plugin-forms.tsx +7 -7
  40. package/src/components/admin/settings-form.tsx +5 -2
  41. package/src/components/admin/theme-forms.tsx +7 -10
  42. package/src/components/auth/form-controls.tsx +23 -1
  43. package/src/components/auth/second-factor-form.tsx +4 -4
  44. package/src/components/content/composer-intents.tsx +7 -6
  45. package/src/components/content/edit-post-form.tsx +7 -7
  46. package/src/components/content/poll-edit-form.tsx +3 -1
  47. package/src/components/content/poll.tsx +4 -2
  48. package/src/components/content/thanks-button.tsx +3 -3
  49. package/src/components/content/thread-rating.tsx +3 -3
  50. package/src/components/messages/message-forms.tsx +15 -15
  51. package/src/components/moderation/inline-moderation-form.tsx +19 -20
  52. package/src/components/moderation/queue-form.tsx +5 -7
  53. package/src/components/moderation/report-forms.tsx +8 -10
  54. package/src/components/moderation/signature-lock-form.tsx +5 -5
  55. package/src/components/moderation/thread-surgery-form.tsx +8 -5
  56. package/src/components/moderation/thread-tools-form.tsx +11 -22
  57. package/src/components/moderation/warning-forms.tsx +6 -6
  58. package/src/components/shell/onboarding-banner.tsx +6 -2
  59. package/src/components/shell/theme-switcher.tsx +6 -6
  60. package/src/components/shell/view-tabs.tsx +12 -7
  61. package/src/server/theme.ts +1 -1
  62. package/src/server/upgrade-notice.ts +1 -1
@@ -5,7 +5,7 @@ import { useActionState } from 'react'
5
5
  import { EMPTY_STATE } from '@/server/auth-form-state'
6
6
  import { messageBulkAction, sendMessageAction } from '@/server/message-actions'
7
7
 
8
- import { FormError } from '../auth/form-controls'
8
+ import { FormError, PendingButton } from '../auth/form-controls'
9
9
  import { MarkdownEditor } from '../content/markdown-editor'
10
10
  import { type Copy, fromCopy } from '../shell/copy'
11
11
 
@@ -89,9 +89,9 @@ export function ComposeForm({
89
89
  </label>
90
90
 
91
91
  <div>
92
- <button type="submit" className={BUTTON}>
92
+ <PendingButton className={BUTTON} showWorking>
93
93
  {fromCopy(copy, 'messageForm.send')}
94
- </button>
94
+ </PendingButton>
95
95
  </div>
96
96
  </form>
97
97
  )
@@ -120,31 +120,31 @@ export function MessageActionBar({
120
120
 
121
121
  {folder === 'inbox' && (
122
122
  <>
123
- <button type="submit" name="command" value="read" className={SECONDARY}>
123
+ <PendingButton name="command" value="read" className={SECONDARY}>
124
124
  {fromCopy(copy, 'messageForm.markRead')}
125
- </button>
126
- <button type="submit" name="command" value="unread" className={SECONDARY}>
125
+ </PendingButton>
126
+ <PendingButton name="command" value="unread" className={SECONDARY}>
127
127
  {fromCopy(copy, 'messageForm.markUnread')}
128
- </button>
128
+ </PendingButton>
129
129
  </>
130
130
  )}
131
131
 
132
132
  {folder === 'trash' ? (
133
133
  <>
134
- <button type="submit" name="command" value="restore" className={SECONDARY}>
134
+ <PendingButton name="command" value="restore" className={SECONDARY}>
135
135
  {fromCopy(copy, 'messageForm.restore')}
136
- </button>
137
- <button type="submit" name="command" value="delete" className={SECONDARY}>
136
+ </PendingButton>
137
+ <PendingButton name="command" value="delete" className={SECONDARY}>
138
138
  {fromCopy(copy, 'messageForm.deleteForever')}
139
- </button>
140
- <button type="submit" name="command" value="empty" className={SECONDARY}>
139
+ </PendingButton>
140
+ <PendingButton name="command" value="empty" className={SECONDARY}>
141
141
  {fromCopy(copy, 'messageForm.emptyTrash')}
142
- </button>
142
+ </PendingButton>
143
143
  </>
144
144
  ) : (
145
- <button type="submit" name="command" value="trash" className={SECONDARY}>
145
+ <PendingButton name="command" value="trash" className={SECONDARY}>
146
146
  {fromCopy(copy, 'messageForm.moveToTrash')}
147
- </button>
147
+ </PendingButton>
148
148
  )}
149
149
  </div>
150
150
  </form>
@@ -7,7 +7,7 @@ import { EMPTY_STATE } from '@/server/auth-form-state'
7
7
  import { inlineModerateAction } from '@/server/inline-moderation-actions'
8
8
  import { splitSelectedAction } from '@/server/surgery-actions'
9
9
 
10
- import { FormError } from '../auth/form-controls'
10
+ import { FormError, PendingButton } from '../auth/form-controls'
11
11
  import { type Copy, fromCopy } from '../shell/copy'
12
12
 
13
13
  const BUTTON =
@@ -70,34 +70,34 @@ export function InlineModerationForm({
70
70
  </span>
71
71
 
72
72
  {rights.approve && (
73
- <button type="submit" name="tool" value="approve" className={BUTTON}>
73
+ <PendingButton name="tool" value="approve" className={BUTTON}>
74
74
  {fromCopy(copy, 'moderationForm.inline.approve')}
75
- </button>
75
+ </PendingButton>
76
76
  )}
77
77
  {rights.lock && scope === 'threads' && (
78
78
  <>
79
- <button type="submit" name="tool" value="lock" className={BUTTON}>
79
+ <PendingButton name="tool" value="lock" className={BUTTON}>
80
80
  {fromCopy(copy, 'moderationForm.tool.lock')}
81
- </button>
82
- <button type="submit" name="tool" value="unlock" className={BUTTON}>
81
+ </PendingButton>
82
+ <PendingButton name="tool" value="unlock" className={BUTTON}>
83
83
  {fromCopy(copy, 'moderationForm.tool.unlock')}
84
- </button>
84
+ </PendingButton>
85
85
  </>
86
86
  )}
87
87
  {rights.stick && scope === 'threads' && (
88
88
  <>
89
- <button type="submit" name="tool" value="stick" className={BUTTON}>
89
+ <PendingButton name="tool" value="stick" className={BUTTON}>
90
90
  {fromCopy(copy, 'moderationForm.tool.pin')}
91
- </button>
92
- <button type="submit" name="tool" value="unstick" className={BUTTON}>
91
+ </PendingButton>
92
+ <PendingButton name="tool" value="unstick" className={BUTTON}>
93
93
  {fromCopy(copy, 'moderationForm.tool.unpin')}
94
- </button>
94
+ </PendingButton>
95
95
  </>
96
96
  )}
97
97
  {rights.restore && (
98
- <button type="submit" name="tool" value="restore" className={BUTTON}>
98
+ <PendingButton name="tool" value="restore" className={BUTTON}>
99
99
  {fromCopy(copy, 'moderationForm.inline.restore')}
100
- </button>
100
+ </PendingButton>
101
101
  )}
102
102
 
103
103
  {rights.move && scope === 'threads' && moveTargets.length > 0 && (
@@ -115,9 +115,9 @@ export function InlineModerationForm({
115
115
  ))}
116
116
  </select>
117
117
  </label>
118
- <button type="submit" name="tool" value="move" className={BUTTON}>
118
+ <PendingButton name="tool" value="move" className={BUTTON}>
119
119
  {fromCopy(copy, 'moderationForm.tool.move')}
120
- </button>
120
+ </PendingButton>
121
121
  </span>
122
122
  )}
123
123
 
@@ -136,21 +136,20 @@ export function InlineModerationForm({
136
136
  className="h-8 w-48 rounded-md border border-border bg-background px-2 text-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
137
137
  />
138
138
  </label>
139
- <button type="submit" formAction={splitAction} className={BUTTON}>
139
+ <PendingButton formAction={splitAction} className={BUTTON}>
140
140
  {fromCopy(copy, 'moderationForm.inline.splitOut')}
141
- </button>
141
+ </PendingButton>
142
142
  </span>
143
143
  )}
144
144
 
145
145
  {rights.delete && (
146
- <button
147
- type="submit"
146
+ <PendingButton
148
147
  name="tool"
149
148
  value="delete"
150
149
  className={`${BUTTON} border-destructive/40 text-destructive`}
151
150
  >
152
151
  {fromCopy(copy, 'moderationForm.inline.delete')}
153
- </button>
152
+ </PendingButton>
154
153
  )}
155
154
  </form>
156
155
  </div>
@@ -5,7 +5,7 @@ import { useActionState } from 'react'
5
5
  import { EMPTY_STATE } from '@/server/auth-form-state'
6
6
  import { moderateQueueAction } from '@/server/moderation-actions'
7
7
 
8
- import { FormError } from '../auth/form-controls'
8
+ import { FormError, PendingButton } from '../auth/form-controls'
9
9
  import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
10
10
 
11
11
  export interface QueueFormRow {
@@ -74,22 +74,20 @@ export function QueueForm({ rows, copy }: { rows: readonly QueueFormRow[]; copy:
74
74
  </ul>
75
75
 
76
76
  <div className="flex flex-wrap gap-3">
77
- <button
78
- type="submit"
77
+ <PendingButton
79
78
  name="decision"
80
79
  value="approve"
81
80
  className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
82
81
  >
83
82
  {fromCopy(copy, 'moderationForm.queue.approve')}
84
- </button>
85
- <button
86
- type="submit"
83
+ </PendingButton>
84
+ <PendingButton
87
85
  name="decision"
88
86
  value="reject"
89
87
  className="inline-flex h-10 items-center justify-center rounded-md border border-destructive/40 px-4 text-sm font-medium text-destructive transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
90
88
  >
91
89
  {fromCopy(copy, 'moderationForm.queue.reject')}
92
- </button>
90
+ </PendingButton>
93
91
  </div>
94
92
  </form>
95
93
  )
@@ -7,7 +7,7 @@ import { Card, CardContent, Field, Textarea } from '@meith/ui'
7
7
  import { EMPTY_STATE } from '@/server/auth-form-state'
8
8
  import { assignReportAction, closeReportAction, fileReportAction } from '@/server/report-actions'
9
9
 
10
- import { FormError, SubmitButton } from '../auth/form-controls'
10
+ import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
11
11
  import { type Copy, fromCopy } from '../shell/copy'
12
12
 
13
13
  export function ReportForm({
@@ -66,14 +66,14 @@ export function AssignReportForm({
66
66
  <FormError message={state.error} />
67
67
  <input type="hidden" name="reportId" value={reportId} />
68
68
  <input type="hidden" name="take" value={mine ? '0' : '1'} />
69
- <button
70
- type="submit"
69
+ <PendingButton
70
+ showWorking
71
71
  className="text-xs font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
72
72
  >
73
73
  {mine
74
74
  ? fromCopy(copy, 'moderationForm.report.putBack')
75
75
  : fromCopy(copy, 'moderationForm.report.take')}
76
- </button>
76
+ </PendingButton>
77
77
  </form>
78
78
  )
79
79
  }
@@ -98,22 +98,20 @@ export function CloseReportForm({ reportId, copy }: { reportId: number; copy: Co
98
98
  />
99
99
  </label>
100
100
  <div className="flex flex-wrap gap-2">
101
- <button
102
- type="submit"
101
+ <PendingButton
103
102
  name="status"
104
103
  value="resolved"
105
104
  className="inline-flex h-8 items-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
106
105
  >
107
106
  {fromCopy(copy, 'moderationForm.report.resolve')}
108
- </button>
109
- <button
110
- type="submit"
107
+ </PendingButton>
108
+ <PendingButton
111
109
  name="status"
112
110
  value="rejected"
113
111
  className="inline-flex h-8 items-center rounded-md border border-border px-3 text-xs font-medium hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
114
112
  >
115
113
  {fromCopy(copy, 'moderationForm.report.dismiss')}
116
- </button>
114
+ </PendingButton>
117
115
  </div>
118
116
  </form>
119
117
  )
@@ -5,7 +5,7 @@ import { useActionState } from 'react'
5
5
  import { EMPTY_STATE } from '@/server/auth-form-state'
6
6
  import { setAvatarLockAction, setSignatureLockAction } from '@/server/moderation-actions'
7
7
 
8
- import { FormError } from '../auth/form-controls'
8
+ import { FormError, PendingButton } from '../auth/form-controls'
9
9
  import { type Copy, fromCopy } from '../shell/copy'
10
10
 
11
11
  const REASON_INPUT = 'h-8 rounded-md border border-border bg-background px-2 text-xs'
@@ -38,11 +38,11 @@ export function SignatureLockForm({
38
38
  />
39
39
  )}
40
40
 
41
- <button type="submit" className={LOCK_BUTTON}>
41
+ <PendingButton showWorking className={LOCK_BUTTON}>
42
42
  {locked
43
43
  ? fromCopy(copy, 'moderationForm.lock.unlockSignature')
44
44
  : fromCopy(copy, 'moderationForm.lock.signature')}
45
- </button>
45
+ </PendingButton>
46
46
  </form>
47
47
  )
48
48
  }
@@ -73,11 +73,11 @@ export function AvatarLockForm({
73
73
  />
74
74
  )}
75
75
 
76
- <button type="submit" className={LOCK_BUTTON}>
76
+ <PendingButton showWorking className={LOCK_BUTTON}>
77
77
  {locked
78
78
  ? fromCopy(copy, 'moderationForm.lock.unlockAvatar')
79
79
  : fromCopy(copy, 'moderationForm.lock.avatar')}
80
- </button>
80
+ </PendingButton>
81
81
  </form>
82
82
  )
83
83
  }
@@ -5,7 +5,7 @@ import { useActionState } from 'react'
5
5
  import { EMPTY_STATE } from '@/server/auth-form-state'
6
6
  import { mergeThreadAction, splitThreadAction } from '@/server/surgery-actions'
7
7
 
8
- import { FormError } from '../auth/form-controls'
8
+ import { FormError, PendingButton } from '../auth/form-controls'
9
9
  import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
10
10
 
11
11
  const BUTTON =
@@ -64,9 +64,9 @@ export function ThreadSurgeryForm({
64
64
  className={`${FIELD} w-48`}
65
65
  />
66
66
  </label>
67
- <button type="submit" className={BUTTON}>
67
+ <PendingButton showWorking className={BUTTON}>
68
68
  {fromCopy(copy, 'moderationForm.surgery.split')}
69
- </button>
69
+ </PendingButton>
70
70
  </div>
71
71
  <FormError message={splitState.error} />
72
72
  </form>
@@ -88,9 +88,12 @@ export function ThreadSurgeryForm({
88
88
  className={`${FIELD} w-40`}
89
89
  />
90
90
  </label>
91
- <button type="submit" className={`${BUTTON} border-destructive/40 text-destructive`}>
91
+ <PendingButton
92
+ showWorking
93
+ className={`${BUTTON} border-destructive/40 text-destructive`}
94
+ >
92
95
  {fromCopy(copy, 'moderationForm.surgery.merge')}
93
- </button>
96
+ </PendingButton>
94
97
  </div>
95
98
  <FormError message={mergeState.error} />
96
99
  </form>
@@ -5,7 +5,7 @@ import { useActionState } from 'react'
5
5
  import { EMPTY_STATE } from '@/server/auth-form-state'
6
6
  import { threadToolAction } from '@/server/thread-tool-actions'
7
7
 
8
- import { FormError } from '../auth/form-controls'
8
+ import { FormError, PendingButton } from '../auth/form-controls'
9
9
  import { type Copy, fromCopy } from '../shell/copy'
10
10
 
11
11
  const BUTTON =
@@ -49,38 +49,27 @@ export function ThreadToolsForm({
49
49
  <input type="hidden" name="threadId" value={threadId} />
50
50
 
51
51
  {rights.lock && (
52
- <button
53
- type="submit"
54
- name="tool"
55
- value={isLocked ? 'unlock' : 'lock'}
56
- className={BUTTON}
57
- >
52
+ <PendingButton name="tool" value={isLocked ? 'unlock' : 'lock'} className={BUTTON}>
58
53
  {isLocked
59
54
  ? fromCopy(copy, 'moderationForm.tool.unlock')
60
55
  : fromCopy(copy, 'moderationForm.tool.lock')}
61
- </button>
56
+ </PendingButton>
62
57
  )}
63
58
  {rights.stick && (
64
- <button
65
- type="submit"
66
- name="tool"
67
- value={isSticky ? 'unstick' : 'stick'}
68
- className={BUTTON}
69
- >
59
+ <PendingButton name="tool" value={isSticky ? 'unstick' : 'stick'} className={BUTTON}>
70
60
  {isSticky
71
61
  ? fromCopy(copy, 'moderationForm.tool.unpin')
72
62
  : fromCopy(copy, 'moderationForm.tool.pin')}
73
- </button>
63
+ </PendingButton>
74
64
  )}
75
65
  {rights.delete && (
76
- <button
77
- type="submit"
66
+ <PendingButton
78
67
  name="tool"
79
68
  value="delete"
80
69
  className={`${BUTTON} border-destructive/40 text-destructive`}
81
70
  >
82
71
  {fromCopy(copy, 'moderationForm.tool.deleteThread')}
83
- </button>
72
+ </PendingButton>
84
73
  )}
85
74
 
86
75
  {rights.move && moveTargets.length > 0 && (
@@ -98,12 +87,12 @@ export function ThreadToolsForm({
98
87
  ))}
99
88
  </select>
100
89
  </label>
101
- <button type="submit" name="tool" value="move" className={BUTTON}>
90
+ <PendingButton name="tool" value="move" className={BUTTON}>
102
91
  {fromCopy(copy, 'moderationForm.tool.move')}
103
- </button>
104
- <button type="submit" name="tool" value="copy" className={BUTTON}>
92
+ </PendingButton>
93
+ <PendingButton name="tool" value="copy" className={BUTTON}>
105
94
  {fromCopy(copy, 'moderationForm.tool.copy')}
106
- </button>
95
+ </PendingButton>
107
96
  </span>
108
97
  )}
109
98
  </form>
@@ -5,7 +5,7 @@ import { useActionState } from 'react'
5
5
  import { EMPTY_STATE } from '@/server/auth-form-state'
6
6
  import { issueWarningAction, revokeWarningAction } from '@/server/warning-actions'
7
7
 
8
- import { FormError } from '../auth/form-controls'
8
+ import { FormError, PendingButton } from '../auth/form-controls'
9
9
  import { type Copy, fromCopy } from '../shell/copy'
10
10
 
11
11
  const FIELD =
@@ -80,9 +80,9 @@ export function IssueWarningForm({
80
80
  </label>
81
81
 
82
82
  <div>
83
- <button type="submit" className={BUTTON}>
83
+ <PendingButton showWorking className={BUTTON}>
84
84
  {fromCopy(copy, 'moderationForm.warn.submit')}
85
- </button>
85
+ </PendingButton>
86
86
  </div>
87
87
  </form>
88
88
  )
@@ -114,12 +114,12 @@ export function RevokeWarningForm({
114
114
  className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
115
115
  />
116
116
  </label>
117
- <button
118
- type="submit"
117
+ <PendingButton
118
+ showWorking
119
119
  className="inline-flex h-8 shrink-0 items-center rounded-md border border-border px-3 text-xs font-medium hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
120
120
  >
121
121
  {fromCopy(copy, 'moderationForm.warn.revoke')}
122
- </button>
122
+ </PendingButton>
123
123
  </form>
124
124
  )
125
125
  }
@@ -1,3 +1,4 @@
1
+ import { PendingButton } from '@/components/auth/form-controls'
1
2
  import { BOARD_MEASURE } from '@/components/shell/measure'
2
3
  import type { OnboardingNoticeModel } from '@/server/onboarding'
3
4
  import { dismissOnboardingAction } from '@/server/onboarding-actions'
@@ -29,9 +30,12 @@ export function OnboardingBanner({
29
30
 
30
31
  <form action={dismissOnboardingAction}>
31
32
  <input type="hidden" name="panel" value={panel} />
32
- <button type="submit" className="text-xs text-muted-foreground hover:text-foreground">
33
+ <PendingButton
34
+ showWorking
35
+ className="text-xs text-muted-foreground hover:text-foreground"
36
+ >
33
37
  {dismissLabel}
34
- </button>
38
+ </PendingButton>
35
39
  </form>
36
40
  </div>
37
41
  </div>
@@ -1,3 +1,4 @@
1
+ import { PendingButton } from '@/components/auth/form-controls'
1
2
  import { MonitorIcon, MoonIcon, SunIcon } from '@/components/shell/appearance-icons'
2
3
  import { BOARD_MEASURE } from '@/components/shell/measure'
3
4
  import { setAppearanceAction } from '@/server/appearance-actions'
@@ -50,12 +51,12 @@ export async function ThemeSwitcher() {
50
51
  </option>
51
52
  ))}
52
53
  </select>
53
- <button
54
- type="submit"
54
+ <PendingButton
55
+ showWorking
55
56
  className="h-8 rounded-md border border-border px-2.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
56
57
  >
57
58
  {t.t('appearance.apply')}
58
- </button>
59
+ </PendingButton>
59
60
  </form>
60
61
  )}
61
62
 
@@ -70,9 +71,8 @@ export async function ThemeSwitcher() {
70
71
  const label = t.t(SCHEME_LABEL_KEY[option])
71
72
  const selected = scheme === option
72
73
  return (
73
- <button
74
+ <PendingButton
74
75
  key={option}
75
- type="submit"
76
76
  name="scheme"
77
77
  value={option}
78
78
  title={label}
@@ -85,7 +85,7 @@ export async function ThemeSwitcher() {
85
85
  >
86
86
  <Icon />
87
87
  <span className="sr-only">{label}</span>
88
- </button>
88
+ </PendingButton>
89
89
  )
90
90
  })}
91
91
  </span>
@@ -21,27 +21,32 @@ export function ViewTabs({
21
21
  if (tabs.length === 0) return null
22
22
 
23
23
  return (
24
- <nav aria-label={label} className={cn('flex items-center gap-x-3', className)}>
25
- <ul className="-my-1.5 flex min-w-0 flex-1 items-center gap-1 overflow-x-auto py-1.5">
24
+ <nav
25
+ aria-label={label}
26
+ className={cn('flex flex-wrap items-center gap-x-3 gap-y-2', className)}
27
+ >
28
+ <ul className="inline-flex max-w-full items-center gap-1 overflow-x-auto rounded-lg border border-border bg-surface p-1">
26
29
  {tabs.map((tab) => (
27
30
  <li key={tab.href} className="shrink-0">
28
31
  <a
29
32
  href={tab.href}
30
33
  {...(tab.isCurrent ? { 'aria-current': 'page' as const } : {})}
31
34
  className={cn(
32
- 'inline-flex h-9 items-center gap-1.5 rounded-full px-3.5 text-sm font-medium whitespace-nowrap transition-colors',
35
+ 'inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-sm whitespace-nowrap transition-colors',
33
36
  'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
34
37
  tab.isCurrent
35
- ? 'bg-primary font-semibold text-primary-foreground'
36
- : 'text-muted-foreground hover:bg-accent hover:text-foreground',
38
+ ? 'bg-card font-semibold text-foreground shadow-sm'
39
+ : 'font-medium text-muted-foreground hover:text-foreground',
37
40
  )}
38
41
  >
39
42
  {tab.label}
40
43
  {tab.count !== undefined && tab.count > 0 && (
41
44
  <span
42
45
  className={cn(
43
- 'rounded-full px-1.5 text-xs font-semibold tabular-nums',
44
- tab.isCurrent ? 'bg-primary-foreground/20' : 'bg-secondary',
46
+ 'rounded px-1.5 text-xs font-semibold tabular-nums',
47
+ tab.isCurrent
48
+ ? 'bg-secondary text-secondary-foreground'
49
+ : 'bg-muted text-muted-foreground',
45
50
  )}
46
51
  >
47
52
  {tab.count}
@@ -25,7 +25,7 @@ export const buildTheme: ResolvedTheme = RESOLVED.get(forumConfig.defaultTheme)!
25
25
 
26
26
  if (buildTheme === undefined) {
27
27
  throw new Error(
28
- `community.config.ts: defaultTheme "${forumConfig.defaultTheme}" fills no slots, so this ` +
28
+ `meith.config.ts: defaultTheme "${forumConfig.defaultTheme}" fills no slots, so this ` +
29
29
  'board cannot render a page. A token-only theme is legitimate as an alternate, ' +
30
30
  'never as the theme a build is made of.',
31
31
  )
@@ -6,7 +6,7 @@ import { planUpgrade, type UpgradeState, upgradeNotice } from '@meith/upgrade'
6
6
 
7
7
  import { activeDefinitions } from './plugin-host'
8
8
 
9
- export const CODE_VERSION = '0.25.0'
9
+ export const CODE_VERSION = '0.26.0'
10
10
 
11
11
  export async function pendingUpgradeNotice(): Promise<string | null> {
12
12
  if (env.DATA_SOURCE !== 'postgres') return null