@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
@@ -24,7 +24,7 @@ import {
24
24
  updateWordFilterAction,
25
25
  } from '@/server/content-admin-actions'
26
26
 
27
- import { FormError, SubmitButton } from '../auth/form-controls'
27
+ import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
28
28
  import { type Copy, fromCopy } from '../shell/copy'
29
29
  import { INPUT, Saved } from './form-bits'
30
30
 
@@ -87,9 +87,9 @@ export function WordFilterRowForm({ filter, copy }: { filter: WordFilterValues;
87
87
 
88
88
  <form action={removeAction}>
89
89
  <input type="hidden" name="id" value={filter.id} />
90
- <button type="submit" className="text-xs text-muted-foreground hover:underline">
90
+ <PendingButton showWorking className="text-xs text-muted-foreground hover:underline">
91
91
  {fromCopy(copy, 'adminContent.filter.removeThis')}
92
- </button>
92
+ </PendingButton>
93
93
  </form>
94
94
  </div>
95
95
  )
@@ -143,9 +143,9 @@ export function DeletePrefixForm({ prefix, copy }: { prefix: PrefixValues; copy:
143
143
  <form action={action} className="flex items-center gap-2">
144
144
  <FormError message={state.error} />
145
145
  <input type="hidden" name="id" value={prefix.id} />
146
- <button type="submit" className="text-xs text-muted-foreground hover:underline">
146
+ <PendingButton showWorking className="text-xs text-muted-foreground hover:underline">
147
147
  {fromCopy(copy, 'admin.remove')}
148
- </button>
148
+ </PendingButton>
149
149
  </form>
150
150
  )
151
151
  }
@@ -252,9 +252,9 @@ export function SmileyRowForm({ smiley, copy }: { smiley: SmileyValues; copy: Co
252
252
 
253
253
  <form action={removeAction}>
254
254
  <input type="hidden" name="id" value={smiley.id} />
255
- <button type="submit" className="text-xs text-destructive hover:underline">
255
+ <PendingButton showWorking className="text-xs text-destructive hover:underline">
256
256
  {fromCopy(copy, 'admin.remove')}
257
- </button>
257
+ </PendingButton>
258
258
  </form>
259
259
  </div>
260
260
  )
@@ -362,9 +362,9 @@ export function DirectiveRowForm({ directive, copy }: { directive: DirectiveValu
362
362
 
363
363
  <form action={removeAction}>
364
364
  <input type="hidden" name="id" value={directive.id} />
365
- <button type="submit" className="text-xs text-destructive hover:underline">
365
+ <PendingButton showWorking className="text-xs text-destructive hover:underline">
366
366
  {fromCopy(copy, 'admin.remove')}
367
- </button>
367
+ </PendingButton>
368
368
  </form>
369
369
  </div>
370
370
  )
@@ -422,9 +422,9 @@ export function DeleteAttachmentForm({ attachmentId, copy }: { attachmentId: num
422
422
  <form action={action} className="shrink-0">
423
423
  <FormError message={state.error} />
424
424
  <input type="hidden" name="id" value={attachmentId} />
425
- <button type="submit" className="text-xs text-destructive hover:underline">
425
+ <PendingButton showWorking className="text-xs text-destructive hover:underline">
426
426
  {fromCopy(copy, 'adminContent.attachment.delete')}
427
- </button>
427
+ </PendingButton>
428
428
  </form>
429
429
  )
430
430
  }
@@ -568,9 +568,9 @@ export function AnnouncementRowForm({
568
568
 
569
569
  <form action={removeAction}>
570
570
  <input type="hidden" name="id" value={announcement.id} />
571
- <button type="submit" className="text-xs text-destructive hover:underline">
571
+ <PendingButton showWorking className="text-xs text-destructive hover:underline">
572
572
  {fromCopy(copy, 'admin.remove')}
573
- </button>
573
+ </PendingButton>
574
574
  </form>
575
575
  </div>
576
576
  )
@@ -670,9 +670,9 @@ export function CaptchaQuestionRowForm({
670
670
 
671
671
  <form action={removeAction}>
672
672
  <input type="hidden" name="id" value={question.id} />
673
- <button type="submit" className="text-xs text-destructive hover:underline">
673
+ <PendingButton showWorking className="text-xs text-destructive hover:underline">
674
674
  {fromCopy(copy, 'admin.remove')}
675
- </button>
675
+ </PendingButton>
676
676
  </form>
677
677
  </div>
678
678
  )
@@ -5,7 +5,7 @@ import { useActionState } from 'react'
5
5
  import type { MatrixCell, MatrixRow } from '@meith/authorization'
6
6
  import { cn, Disclosure } from '@meith/ui'
7
7
 
8
- import { PANEL_CARD, PANEL_LIST, PANEL_ROW } from '@/components/shell/panel-list'
8
+ import { PANEL_CARD, PANEL_LIST, PANEL_NOTE, PANEL_ROW } from '@/components/shell/panel-list'
9
9
  import { EMPTY_STATE } from '@/server/auth-form-state'
10
10
  import {
11
11
  appointModeratorAction,
@@ -17,7 +17,7 @@ import {
17
17
  saveForumPermissionsAction,
18
18
  } from '@/server/forum-admin-actions'
19
19
 
20
- import { FormError, SubmitButton } from '../auth/form-controls'
20
+ import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
21
21
  import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
22
22
  import { INPUT, Saved } from './form-bits'
23
23
 
@@ -534,13 +534,13 @@ export function ModeratorsPanel({
534
534
 
535
535
  return (
536
536
  <section className="flex flex-col gap-4">
537
- <h2 className="text-lg font-semibold tracking-tight">
537
+ <h2 className="font-heading text-lg font-semibold">
538
538
  {fromCopy(copy, 'adminForum.moderators')}
539
539
  </h2>
540
540
 
541
541
  <FormError message={removeState.error} />
542
542
  {moderators.length === 0 ? (
543
- <p className="text-sm text-muted-foreground">{fromCopy(copy, 'adminForum.noModerators')}</p>
543
+ <p className={PANEL_NOTE}>{fromCopy(copy, 'adminForum.noModerators')}</p>
544
544
  ) : (
545
545
  <ul className={PANEL_LIST}>
546
546
  {moderators.map((moderator) => (
@@ -561,12 +561,12 @@ export function ModeratorsPanel({
561
561
  <form action={remove}>
562
562
  <input type="hidden" name="forumId" value={forumId} />
563
563
  <input type="hidden" name="appointmentId" value={moderator.id} />
564
- <button
565
- type="submit"
564
+ <PendingButton
565
+ showWorking
566
566
  className="text-xs text-muted-foreground hover:text-foreground"
567
567
  >
568
568
  {fromCopy(copy, 'admin.remove')}
569
- </button>
569
+ </PendingButton>
570
570
  </form>
571
571
  </li>
572
572
  ))}
@@ -16,7 +16,7 @@ import {
16
16
  updatePromotionRuleAction,
17
17
  } from '@/server/group-admin-actions'
18
18
 
19
- import { FormError, SubmitButton } from '../auth/form-controls'
19
+ import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
20
20
  import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
21
21
  import { INPUT, Saved } from './form-bits'
22
22
  import { OklchPicker } from './oklch-picker'
@@ -579,18 +579,18 @@ export function PromotionRuleRowForm({
579
579
  <form action={toggleAction} className="flex items-center">
580
580
  <input type="hidden" name="id" value={rule.id} />
581
581
  {!rule.enabled && <input type="hidden" name="enabled" value="1" />}
582
- <button type="submit" className="text-xs text-muted-foreground hover:underline">
582
+ <PendingButton className="text-xs text-muted-foreground hover:underline">
583
583
  {rule.enabled
584
584
  ? fromCopy(copy, 'adminGroup.rule.disable')
585
585
  : fromCopy(copy, 'adminGroup.rule.enable')}
586
- </button>
586
+ </PendingButton>
587
587
  </form>
588
588
 
589
589
  <form action={removeAction} className="flex items-center">
590
590
  <input type="hidden" name="id" value={rule.id} />
591
- <button type="submit" className="text-xs text-destructive hover:underline">
591
+ <PendingButton className="text-xs text-destructive hover:underline">
592
592
  {fromCopy(copy, 'admin.remove')}
593
- </button>
593
+ </PendingButton>
594
594
  </form>
595
595
  </div>
596
596
  <p className="text-xs text-muted-foreground">
@@ -6,6 +6,7 @@ import { PANEL_CARD } from '@/components/shell/panel-list'
6
6
  import { EMPTY_STATE, type FormState } from '@/server/auth-form-state'
7
7
  import { sendTestMailAction } from '@/server/mail-test-actions'
8
8
 
9
+ import { PendingButton } from '../auth/form-controls'
9
10
  import { type Copy, fromCopy } from '../shell/copy'
10
11
  import { Saved } from './form-bits'
11
12
 
@@ -66,15 +67,14 @@ export function MailTestCard({
66
67
 
67
68
  <form action={submit} className="flex flex-col gap-2">
68
69
  <div>
69
- <button
70
- type="submit"
70
+ <PendingButton
71
71
  disabled={pending || !sends}
72
72
  className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-60"
73
73
  >
74
74
  {pending
75
75
  ? fromCopy(copy, 'adminPanel.mail.sending')
76
76
  : fromCopy(copy, 'adminPanel.mail.send')}
77
- </button>
77
+ </PendingButton>
78
78
  </div>
79
79
  <p className="text-xs text-muted-foreground">
80
80
  {copy['adminPanel.mail.testNoteLead']}
@@ -5,7 +5,7 @@ import { useActionState } from 'react'
5
5
  import { EMPTY_STATE } from '@/server/auth-form-state'
6
6
  import { refreshMarketplaceAction } from '@/server/marketplace-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
  import { Saved } from './form-bits'
11
11
 
@@ -18,12 +18,12 @@ export function MarketplaceRefreshForm({ copy }: { copy: Copy }) {
18
18
  <Saved when={state.notice === 'refreshed'}>
19
19
  {fromCopy(copy, 'adminPanel.marketplace.refreshed')}
20
20
  </Saved>
21
- <button
22
- type="submit"
21
+ <PendingButton
22
+ showWorking
23
23
  className="inline-flex h-9 w-fit items-center justify-center rounded-md border border-border px-3 text-sm font-medium transition-colors hover:border-primary hover:bg-accent"
24
24
  >
25
25
  {fromCopy(copy, 'adminPanel.marketplace.refresh')}
26
- </button>
26
+ </PendingButton>
27
27
  </form>
28
28
  )
29
29
  }
@@ -9,7 +9,7 @@ import {
9
9
  updateNavigationItemAction,
10
10
  } from '@/server/navigation-admin-actions'
11
11
 
12
- import { FormError, SubmitButton } from '../auth/form-controls'
12
+ import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
13
13
  import { type Copy, fromCopy } from '../shell/copy'
14
14
  import { INPUT, Saved } from './form-bits'
15
15
 
@@ -193,9 +193,9 @@ export function NavigationItemRowForm({
193
193
 
194
194
  <form action={removeAction}>
195
195
  <input type="hidden" name="id" value={item.id} />
196
- <button type="submit" className="text-xs text-destructive hover:underline">
196
+ <PendingButton showWorking className="text-xs text-destructive hover:underline">
197
197
  {fromCopy(copy, 'adminNavigation.removeThis')}
198
- </button>
198
+ </PendingButton>
199
199
  </form>
200
200
  </div>
201
201
  )
@@ -9,7 +9,7 @@ import {
9
9
  setPluginEnabledAction,
10
10
  } from '@/server/plugin-admin-actions'
11
11
 
12
- import { FormError, SubmitButton } from '../auth/form-controls'
12
+ import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
13
13
  import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
14
14
  import { INPUT, Saved } from './form-bits'
15
15
 
@@ -44,14 +44,14 @@ export function PluginEnableForm({
44
44
  <FormError message={state.error} />
45
45
  <input type="hidden" name="key" value={pluginKey} />
46
46
  <input type="hidden" name="enabled" value={enabled ? '0' : '1'} />
47
- <button
48
- type="submit"
47
+ <PendingButton
48
+ showWorking
49
49
  className="inline-flex h-9 items-center justify-center rounded-md border border-border px-3 text-sm"
50
50
  >
51
51
  {enabled
52
52
  ? fromCopy(copy, 'adminPanel.plugin.disable')
53
53
  : fromCopy(copy, 'adminPanel.plugin.enable')}
54
- </button>
54
+ </PendingButton>
55
55
  </form>
56
56
  )
57
57
  }
@@ -63,12 +63,12 @@ export function PluginHealthResetForm({ pluginKey, copy }: { pluginKey: string;
63
63
  <form action={action} className="flex flex-col gap-2">
64
64
  <FormError message={state.error} />
65
65
  <input type="hidden" name="key" value={pluginKey} />
66
- <button
67
- type="submit"
66
+ <PendingButton
67
+ showWorking
68
68
  className="inline-flex h-9 items-center justify-center rounded-md border border-border px-3 text-sm"
69
69
  >
70
70
  {fromCopy(copy, 'adminPanel.plugin.clearFailures')}
71
- </button>
71
+ </PendingButton>
72
72
  </form>
73
73
  )
74
74
  }
@@ -2,12 +2,15 @@
2
2
 
3
3
  import { useActionState } from 'react'
4
4
 
5
+ import { cn } from '@meith/ui'
6
+
5
7
  import { saveAdminSettingsAction } from '@/server/admin-settings-actions'
6
8
  import { EMPTY_STATE } from '@/server/auth-form-state'
7
9
  import type { SettingFieldModel, SettingGroupModel } from '@/view/admin-settings'
8
10
 
9
11
  import { FormError, SubmitButton } from '../auth/form-controls'
10
12
  import { type Copy, fromCopy } from '../shell/copy'
13
+ import { PANEL_CARD } from '../shell/panel-list'
11
14
  import { INPUT, Saved } from './form-bits'
12
15
 
13
16
  function Control({ setting, copy }: { setting: SettingFieldModel; copy: Copy }) {
@@ -105,8 +108,8 @@ export function AdminSettingsForm({
105
108
  <input type="hidden" name="keys" value={keys.join(',')} />
106
109
 
107
110
  {groups.map((group) => (
108
- <section key={group.group} className="flex flex-col gap-4">
109
- <h2 className="text-lg font-semibold tracking-tight">{group.label}</h2>
111
+ <section key={group.group} className={cn(PANEL_CARD, 'gap-5')}>
112
+ <h2 className="font-heading text-lg font-semibold">{group.label}</h2>
110
113
  <div className="flex flex-col gap-5">
111
114
  {group.settings.map((setting) => (
112
115
  <div key={setting.key} className="flex flex-col gap-1">
@@ -29,7 +29,7 @@ import {
29
29
  } from '@/view/theme-draft'
30
30
  import { BRAND_PRESETS, groupTokens } from '@/view/theme-tokens'
31
31
 
32
- import { FormError, SubmitButton } from '../auth/form-controls'
32
+ import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
33
33
  import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
34
34
  import { Saved } from './form-bits'
35
35
  import { OklchPicker } from './oklch-picker'
@@ -473,14 +473,13 @@ export function ThemeEditorForm({
473
473
  : formatFromCopy(copy, 'adminTheme.saveChanges', { count: unsaved })}
474
474
  </SubmitButton>
475
475
  </span>
476
- <button
477
- type="submit"
476
+ <PendingButton
478
477
  name="intent"
479
478
  value="preview"
480
479
  className="inline-flex h-10 items-center justify-center rounded-md border border-border px-4 text-sm"
481
480
  >
482
481
  {fromCopy(copy, 'adminTheme.previewWithoutSaving')}
483
- </button>
482
+ </PendingButton>
484
483
  <span className="text-xs text-muted-foreground">
485
484
  {unsaved === 0
486
485
  ? fromCopy(copy, 'adminTheme.nothingToSave')
@@ -593,13 +592,12 @@ export function ThemeStateForms({
593
592
  {!isDefault && enabled && (
594
593
  <form action={defaultAction}>
595
594
  <input type="hidden" name="key" value={themeKey} />
596
- <button
597
- type="submit"
595
+ <PendingButton
598
596
  aria-label={formatFromCopy(copy, 'adminTheme.state.makeDefaultFor', { title })}
599
597
  className={GHOST_BUTTON}
600
598
  >
601
599
  {fromCopy(copy, 'adminTheme.state.makeDefault')}
602
- </button>
600
+ </PendingButton>
603
601
  </form>
604
602
  )}
605
603
 
@@ -607,8 +605,7 @@ export function ThemeStateForms({
607
605
  <form action={enabledAction}>
608
606
  <input type="hidden" name="key" value={themeKey} />
609
607
  <input type="hidden" name="enabled" value={enabled ? 'false' : 'true'} />
610
- <button
611
- type="submit"
608
+ <PendingButton
612
609
  aria-label={formatFromCopy(
613
610
  copy,
614
611
  enabled ? 'adminTheme.state.turnOffFor' : 'adminTheme.state.turnOnFor',
@@ -619,7 +616,7 @@ export function ThemeStateForms({
619
616
  {enabled
620
617
  ? fromCopy(copy, 'adminTheme.state.turnOff')
621
618
  : fromCopy(copy, 'adminTheme.state.turnOn')}
622
- </button>
619
+ </PendingButton>
623
620
  </form>
624
621
  )}
625
622
 
@@ -3,7 +3,7 @@
3
3
  import { useEffect, useRef } from 'react'
4
4
  import { useFormStatus } from 'react-dom'
5
5
 
6
- import { Alert, AlertDescription, AlertTitle, Input, Field as UiField } from '@meith/ui'
6
+ import { Alert, AlertDescription, AlertTitle, cn, Input, Field as UiField } from '@meith/ui'
7
7
  import { Button } from '@meith/ui/button'
8
8
 
9
9
  import { fromCopy, useCopy } from '../shell/copy'
@@ -31,6 +31,28 @@ export function SubmitButton({
31
31
  )
32
32
  }
33
33
 
34
+ export function PendingButton({
35
+ children,
36
+ showWorking = false,
37
+ className,
38
+ ...props
39
+ }: React.ComponentProps<'button'> & { showWorking?: boolean }) {
40
+ const { pending } = useFormStatus()
41
+ const copy = useCopy()
42
+
43
+ return (
44
+ <button
45
+ {...props}
46
+ type="submit"
47
+ disabled={pending || props.disabled}
48
+ aria-busy={pending || undefined}
49
+ className={cn(className, 'disabled:cursor-not-allowed disabled:opacity-60')}
50
+ >
51
+ {pending && showWorking ? fromCopy(copy, 'form.working') : children}
52
+ </button>
53
+ )
54
+ }
55
+
34
56
  export function useFocusOnFail<T extends HTMLElement>(failed: boolean) {
35
57
  const { pending } = useFormStatus()
36
58
  const ref = useRef<T>(null)
@@ -2,13 +2,13 @@
2
2
 
3
3
  import { useActionState } from 'react'
4
4
 
5
- import { Button } from '@meith/ui/button'
5
+ import { buttonVariants } from '@meith/ui'
6
6
 
7
7
  import { abandonSecondFactorAction, verifySecondFactorAction } from '@/server/auth-actions'
8
8
  import { EMPTY_STATE } from '@/server/auth-form-state'
9
9
 
10
10
  import { type Copy, fromCopy } from '../shell/copy'
11
- import { FormError, SubmitButton } from './form-controls'
11
+ import { FormError, PendingButton, SubmitButton } from './form-controls'
12
12
  import { OtpField, otpRecoveryFromCopy } from './otp-field'
13
13
 
14
14
  export function SecondFactorForm({
@@ -43,9 +43,9 @@ export function SecondFactorForm({
43
43
  </form>
44
44
 
45
45
  <form action={abandonSecondFactorAction}>
46
- <Button type="submit" variant="ghost" size="sm">
46
+ <PendingButton showWorking className={buttonVariants({ variant: 'ghost', size: 'sm' })}>
47
47
  {fromCopy(copy, 'authForm.secondFactor.cancel')}
48
- </Button>
48
+ </PendingButton>
49
49
  </form>
50
50
  </div>
51
51
  )
@@ -1,27 +1,28 @@
1
1
  import { buttonVariants } from '@meith/ui'
2
2
 
3
+ import { PendingButton } from '../auth/form-controls'
3
4
  import { fromCopy, useCopy } from '../shell/copy'
4
5
 
5
6
  export function ComposerIntents() {
6
7
  const copy = useCopy()
7
8
  return (
8
9
  <>
9
- <button
10
- type="submit"
10
+ <PendingButton
11
11
  name="intent"
12
12
  value="preview"
13
+ showWorking
13
14
  className={buttonVariants({ variant: 'outline' })}
14
15
  >
15
16
  {fromCopy(copy, 'composer.preview')}
16
- </button>
17
- <button
18
- type="submit"
17
+ </PendingButton>
18
+ <PendingButton
19
19
  name="intent"
20
20
  value="save_draft"
21
+ showWorking
21
22
  className={buttonVariants({ variant: 'ghost' })}
22
23
  >
23
24
  {fromCopy(copy, 'composer.saveDraft')}
24
- </button>
25
+ </PendingButton>
25
26
  </>
26
27
  )
27
28
  }
@@ -6,7 +6,7 @@ import { useActionState } from 'react'
6
6
  import { EMPTY_STATE } from '@/server/auth-form-state'
7
7
  import { deletePostAction, editPostAction, restorePostAction } from '@/server/content-actions'
8
8
 
9
- import { FormError, SubmitButton } from '../auth/form-controls'
9
+ import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
10
10
  import { type Copy, fromCopy } from '../shell/copy'
11
11
  import { MarkdownEditor } from './markdown-editor'
12
12
 
@@ -53,14 +53,14 @@ export function EditPostForm({
53
53
 
54
54
  <div className="flex flex-wrap gap-3">
55
55
  <SubmitButton>{fromCopy(copy, 'composer.edit.submit')}</SubmitButton>
56
- <button
57
- type="submit"
56
+ <PendingButton
58
57
  name="intent"
59
58
  value="preview"
59
+ showWorking
60
60
  className="inline-flex h-10 items-center justify-center rounded-md border border-border px-4 text-sm font-medium transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
61
61
  >
62
62
  {fromCopy(copy, 'composer.preview')}
63
- </button>
63
+ </PendingButton>
64
64
  </div>
65
65
  </form>
66
66
  )
@@ -84,12 +84,12 @@ export function DeletePostForm({
84
84
  <input type="hidden" name="postId" value={postId} />
85
85
  <p className="text-sm text-muted-foreground">{fromCopy(copy, 'composer.edit.deleteBlurb')}</p>
86
86
  <div>
87
- <button
88
- type="submit"
87
+ <PendingButton
88
+ showWorking
89
89
  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
90
  >
91
91
  {fromCopy(copy, 'composer.edit.delete')}
92
- </button>
92
+ </PendingButton>
93
93
  </div>
94
94
  </form>
95
95
  )
@@ -3,6 +3,8 @@ import { POLL_OPTION_LENGTH_MAX, POLL_QUESTION_MAX, type Poll } from '@meith/pol
3
3
  import { getTranslator } from '@/server/i18n'
4
4
  import { editPollAction } from '@/server/poll-actions'
5
5
 
6
+ import { PendingButton } from '../auth/form-controls'
7
+
6
8
  const NEW_OPTION_SLOTS = ['first', 'second', 'third'] as const
7
9
 
8
10
  function inputValue(at: Date | null): string {
@@ -89,7 +91,7 @@ export async function PollEditForm({ poll, threadId }: { poll: Poll; threadId: n
89
91
  </label>
90
92
 
91
93
  <div>
92
- <button type="submit">{t.t('pollEdit.save')}</button>
94
+ <PendingButton showWorking>{t.t('pollEdit.save')}</PendingButton>
93
95
  </div>
94
96
  </form>
95
97
  )
@@ -3,6 +3,8 @@ import { POLL_CHOICES_UNLIMITED, type Poll } from '@meith/polls'
3
3
  import { getTranslator } from '@/server/i18n'
4
4
  import { votePollAction } from '@/server/poll-actions'
5
5
 
6
+ import { PendingButton } from '../auth/form-controls'
7
+
6
8
  export async function PollForm({
7
9
  poll,
8
10
  threadId,
@@ -69,9 +71,9 @@ export async function PollForm({
69
71
  </div>
70
72
  ))}
71
73
  {mayCast ? (
72
- <button type="submit">
74
+ <PendingButton showWorking>
73
75
  {hasVoted ? t.t('pollForm.changeVote') : t.t('pollForm.vote')}
74
- </button>
76
+ </PendingButton>
75
77
  ) : (
76
78
  <p className="text-sm text-muted-foreground">
77
79
  {closed ? t.t('pollForm.closed') : t.t('pollForm.votes', { count: total })}
@@ -7,6 +7,7 @@ import { cn } from '@meith/ui'
7
7
  import { EMPTY_STATE } from '@/server/auth-form-state'
8
8
  import { thankForPostAction } from '@/server/reputation-actions'
9
9
 
10
+ import { PendingButton } from '../auth/form-controls'
10
11
  import { formatFromCopy, fromCopy, useCopy } from '../shell/copy'
11
12
 
12
13
  export function ThanksButton({
@@ -31,8 +32,7 @@ export function ThanksButton({
31
32
  <input type="hidden" name="userId" value={authorUserId} />
32
33
  <input type="hidden" name="returnTo" value={returnTo} />
33
34
 
34
- <button
35
- type="submit"
35
+ <PendingButton
36
36
  aria-pressed={thanked}
37
37
  className={cn(
38
38
  'inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-sm font-medium transition-colors',
@@ -55,7 +55,7 @@ export function ThanksButton({
55
55
  </span>
56
56
  </span>
57
57
  )}
58
- </button>
58
+ </PendingButton>
59
59
 
60
60
  {state.error !== undefined && (
61
61
  <span className="ml-2 text-xs text-destructive">{state.error}</span>
@@ -3,6 +3,7 @@ import { cn } from '@meith/ui'
3
3
 
4
4
  import { rateThreadAction } from '@/server/thread-rating-actions'
5
5
 
6
+ import { PendingButton } from '../auth/form-controls'
6
7
  import { type Copy, formatFromCopy, fromCopy } from '../shell/copy-record'
7
8
 
8
9
  const STAR_PATH = 'M10 1.6l2.47 5.01 5.53.8-4 3.9.94 5.5L10 14.21l-4.94 2.6.94-5.5-4-3.9 5.53-.8z'
@@ -109,9 +110,8 @@ export function ThreadRatingForm({
109
110
  const chosen = rating.mine !== null && value <= rating.mine
110
111
 
111
112
  return (
112
- <button
113
+ <PendingButton
113
114
  key={value}
114
- type="submit"
115
115
  name="rating"
116
116
  value={value}
117
117
  aria-label={
@@ -126,7 +126,7 @@ export function ThreadRatingForm({
126
126
  )}
127
127
  >
128
128
  <Star fill={chosen ? 1 : 0} className="size-5" />
129
- </button>
129
+ </PendingButton>
130
130
  )
131
131
  })}
132
132
  </span>